mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df561a6211 | ||
|
|
50d3404f3b | ||
|
|
607fc8d338 | ||
|
|
8c366dfc38 | ||
|
|
5ed08fa9ab | ||
|
|
ddb557ebef | ||
|
|
b48615df3f | ||
|
|
86c7b92675 | ||
|
|
041f362cc4 | ||
|
|
65f491325e | ||
|
|
57aef5f5e5 | ||
|
|
e704e6fa04 | ||
|
|
4c99bbb58a | ||
|
|
c0647eef2c | ||
|
|
c619b9be93 | ||
|
|
abab3f9688 | ||
|
|
7b15111e8f | ||
|
|
2c7f38c503 | ||
|
|
ed80df1372 | ||
|
|
3614d1796c |
No files matched your search
@@ -75,3 +75,6 @@ Rails/SkipsModelValidations:
|
|||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Capybara/ClickLinkOrButtonStyle:
|
||||||
|
Enabled: false
|
||||||
+2
-1
@@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test"
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation
|
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ COPY LICENSE README.md Rakefile config.ru ./
|
|||||||
|
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
|
RUN ln -s /fonts /app/public/fonts
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
|
|||||||
+13
-15
@@ -98,7 +98,6 @@ GEM
|
|||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.1.1)
|
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.19)
|
||||||
better_html (2.0.2)
|
better_html (2.0.2)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
@@ -294,7 +293,7 @@ GEM
|
|||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.4)
|
||||||
minitest (5.19.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
@@ -345,7 +344,7 @@ GEM
|
|||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.0.4)
|
pagy (6.0.4)
|
||||||
parallel (1.23.0)
|
parallel (1.23.0)
|
||||||
parser (3.2.2.3)
|
parser (3.2.2.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.11.0)
|
pdf-reader (2.11.0)
|
||||||
@@ -418,7 +417,7 @@ GEM
|
|||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
redis-client (0.16.0)
|
redis-client (0.16.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.2)
|
||||||
reline (0.3.8)
|
reline (0.3.8)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
@@ -455,33 +454,32 @@ GEM
|
|||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.12)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.12)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rubocop (1.56.1)
|
rubocop (1.57.2)
|
||||||
base64 (~> 0.1.1)
|
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.3)
|
parser (>= 3.2.2.4)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.28.1, < 2.0)
|
rubocop-ast (>= 1.28.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.30.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-capybara (2.18.0)
|
rubocop-capybara (2.19.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.23.1)
|
rubocop-factory_bot (2.24.0)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-performance (1.19.0)
|
rubocop-performance (1.19.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 0.4.0)
|
||||||
rubocop-rails (2.20.2)
|
rubocop-rails (2.22.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-rspec (2.23.2)
|
rubocop-rspec (2.25.0)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
@@ -537,7 +535,7 @@ GEM
|
|||||||
tzinfo-data (1.2023.3)
|
tzinfo-data (1.2023.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
version_gem (1.1.3)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
FORMAT = Templates::ProcessDocument::FORMAT
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if Docuseal.multitenant?
|
|
||||||
Rollbar.warning('load page')
|
|
||||||
|
|
||||||
return head :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SsoSettingsController < ApplicationController
|
||||||
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
authorize_resource :encrypted_config, parent: false, except: :index
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -57,9 +57,18 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template.update!(deleted_at: Time.current)
|
notice =
|
||||||
|
if !Docuseal.multitenant? && params[:permanently].present?
|
||||||
|
@template.destroy!
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
'Template has been removed.'
|
||||||
|
else
|
||||||
|
@template.update!(deleted_at: Time.current)
|
||||||
|
|
||||||
|
'Template has been archived.'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path, notice:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -3,20 +3,55 @@
|
|||||||
class TemplatesUploadsController < ApplicationController
|
class TemplatesUploadsController < ApplicationController
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
layout 'plain'
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
|
|
||||||
@template.account = current_account
|
@template.account = current_account
|
||||||
@template.author = current_user
|
@template.author = current_user
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
@template.name = File.basename(params[:files].first.original_filename, '.*')
|
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
@template.save!
|
@template.save!
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to root_path, alert: 'Unable to upload file'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_file_params_from_url
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(conn.get(params[:url]).body)
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename: File.basename(
|
||||||
|
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
||||||
|
),
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
{ files: [file] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def conn
|
||||||
|
Faraday.new do |faraday|
|
||||||
|
faraday.response :follow_redirects
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -12,7 +12,7 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
cert_data = if Docuseal.multitenant?
|
cert_data = if Docuseal.multitenant?
|
||||||
Docuseal::CERTS
|
Docuseal::CERTS
|
||||||
else
|
else
|
||||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
|
|||||||
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
|
import SubmitForm from './elements/submit_form'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
|||||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl"
|
||||||
|
>{{ field.name || t('date') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
|
@click.prevent="setCurrentDate"
|
||||||
|
>
|
||||||
|
<IconCalendarCheck :width="16" />
|
||||||
|
{{ t('set_today') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="value"
|
||||||
|
class="base-input !text-2xl text-center w-full"
|
||||||
|
:required="field.required"
|
||||||
|
type="date"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DateStep',
|
||||||
|
components: {
|
||||||
|
IconCalendarCheck
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCurrentDate () {
|
||||||
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -65,34 +65,13 @@
|
|||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentField.type === 'date'">
|
<DateStep
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
v-else-if="currentField.type === 'date'"
|
||||||
<label
|
:key="currentField.uuid"
|
||||||
:for="currentField.uuid"
|
v-model="values[currentField.uuid]"
|
||||||
class="label text-2xl"
|
:field="currentField"
|
||||||
>{{ currentField.name || t('date') }}
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
/>
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
|
||||||
@click.prevent="setCurrentDate"
|
|
||||||
>
|
|
||||||
<IconCalendarCheck :width="16" />
|
|
||||||
{{ t('set_today') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl text-center w-full"
|
|
||||||
:required="currentField.required"
|
|
||||||
type="date"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
v-if="currentField.name"
|
||||||
@@ -242,6 +221,7 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:previous-value="previousSignatureValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -255,6 +235,7 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:previous-value="previousInitialsValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -350,8 +331,9 @@ import AttachmentStep from './attachment_step'
|
|||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2, IconCalendarCheck } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { t } from './i18n'
|
import { t } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -364,10 +346,10 @@ export default {
|
|||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
|
DateStep,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
IconCalendarCheck,
|
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
@@ -392,6 +374,13 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
onComplete: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -471,6 +460,16 @@ export default {
|
|||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
|
previousSignatureValue () {
|
||||||
|
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[signatureField?.uuid]
|
||||||
|
},
|
||||||
|
previousInitialsValue () {
|
||||||
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[initialsField?.uuid]
|
||||||
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||||
},
|
},
|
||||||
@@ -635,13 +634,6 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setCurrentDate () {
|
|
||||||
const inputEl = document.getElementById(this.currentField.uuid)
|
|
||||||
|
|
||||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
|
||||||
|
|
||||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
|
||||||
},
|
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
@@ -696,6 +688,12 @@ export default {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.isCompleted = true
|
this.isCompleted = true
|
||||||
|
|
||||||
|
const respData = await response.text()
|
||||||
|
|
||||||
|
if (respData) {
|
||||||
|
this.onComplete(JSON.parse(respData))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.error('Error submitting form:', error)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
@@ -51,26 +51,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
:required="field.required && !isInitialsStarted && !modelValue"
|
:required="field.required && !isInitialsStarted"
|
||||||
:placeholder="`${t('type_initial_here')}...`"
|
:placeholder="`${t('type_initial_here')}...`"
|
||||||
type="text"
|
type="text"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
@@ -110,6 +111,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -119,10 +125,20 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isInitialsStarted: false,
|
isInitialsStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isDrawInitials: false
|
isDrawInitials: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -137,8 +153,6 @@ export default {
|
|||||||
import('@rails/activestorage')
|
import('@rails/activestorage')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -152,6 +166,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isInitialsStarted = false
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -193,7 +210,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||||
<a
|
<a
|
||||||
|
v-if="!defaultValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="link"
|
class="link"
|
||||||
@click.prevent="isCodeSent = false"
|
@click.prevent="isCodeSent = false"
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="phone"
|
ref="phone"
|
||||||
:value="modelValue || defaultValue"
|
:value="modelValue || defaultValue"
|
||||||
|
:readonly="!!defaultValue"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
pattern="^\+[0-9\s\-]+$"
|
pattern="^\+[0-9\s\-]+$"
|
||||||
|
|||||||
@@ -9,14 +9,15 @@
|
|||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Type text"
|
data-tip="Type text"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-sm btn-circle"
|
||||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
@@ -34,23 +35,26 @@
|
|||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('redraw') }}
|
{{ t('redraw') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
|
href="#"
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
@@ -59,21 +63,21 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded"
|
||||||
/>
|
/>
|
||||||
@@ -93,6 +97,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
|
let isFontLoaded = false
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignatureStep',
|
name: 'SignatureStep',
|
||||||
@@ -122,6 +129,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -131,10 +143,20 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: false,
|
isSignatureStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isTextSignature: false
|
isTextSignature: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -147,8 +169,6 @@ export default {
|
|||||||
import('@rails/activestorage')
|
import('@rails/activestorage')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -162,6 +182,22 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isSignatureStarted = false
|
||||||
|
},
|
||||||
|
loadFont () {
|
||||||
|
if (!isFontLoaded) {
|
||||||
|
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
|
||||||
|
|
||||||
|
font.load().then((loadedFont) => {
|
||||||
|
document.fonts.add(loadedFont)
|
||||||
|
|
||||||
|
isFontLoaded = true
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Font loading failed:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -178,8 +214,8 @@ export default {
|
|||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const fontFamily = 'Arial'
|
const fontFamily = 'Dancing Script'
|
||||||
const fontSize = '44px'
|
const fontSize = '38px'
|
||||||
const fontStyle = 'italic'
|
const fontStyle = 'italic'
|
||||||
const fontWeight = ''
|
const fontWeight = ''
|
||||||
|
|
||||||
@@ -197,6 +233,8 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
this.$refs.textInput.focus()
|
||||||
|
|
||||||
|
this.loadFont()
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -250,7 +288,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
:href="`/templates/${template.id}/submissions/new`"
|
:href="`/templates/${template.id}/submissions/new`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="btn btn-primary text-base"
|
class="btn btn-primary text-base"
|
||||||
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconUsersPlus
|
<IconUsersPlus
|
||||||
width="20"
|
width="20"
|
||||||
@@ -691,14 +692,25 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
onSaveClick () {
|
maybeShowEmptyTemplateAlert (e) {
|
||||||
this.isSaving = true
|
if (!this.template.fields.length) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
this.save().then(() => {
|
alert('Please draw fields to prepare the document.')
|
||||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
}
|
||||||
}).finally(() => {
|
},
|
||||||
this.isSaving = false
|
onSaveClick () {
|
||||||
})
|
if (this.template.fields.length) {
|
||||||
|
this.isSaving = true
|
||||||
|
|
||||||
|
this.save().then(() => {
|
||||||
|
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isSaving = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
alert('Please draw fields to prepare the document.')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group"
|
class="border border-base-300 rounded rounded-tr-none relative group"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative">
|
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-between"
|
class="cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 w-full flex justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -87,12 +87,11 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
documents.each do |attachment|
|
documents.each do |attachment|
|
||||||
data = attachment.download
|
total_size += attachment.byte_size
|
||||||
total_size += data.size
|
|
||||||
|
|
||||||
break if total_size >= MAX_ATTACHMENTS_SIZE
|
break if total_size >= MAX_ATTACHMENTS_SIZE
|
||||||
|
|
||||||
attachments[attachment.filename.to_s] = data
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
end
|
end
|
||||||
|
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
|
|||||||
@@ -41,12 +41,12 @@
|
|||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
<% unless Docuseal.multitenant? %>
|
||||||
<li>
|
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) %>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<li>
|
||||||
Plans
|
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
<% end %>
|
</li>
|
||||||
</li>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||||
<% if can?(:read, AccessToken) %>
|
<% if can?(:read, AccessToken) %>
|
||||||
@@ -66,6 +66,13 @@
|
|||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/settings_nav_extra' %>
|
<%= render 'shared/settings_nav_extra' %>
|
||||||
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
|
Plans
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="alert">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Single Sign On with SAML 2.0</p>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Unlock with DocuSeal Enterprise
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">SMS</h1>
|
||||||
|
<%= render 'placeholder' %>
|
||||||
|
</div>
|
||||||
|
<div class="w-0 md:w-52"></div>
|
||||||
|
</div>
|
||||||
@@ -52,9 +52,9 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !template.deleted_at? && can?(:destroy, template) %>
|
<% if (!template.deleted_at? || !Docuseal.multitenant?) && can?(:destroy, template) %>
|
||||||
<span class="tooltip tooltip-left" data-tip="Archive">
|
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Remove' : 'Archive' %>">
|
||||||
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
||||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="h-screen">
|
||||||
|
<div
|
||||||
|
class="text-center p-8 h-full flex items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %>
|
||||||
|
<span>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<submit-form>
|
||||||
|
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %>
|
||||||
|
<button type="submit"></button>
|
||||||
|
<input name="url" value="<%= params[:url] %>">
|
||||||
|
<input name="filename" value="<%= params[:filename] %>">
|
||||||
|
<% end %>
|
||||||
|
</submit-form>
|
||||||
@@ -59,7 +59,7 @@ Rails.application.configure do
|
|||||||
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
|
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
|
||||||
|
|
||||||
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
||||||
config.force_ssl = ENV['FORCE_SSL'] == 'true'
|
config.force_ssl = ENV['FORCE_SSL'].present?
|
||||||
|
|
||||||
# Include generic and useful information about system operation, but avoid logging too much
|
# Include generic and useful information about system operation, but avoid logging too much
|
||||||
# information to avoid inadvertent exposure of personally identifiable information (PII).
|
# information to avoid inadvertent exposure of personally identifiable information (PII).
|
||||||
|
|||||||
@@ -66,4 +66,16 @@ Rails.application.configure do
|
|||||||
deterministic_key: 'test deterministic key',
|
deterministic_key: 'test deterministic key',
|
||||||
key_derivation_salt: 'test key derivation salt'
|
key_derivation_salt: 'test key derivation salt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.middleware.use(Class.new do
|
||||||
|
def initialize(app)
|
||||||
|
@app = app
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
return [200, {}, []] if env['PATH_INFO'].starts_with?('/fonts')
|
||||||
|
|
||||||
|
@app.call(env)
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
+1
-1
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
|
|||||||
environment ENV.fetch('RAILS_ENV', 'development')
|
environment ENV.fetch('RAILS_ENV', 'development')
|
||||||
|
|
||||||
# Specifies the `pidfile` that Puma will use.
|
# Specifies the `pidfile` that Puma will use.
|
||||||
# pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
|
pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
|
||||||
|
|
||||||
# Specifies the number of `workers` to boot in clustered mode.
|
# Specifies the number of `workers` to boot in clustered mode.
|
||||||
# Workers are forked web server processes. If using threads and workers together
|
# Workers are forked web server processes. If using threads and workers together
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ Rails.application.routes.draw do
|
|||||||
resources :submissions, only: %i[show destroy]
|
resources :submissions, only: %i[show destroy]
|
||||||
resources :console_redirect, only: %i[index]
|
resources :console_redirect, only: %i[index]
|
||||||
resource :templates_upload, only: %i[create]
|
resource :templates_upload, only: %i[create]
|
||||||
|
authenticated do
|
||||||
|
resource :templates_upload, only: %i[show], path: 'new'
|
||||||
|
end
|
||||||
resources :templates_archived, only: %i[index], path: 'archived'
|
resources :templates_archived, only: %i[index], path: 'archived'
|
||||||
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
|
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
|
||||||
resources :templates, only: %i[new create edit show destroy] do
|
resources :templates, only: %i[new create edit show destroy] do
|
||||||
@@ -92,6 +95,7 @@ Rails.application.routes.draw do
|
|||||||
resources :storage, only: %i[index create], controller: 'storage_settings'
|
resources :storage, only: %i[index create], controller: 'storage_settings'
|
||||||
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
||||||
resources :sms, only: %i[index], controller: 'sms_settings'
|
resources :sms, only: %i[index], controller: 'sms_settings'
|
||||||
|
resources :sso, only: %i[index], controller: 'sso_settings'
|
||||||
end
|
end
|
||||||
resources :notifications, only: %i[index create], controller: 'notifications_settings'
|
resources :notifications, only: %i[index create], controller: 'notifications_settings'
|
||||||
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
|
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class AddFolderIdToTemplates < ActiveRecord::Migration[7.0]
|
|||||||
def up
|
def up
|
||||||
add_reference :templates, :folder, foreign_key: { to_table: :template_folders }, index: true, null: true
|
add_reference :templates, :folder, foreign_key: { to_table: :template_folders }, index: true, null: true
|
||||||
|
|
||||||
MigrationAccount.all.pluck(:id).each do |account_id|
|
MigrationAccount.pluck(:id).each do |account_id|
|
||||||
author_id = MigrationUser.where(account_id:).minimum(:id)
|
author_id = MigrationUser.where(account_id:).minimum(:id)
|
||||||
|
|
||||||
next if author_id.blank?
|
next if author_id.blank?
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/data
|
- .:/data
|
||||||
environment:
|
environment:
|
||||||
FORCE_SSL: 'true'
|
- FORCE_SSL=${HOST}
|
||||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/docuseal
|
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
|||||||
+4
-5
@@ -47,7 +47,7 @@ module Accounts
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_webhook_configs(account)
|
def load_webhook_configs(account)
|
||||||
account = Account.order(:id).first unless Docuseal.multitenant?
|
EncryptedConfig.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) unless Docuseal.multitenant?
|
||||||
|
|
||||||
account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
end
|
end
|
||||||
@@ -57,8 +57,7 @@ module Accounts
|
|||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS
|
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS
|
||||||
else
|
else
|
||||||
EncryptedConfig.find_by(account: Account.order(:id).first,
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
|
||||||
key: EncryptedConfig::ESIGN_CERTS_KEY).value
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
|
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
|
||||||
@@ -68,11 +67,11 @@ module Accounts
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_send_emails?(account)
|
def can_send_emails?(_account)
|
||||||
return true if Docuseal.multitenant?
|
return true if Docuseal.multitenant?
|
||||||
return true if ENV['SMTP_ADDRESS'].present?
|
return true if ENV['SMTP_ADDRESS'].present?
|
||||||
|
|
||||||
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_timezone(timezone)
|
def normalize_timezone(timezone)
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ module Docuseal
|
|||||||
|
|
||||||
DEFAULT_URL_OPTIONS = {
|
DEFAULT_URL_OPTIONS = {
|
||||||
host: HOST,
|
host: HOST,
|
||||||
protocol: ENV['FORCE_SSL'] == 'true' ? 'https' : 'http'
|
protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ module Submissions
|
|||||||
module GenerateAuditTrail
|
module GenerateAuditTrail
|
||||||
FONT_SIZE = 9
|
FONT_SIZE = 9
|
||||||
TEXT_COLOR = '525252'
|
TEXT_COLOR = '525252'
|
||||||
FONT_PATH = '/LiberationSans-Regular.ttf'
|
FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
|
||||||
FONT_BOLD_PATH = '/LiberationSans-Bold.ttf'
|
FONT_BOLD_PATH = '/fonts/LiberationSans-Bold.ttf'
|
||||||
FONT_NAME = if File.exist?(FONT_PATH)
|
FONT_NAME = if File.exist?(FONT_PATH)
|
||||||
FONT_PATH
|
FONT_PATH
|
||||||
else
|
else
|
||||||
@@ -105,7 +105,8 @@ module Submissions
|
|||||||
document.metadata['sha256'] || document.checksum,
|
document.metadata['sha256'] || document.checksum,
|
||||||
"\n",
|
"\n",
|
||||||
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
I18n.l(document.created_at, format: :long, locale: account.locale)
|
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
||||||
|
"#{timezone_abbr(account.timezone, document.created_at)}"
|
||||||
], line_spacing: 1.8
|
], line_spacing: 1.8
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -166,57 +167,59 @@ module Submissions
|
|||||||
|
|
||||||
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
|
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
|
||||||
|
|
||||||
composer.column(columns: 1, gaps: 0, style: { padding: [0, 200, 0, 0] }) do |column|
|
submission.template_fields.filter_map do |field|
|
||||||
submission.template_fields.filter_map do |field|
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
|
||||||
|
|
||||||
submitter_field_counters[field['type']] += 1
|
submitter_field_counters[field['type']] += 1
|
||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
next if Array.wrap(value).compact_blank.blank?
|
next if Array.wrap(value).compact_blank.blank?
|
||||||
|
|
||||||
[
|
[
|
||||||
column.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: field['name'].to_s.upcase.presence ||
|
text: field['name'].to_s.upcase.presence ||
|
||||||
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase,
|
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase,
|
||||||
font_size: 6
|
font_size: 6
|
||||||
}
|
}
|
||||||
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
|
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials])
|
if field['type'].in?(%w[image signature initials])
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||||
|
|
||||||
scale = [300.0 / image.width, 300.0 / image.height].min
|
scale = [600.0 / image.width, 600.0 / image.height].min
|
||||||
|
|
||||||
io = StringIO.new(image.resize([scale, 1].min).write_to_buffer('.png'))
|
resized_image = image.resize([scale, 1].min)
|
||||||
|
io = StringIO.new(resized_image.write_to_buffer('.png'))
|
||||||
|
|
||||||
column.image(io, padding: [0, field['type'] == 'initials' ? 200 : 100, 10, 0])
|
width = field['type'] == 'initials' ? 100 : 200
|
||||||
column.formatted_text_box([{ text: '' }])
|
height = resized_image.height * (width.to_f / resized_image.width)
|
||||||
elsif field['type'] == 'file'
|
|
||||||
column.formatted_text_box(
|
|
||||||
Array.wrap(value).map do |uuid|
|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
|
||||||
link =
|
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options)
|
|
||||||
|
|
||||||
{ link:, text: "#{attachment.filename}\n", style: :link }
|
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
||||||
end,
|
composer.formatted_text_box([{ text: '' }])
|
||||||
padding: [0, 0, 10, 0]
|
elsif field['type'] == 'file'
|
||||||
)
|
composer.formatted_text_box(
|
||||||
elsif field['type'] == 'checkbox'
|
Array.wrap(value).map do |uuid|
|
||||||
column.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
else
|
link =
|
||||||
value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date'
|
Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options)
|
||||||
value = value.join(', ') if value.is_a?(Array)
|
|
||||||
|
|
||||||
column.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0])
|
{ link:, text: "#{attachment.filename}\n", style: :link }
|
||||||
end
|
end,
|
||||||
]
|
padding: [0, 0, 10, 0]
|
||||||
end
|
)
|
||||||
|
elsif field['type'] == 'checkbox'
|
||||||
|
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
||||||
|
else
|
||||||
|
value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date'
|
||||||
|
value = value.join(', ') if value.is_a?(Array)
|
||||||
|
|
||||||
|
composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0])
|
||||||
|
end
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.draw_box(divider)
|
composer.draw_box(divider)
|
||||||
@@ -227,7 +230,8 @@ module Submissions
|
|||||||
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
|
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
|
||||||
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
|
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
|
||||||
[
|
[
|
||||||
I18n.l(event.event_timestamp, format: :long, locale: account.locale),
|
"#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
||||||
|
"#{timezone_abbr(account.timezone, event.event_timestamp)}",
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[
|
[
|
||||||
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
||||||
@@ -263,6 +267,14 @@ module Submissions
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def timezone_abbr(timezone, time = Time.current)
|
||||||
|
tz_info = TZInfo::Timezone.get(
|
||||||
|
ActiveSupport::TimeZone::MAPPING[timezone] || timezone || 'UTC'
|
||||||
|
)
|
||||||
|
|
||||||
|
tz_info.abbreviation(time)
|
||||||
|
end
|
||||||
|
|
||||||
def add_logo(column)
|
def add_logo(column)
|
||||||
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Submissions
|
module Submissions
|
||||||
module GenerateResultAttachments
|
module GenerateResultAttachments
|
||||||
FONT_SIZE = 11
|
FONT_SIZE = 11
|
||||||
FONT_PATH = '/LiberationSans-Regular.ttf'
|
FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
|
||||||
FONT_NAME = if File.exist?(FONT_PATH)
|
FONT_NAME = if File.exist?(FONT_PATH)
|
||||||
FONT_PATH
|
FONT_PATH
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ module Templates
|
|||||||
PDF_CONTENT_TYPE = 'application/pdf'
|
PDF_CONTENT_TYPE = 'application/pdf'
|
||||||
Q = 35
|
Q = 35
|
||||||
MAX_WIDTH = 1400
|
MAX_WIDTH = 1400
|
||||||
MAX_NUMBER_OF_PAGES_PROCESSED =
|
MAX_NUMBER_OF_PAGES_PROCESSED = 15
|
||||||
if Docuseal.multitenant?
|
|
||||||
70
|
|
||||||
else
|
|
||||||
40
|
|
||||||
end
|
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -74,10 +69,24 @@ module Templates
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
||||||
page = Vips::Image.new_from_file(file_path, dpi: DPI, page: page_number)
|
io = StringIO.new
|
||||||
page = page.resize(MAX_WIDTH / page.width.to_f)
|
|
||||||
|
|
||||||
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
command = [
|
||||||
|
'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{Q},optimize=y",
|
||||||
|
'-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
|
||||||
|
'-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
|
||||||
|
'-singlefile', Shellwords.escape(file_path), '-'
|
||||||
|
].join(' ')
|
||||||
|
|
||||||
|
Open3.popen3(command) do |_, stdout, _, _|
|
||||||
|
io.write(stdout.read)
|
||||||
|
|
||||||
|
io.rewind
|
||||||
|
end
|
||||||
|
|
||||||
|
page = Vips::Image.new_from_buffer(io.read, '')
|
||||||
|
|
||||||
|
io.rewind
|
||||||
|
|
||||||
ApplicationRecord.no_touching do
|
ApplicationRecord.no_touching do
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'App Setup', js: true do
|
RSpec.describe 'App Setup' do
|
||||||
let(:form_data) do
|
let(:form_data) do
|
||||||
{
|
{
|
||||||
first_name: 'John',
|
first_name: 'John',
|
||||||
|
|||||||
Reference in new issue
Block a user