diff --git a/Gemfile.lock b/Gemfile.lock index a1472da2d..e62707472 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -524,11 +524,11 @@ GEM simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - sqlite3 (2.9.5-aarch64-linux-gnu) - sqlite3 (2.9.5-aarch64-linux-musl) - sqlite3 (2.9.5-arm64-darwin) - sqlite3 (2.9.5-x86_64-linux-gnu) - sqlite3 (2.9.5-x86_64-linux-musl) + sqlite3 (2.9.6-aarch64-linux-gnu) + sqlite3 (2.9.6-aarch64-linux-musl) + sqlite3 (2.9.6-arm64-darwin) + sqlite3 (2.9.6-x86_64-linux-gnu) + sqlite3 (2.9.6-x86_64-linux-musl) strip_attributes (2.0.1) activemodel (>= 3.0, < 9.0) strscan (3.1.8) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c4955b3bb..33257be89 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base end def sign_in_for_demo - sign_in(User.active.order('random()').take) unless signed_in? + return if signed_in? + + account = Account.create!(name: "Demo #{SecureRandom.hex(3)}") + + sign_in(account.users.create!(first_name: 'Demo', last_name: 'User', + email: "#{SecureRandom.hex}@docuseal.com", password: SecureRandom.hex)) end def current_account diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 8b766b25f..a3b98cebc 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -66,6 +66,8 @@ class SubmissionsController < ApplicationController create_submissions(@template, submissions_params, params) end + raise Submissions::CreateFromSubmitters::BaseError, 'Invalid email address' if submissions.blank? + WebhookUrls.enqueue_events(submissions, 'submission.created') Submissions.send_signature_requests(submissions) diff --git a/app/controllers/submitters_controller.rb b/app/controllers/submitters_controller.rb index 48610854b..ed5a63c29 100644 --- a/app/controllers/submitters_controller.rb +++ b/app/controllers/submitters_controller.rb @@ -81,9 +81,26 @@ class SubmittersController < ApplicationController submitter.name = attrs[:name] if attrs.key?(:name) + if submitter_changed?(submitter) + submitter.submitter_versions.new(slug: submitter.slug, email: submitter.email_was, + name: submitter.name_was, phone: submitter.phone_was) + + submitter.slug = SecureRandom.base58(14) + submitter.values = Submitters.fetch_values_for_delegate(submitter) + end + submitter end + def submitter_changed?(submitter) + if submitter.email_was.blank? && submitter.phone_was.blank? + submitter.name_was.present? && submitter.name_changed? + else + (submitter.email_was.present? && submitter.email_changed?) || + (submitter.phone_was.present? && submitter.phone_changed?) + end + end + def submitter_params params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip) end diff --git a/app/javascript/application.js b/app/javascript/application.js index 7dee5edf9..a08c858a4 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -35,6 +35,9 @@ import PromptPassword from './elements/prompt_password' import EmailsTextarea from './elements/emails_textarea' import ToggleSubmit from './elements/toggle_submit' import ToggleOnSubmit from './elements/toggle_on_submit' +import ToggleOnChecked from './elements/toggle_on_checked' +import ToggleSendTitle from './elements/toggle_send_title' +import AppendTemplate from './elements/append_template' import CheckOnClick from './elements/check_on_click' import PasswordInput from './elements/password_input' import SearchInput from './elements/search_input' @@ -159,6 +162,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea) safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-submit', ToggleSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit) +safeRegisterElement('toggle-on-checked', ToggleOnChecked) +safeRegisterElement('toggle-send-title', ToggleSendTitle) +safeRegisterElement('append-template', AppendTemplate) safeRegisterElement('password-input', PasswordInput) safeRegisterElement('search-input', SearchInput) safeRegisterElement('toggle-attribute', ToggleAttribute) diff --git a/app/javascript/elements/append_template.js b/app/javascript/elements/append_template.js new file mode 100644 index 000000000..18012bcf4 --- /dev/null +++ b/app/javascript/elements/append_template.js @@ -0,0 +1,29 @@ +export default class extends HTMLElement { + connectedCallback () { + this.addEventListener('click', this.onClick) + } + + disconnectedCallback () { + this.removeEventListener('click', this.onClick) + } + + onClick = (e) => { + e.preventDefault() + + const target = document.getElementById(this.dataset.targetId) + const item = this.querySelector('template').content.firstElementChild.cloneNode(true) + + item.querySelector('a').addEventListener('click', (e) => { + e.preventDefault() + + item.remove() + + target.classList.toggle('hidden', !target.childElementCount) + }) + + target.append(item) + target.classList.remove('hidden') + + item.querySelector('input').focus() + } +} diff --git a/app/javascript/elements/dynamic_list.js b/app/javascript/elements/dynamic_list.js index 57c9ccf18..a0ad26c0b 100644 --- a/app/javascript/elements/dynamic_list.js +++ b/app/javascript/elements/dynamic_list.js @@ -2,7 +2,7 @@ import { actionable } from '@github/catalyst/lib/actionable' import { targets, targetable } from '@github/catalyst/lib/targetable' export default actionable(targetable(class extends HTMLElement { - static [targets.static] = ['items'] + static [targets.static] = ['items', 'groups'] addItem (e) { e.preventDefault() @@ -24,11 +24,28 @@ export default actionable(targetable(class extends HTMLElement { duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden')) originalItem.parentNode.append(duplicateItem) + + this.toggleMultipleClasses() + + duplicateItem.querySelector("input:not([type='hidden'])")?.focus() } removeItem (e) { e.preventDefault() this.items.find((item) => item.contains(e.target))?.remove() + + this.toggleMultipleClasses() + } + + toggleMultipleClasses () { + const isMultiple = this.items.length > 1 + const elements = [...this.items, ...this.groups] + + elements.forEach((element) => { + const classes = element.dataset.multipleClasses || this.dataset.multipleClasses + + classes?.split(' ').forEach((cls) => element.classList.toggle(cls, isMultiple)) + }) } })) diff --git a/app/javascript/elements/toggle_on_checked.js b/app/javascript/elements/toggle_on_checked.js new file mode 100644 index 000000000..85ba9b0df --- /dev/null +++ b/app/javascript/elements/toggle_on_checked.js @@ -0,0 +1,23 @@ +export default class extends HTMLElement { + connectedCallback () { + this.form.addEventListener('change', this.toggle) + + this.toggle() + } + + disconnectedCallback () { + this.form.removeEventListener('change', this.toggle) + } + + toggle = () => { + const selector = this.dataset.names.split(' ').map((name) => `[name="${name}"]:checked:enabled`).join(', ') + const isChecked = !!this.form.querySelector(selector) + + this.firstElementChild.classList.toggle('hidden', !isChecked) + this.lastElementChild.classList.toggle('hidden', isChecked) + } + + get form () { + return this.closest('form') + } +} diff --git a/app/javascript/elements/toggle_send_title.js b/app/javascript/elements/toggle_send_title.js new file mode 100644 index 000000000..5d73e7706 --- /dev/null +++ b/app/javascript/elements/toggle_send_title.js @@ -0,0 +1,43 @@ +export default class extends HTMLElement { + connectedCallback () { + this.events.forEach((event) => this.form.addEventListener(event, this.toggle)) + + this.toggle() + } + + disconnectedCallback () { + this.events.forEach((event) => this.form.removeEventListener(event, this.toggle)) + } + + toggle = (event) => { + if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return + + const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) + + const isSend = (this.isChecked('send_email') && !isNameOnly('email')) || + (this.isChecked('send_sms') && !isNameOnly('phone')) + + this.firstElementChild.classList.toggle('hidden', !isSend) + this.lastElementChild.classList.toggle('hidden', isSend) + } + + isChecked (name) { + return !!this.form.querySelector(`[name="${name}"]:checked:enabled`) + } + + isFilled (field) { + return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim()) + } + + fieldSelector (field) { + return `[name$="[][${field}]"]` + } + + get events () { + return ['change', 'input', 'linked-input.update'] + } + + get form () { + return this.closest('form') + } +} diff --git a/app/javascript/native.js b/app/javascript/native.js index 7de1f76a1..5ddd44580 100644 --- a/app/javascript/native.js +++ b/app/javascript/native.js @@ -24,6 +24,9 @@ const ICONS = { user: 'person', users: 'person.2', users_plus: 'person.badge.plus', + user_plus: 'person.badge.plus', + eye_plus: 'eye', + mail_plus: 'envelope', photo: 'photo', settings: 'gearshape', logout: 'rectangle.portrait.and.arrow.right', diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index 88253fa24..e15bf2f8e 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -319,7 +319,7 @@ export default { default: false } }, - emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title', 'multi-select'], + emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'], data () { return { isContenteditable: false, @@ -798,8 +798,6 @@ export default { this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1) } - this.$emit('multi-select', e) - return } diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 26a62bcd5..f8c2580bc 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -3,8 +3,10 @@ ref="dragContainer" style="max-width: 1600px" :class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'" + @dragstart="isInternalDrag = true" + @dragend="isInternalDrag = false" @dragover="onDragover" - @drop="isDragFile = false" + @drop="[isDragFile = false, isInternalDrag = false]" > item.type.startsWith('image/'))) { + if (!e.defaultPrevented) { + e.preventDefault() + + e.dataTransfer.dropEffect = 'none' + } + } else { + this.isDragFile = true + } } }, onWindowDragLeave (event) { diff --git a/app/javascript/template_builder/dynamic_section.vue b/app/javascript/template_builder/dynamic_section.vue index 260a1b768..5842e2091 100644 --- a/app/javascript/template_builder/dynamic_section.vue +++ b/app/javascript/template_builder/dynamic_section.vue @@ -639,6 +639,20 @@ export default { if (!file) return + const image = await this.uploadImage(file) + + if (image) { + this.editor.chain().focus().insertContent(image).run() + } + }, + async insertImageFiles (files, pos) { + const images = (await Promise.all(files.map(this.uploadImage))).filter(Boolean) + + if (images.length) { + this.editor.chain().focus().insertContentAt(pos, images).run() + } + }, + async uploadImage (file) { const formData = new FormData() formData.append('file', file) @@ -656,9 +670,7 @@ export default { await this.$nextTick() - this.editor.chain().focus() - .insertContent({ type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } }) - .run() + return { type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } } }, setDynamicMenuCoords (editor) { const { from, to } = editor.state.selection @@ -975,7 +987,7 @@ export default { onSelectedAreaChange () { this.save() }, - onFieldDrop (view, event, _slice, moved) { + onFieldDrop (view, event, slice, moved) { this.isAreaDrag = false if (moved) { @@ -983,8 +995,9 @@ export default { } const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField + const imageFiles = slice.size ? [] : Array.from(event.dataTransfer.files).filter((file) => file.type.startsWith('image/')) - if (!draggedField) return false + if (!draggedField && !imageFiles.length) return false event.preventDefault() @@ -992,14 +1005,18 @@ export default { if (!pos) return false - this.insertFieldAtRange({ - sourceField: draggedField, - existingField: this.fieldsDragFieldRef?.value, - from: pos.pos - }) + if (draggedField) { + this.insertFieldAtRange({ + sourceField: draggedField, + existingField: this.fieldsDragFieldRef?.value, + from: pos.pos + }) - this.fieldsDragFieldRef.value = null - this.customDragFieldRef.value = null + this.fieldsDragFieldRef.value = null + this.customDragFieldRef.value = null + } else { + this.insertImageFiles(imageFiles, pos.pos) + } return true }, diff --git a/app/javascript/template_builder/page.vue b/app/javascript/template_builder/page.vue index 5c16362c5..84accb314 100644 --- a/app/javascript/template_builder/page.vue +++ b/app/javascript/template_builder/page.vue @@ -57,7 +57,6 @@ @scroll-to="$emit('scroll-to', $event)" @add-custom-field="$emit('add-custom-field', $event)" @contextmenu="openAreaContextMenu($event, item.area, item.field)" - @multi-select="openMultiSelectContextMenu" @click-title="closeContextMenu" /> = 2) { - this.openSelectionContextMenu(event) - } else { - this.closeContextMenu() - } - }, handleSelectionCopy () { this.$emit('copy-selected-areas') diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 96653e662..cff43da56 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -196,7 +196,7 @@ <% end %> <% end %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %> - <% if can?(:manage, account_config) %> + <% if !Docuseal.demo? && can?(:manage, account_config) %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= f.hidden_field :key %>
diff --git a/app/views/icons/_eye_plus.html.erb b/app/views/icons/_eye_plus.html.erb new file mode 100644 index 000000000..ad4a2aef7 --- /dev/null +++ b/app/views/icons/_eye_plus.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/icons/_mail_plus.html.erb b/app/views/icons/_mail_plus.html.erb new file mode 100644 index 000000000..5b4ecab38 --- /dev/null +++ b/app/views/icons/_mail_plus.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/icons/_user_plus.html.erb b/app/views/icons/_user_plus.html.erb index 935d26d05..0a83ce86e 100644 --- a/app/views/icons/_user_plus.html.erb +++ b/app/views/icons/_user_plus.html.erb @@ -1,8 +1,7 @@ - - - + + diff --git a/app/views/shared/_demo_alert.html.erb b/app/views/shared/_demo_alert.html.erb index 45e36accd..7c2600847 100644 --- a/app/views/shared/_demo_alert.html.erb +++ b/app/views/shared/_demo_alert.html.erb @@ -3,7 +3,7 @@
<%= t('demo_environment') %>
- <%= t('create_a_new_template_document_form_or_submit_the_existing_one_html', new_template_link: new_template_path, start_form_link: start_form_url(slug: ::Template.first&.slug)) %> 😊 + <%= t('create_a_new_template_document_form_html', new_template_link: new_template_path) %>
diff --git a/app/views/start_form/completed.html.erb b/app/views/start_form/completed.html.erb index 8711797e7..bb576a831 100644 --- a/app/views/start_form/completed.html.erb +++ b/app/views/start_form/completed.html.erb @@ -12,7 +12,7 @@

<%= @submitter.submission.template&.name %>

- <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %> + <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>

diff --git a/app/views/submissions/_add_new.html.erb b/app/views/submissions/_add_new.html.erb new file mode 100644 index 000000000..3a283f7fb --- /dev/null +++ b/app/views/submissions/_add_new.html.erb @@ -0,0 +1,70 @@ +<% fields_id = "add_new_fields_#{SecureRandom.hex(3)}" %> + + + + + diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index c948f568a..a1b3e76aa 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -1,12 +1,12 @@ <% has_phone_field = false %> -<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> +<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> - +
-
+
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %> - - <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> - <%= t('add_new') %> - + <%= render 'add_new', template: %> <% end %> <%= local_assigns[:variables_form] %> -
+
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <% if has_phone_field %> @@ -101,7 +98,12 @@ <% end %> <%= render 'extra_fields', f: %>
-
- <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> +
+ <%= f.button class: 'base-button' do %> + + <%= button_title(title: t('send'), disabled_with: t('sending')) %> + + + <% end %>
<% end %> diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index 718cb905e..d11464583 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -1,4 +1,4 @@ -<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> +<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% if submitters.size == 1 && local_assigns[:variables_form] %> <% item = submitters.first %> @@ -26,18 +26,18 @@ - <%= f.text_area :emails, required: true, class: 'base-textarea w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %> + <%= f.text_area :emails, required: true, class: 'base-textarea block w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %> <% else %> - +
-
+
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> - - <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> - <%= t('add_new') %> - + <%= render 'add_new', template: %> <% end %> <% end %> <%= local_assigns[:variables_form] %> -
+
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'extra_fields', f: %>
-
+
<%= content_for(:submit_button) || capture do %> - <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> + <%= f.button class: 'base-button' do %> + + <%= button_title(title: t('send'), disabled_with: t('sending')) %> + + + <% end %> <% end %>
<% end %> diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb index a3d9b92d0..cf0f1748c 100644 --- a/app/views/submissions/_phone_form.html.erb +++ b/app/views/submissions/_phone_form.html.erb @@ -1,11 +1,11 @@ -<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> +<%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> - +
-
+
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> - - <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> - <%= t('add_new') %> - + <%= render 'add_new', template: %> <% end %> <%= local_assigns[:variables_form] %> -
+
<%= render 'submitters_order', f:, template: %> <%= render 'send_sms', f:, checked: true %> <%= render 'extra_phone_fields', f: %>
-
- <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> +
+ <%= f.button class: 'base-button' do %> + + <%= button_title(title: t('send'), disabled_with: t('sending')) %> + + + <% end %>
<% end %> diff --git a/app/views/submissions/_send_email_base.html.erb b/app/views/submissions/_send_email_base.html.erb index 24d25e737..2341d70c3 100644 --- a/app/views/submissions/_send_email_base.html.erb +++ b/app/views/submissions/_send_email_base.html.erb @@ -14,7 +14,7 @@ <% end %> diff --git a/app/views/submissions/_send_sms.html.erb b/app/views/submissions/_send_sms.html.erb index 92a8de542..598b3a887 100644 --- a/app/views/submissions/_send_sms.html.erb +++ b/app/views/submissions/_send_sms.html.erb @@ -1,5 +1,7 @@ <% unless turbo_native_ios? %> -
- <%= render 'sms_settings/placeholder' %> -
+ +
+ <%= render 'sms_settings/placeholder' %> +
+
<% end %> diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb index 19fa41aa6..7821f1d2e 100644 --- a/app/views/submissions/new.html.erb +++ b/app/views/submissions/new.html.erb @@ -26,7 +26,7 @@
<% end %> -
+
<% unless only_detailed %>
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index dba9ecfaa..f9728ce06 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -54,7 +54,8 @@ <% if @submission.submitters.to_a.any?(&:completed_at?) %> <% show_combined_download = @submission.completed_at? && !is_combined_enabled %> <% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %> - <% show_download_dropdown = show_combined_download || show_unarchive %> + <% show_event_log_download = signed_in? && can?(:manage, :event_log_download) && can?(:read, @submission) %> + <% show_download_dropdown = show_combined_download || show_event_log_download || show_unarchive %> <% if @submission.completed_at? || !is_combined_enabled %>
@@ -95,6 +96,9 @@ <% end %> + <% if show_event_log_download %> + <%= render 'submissions/download_event_log' %> + <% end %> <% if show_unarchive %>
  • @@ -163,6 +167,7 @@ <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value ||= field['default_value'] if field['type'] == 'heading' %> <% submitter = submitters_index[field['submitter_uuid']] %> + <% value = nil if field['type'] == 'signature' && submitter && !submitter.completed_at? %> <% if value.present? %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> @@ -320,6 +325,7 @@ <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% next if value.blank? %> <% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %> + <% next if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
    <%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %> diff --git a/app/views/submissions_preview/completed.html.erb b/app/views/submissions_preview/completed.html.erb index f844b6188..a09e2c8bd 100644 --- a/app/views/submissions_preview/completed.html.erb +++ b/app/views/submissions_preview/completed.html.erb @@ -15,7 +15,7 @@ <% last_submitter = @submission.submitters.completed.order(:completed_at).last %> <% if last_submitter %>

    - <%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %> + <%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.in_time_zone(@submission.account.timezone).to_date, format: :long)) %>

    <% end %>
    diff --git a/app/views/submit_form/completed.html.erb b/app/views/submit_form/completed.html.erb index 217c97c8a..7c7693814 100644 --- a/app/views/submit_form/completed.html.erb +++ b/app/views/submit_form/completed.html.erb @@ -13,7 +13,7 @@

    <%= @submitter.submission.name || @submitter.submission.template&.name %>

    - <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(completed_at.to_date, format: :long)) %> + <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>

    diff --git a/app/views/submit_form/declined.html.erb b/app/views/submit_form/declined.html.erb index 1c896dc0b..f938ede23 100644 --- a/app/views/submit_form/declined.html.erb +++ b/app/views/submit_form/declined.html.erb @@ -11,7 +11,7 @@
  • <%= @submitter.submission.name || @submitter.submission.template&.name %>

    -

    <%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %>

    +

    <%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>

    diff --git a/app/views/submit_form/delegated.html.erb b/app/views/submit_form/delegated.html.erb index 18a225615..ccf6e3ba6 100644 --- a/app/views/submit_form/delegated.html.erb +++ b/app/views/submit_form/delegated.html.erb @@ -11,7 +11,7 @@

    <%= @submitter.submission.name || @submitter.submission.template&.name %>

    -

    <%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp, format: :long)) %>

    +

    <%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>

    diff --git a/app/views/submit_form/email_2fa.html.erb b/app/views/submit_form/email_2fa.html.erb index b79479d2e..6bb4a584b 100644 --- a/app/views/submit_form/email_2fa.html.erb +++ b/app/views/submit_form/email_2fa.html.erb @@ -19,7 +19,7 @@ <% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %> <% if last_submitter %>

    - <%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %> + <%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>

    <% end %>
    diff --git a/app/views/submit_form/expired.html.erb b/app/views/submit_form/expired.html.erb index 1631f7aee..671583377 100644 --- a/app/views/submit_form/expired.html.erb +++ b/app/views/submit_form/expired.html.erb @@ -11,7 +11,7 @@

    <%= @submitter.submission.name || @submitter.submission.template&.name %>

    -

    <%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %>

    +

    <%= t('form_expired_at_html', time: l(@submitter.submission.expire_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>

    diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index bb9f5eb65..55ae36830 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -704,6 +704,7 @@ en: &en developer_sandbox: Developer Sandbox. upgrade_to_start_using_in_production_html: 'Upgrade to start using in Production.' download_combined_pdf: Download combined PDF + download_event_log: Download Event Log audit_log: Audit Log signers: Signers not_invited_yet: Not invited yet @@ -715,7 +716,7 @@ en: &en declined_on_time: 'Declined on %{time}' expire_on_time: 'Expire on %{time}' sign_in_person: Sign In-person - create_a_new_template_document_form_or_submit_the_existing_one_html: Create a new template document form or submit the existing one + create_a_new_template_document_form_html: Create a new template document form send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address. re_send_email: Re-send Email re_send_emails: Re-send Emails @@ -1027,6 +1028,8 @@ en: &en failed_to_scan_the_document: Failed to scan the document document_scanning_is_not_available_on_this_device: Document scanning is not available on this device click_to_upload: Click to upload + cc_view_only: CC View-only + bcc_completed: BCC Completed devise: confirmations: confirmed: Your email address has been successfully confirmed. @@ -1862,6 +1865,7 @@ es: &es developer_sandbox: Sandbox para desarrolladores. upgrade_to_start_using_in_production_html: 'Actualiza para empezar a usar en Producción.' download_combined_pdf: Descargar PDF combinado + download_event_log: Descargar registro de eventos audit_log: Registro de Auditoría signers: Firmantes not_invited_yet: Aún no invitado @@ -1873,7 +1877,7 @@ es: &es declined_on_time: 'Rechazado el %{time}' expire_on_time: 'Expira el %{time}' sign_in_person: Firma en persona - create_a_new_template_document_form_or_submit_the_existing_one_html: Crear una nueva plantilla de documento o enviar el existente + create_a_new_template_document_form_html: Crear una nueva plantilla de documento send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada. re_send_email: Reenviar email re_send_emails: Reenviar emails @@ -2174,6 +2178,8 @@ es: &es failed_to_save_the_scan: No se pudo guardar el escaneo failed_to_scan_the_document: No se pudo escanear el documento document_scanning_is_not_available_on_this_device: El escaneo de documentos no está disponible en este dispositivo + cc_view_only: CC Solo lectura + bcc_completed: CCO Completado devise: confirmations: confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente. @@ -3023,6 +3029,7 @@ it: &it developer_sandbox: Sandbox per sviluppatori upgrade_to_start_using_in_production_html: 'Aggiorna per iniziare a usare in produzione.' download_combined_pdf: Scarica PDF combinato + download_event_log: Scarica registro degli eventi audit_log: Registro di audit signers: Firmatari not_invited_yet: Non ancora invitato @@ -3034,7 +3041,7 @@ it: &it declined_on_time: 'Rifiutato il %{time}' expire_on_time: 'Scade il %{time}' sign_in_person: Firma di persona - create_a_new_template_document_form_or_submit_the_existing_one_html: Crea un nuovo modello di documento o invia quello esistente + create_a_new_template_document_form_html: Crea un nuovo modello di documento send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato. re_send_email: Reinvia email re_send_emails: Reinvia le email @@ -3335,6 +3342,8 @@ it: &it failed_to_save_the_scan: Impossibile salvare la scansione failed_to_scan_the_document: Impossibile scansionare il documento document_scanning_is_not_available_on_this_device: La scansione dei documenti non è disponibile su questo dispositivo + cc_view_only: CC Sola lettura + bcc_completed: BCC Completato devise: confirmations: confirmed: Il tuo indirizzo email è stato confermato con successo. @@ -4170,6 +4179,7 @@ fr: &fr developer_sandbox: Bac à sable développeur. upgrade_to_start_using_in_production_html: Mettez à niveau pour commencer à utiliser en production. download_combined_pdf: Télécharger le PDF combiné + download_event_log: Télécharger le journal des événements audit_log: Journal d’audit signers: Signataires not_invited_yet: Pas encore invité @@ -4181,7 +4191,7 @@ fr: &fr declined_on_time: Refusé le %{time} expire_on_time: Expire le %{time} sign_in_person: Signer en personne - create_a_new_template_document_form_or_submit_the_existing_one_html: Créez un nouveau modèle de document ou soumettez l’existant + create_a_new_template_document_form_html: Créez un nouveau modèle de document send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée. re_send_email: Renvoyer l’e‑mail re_send_emails: Renvoyer e‑mails @@ -4479,6 +4489,8 @@ fr: &fr failed_to_save_the_scan: Impossible d’enregistrer la numérisation failed_to_scan_the_document: Impossible de numériser le document document_scanning_is_not_available_on_this_device: La numérisation de documents n’est pas disponible sur cet appareil + cc_view_only: Cc Lecture seule + bcc_completed: Cci Terminé devise: confirmations: confirmed: Votre adresse e-mail a été confirmée avec succès. @@ -5327,6 +5339,7 @@ pt: &pt developer_sandbox: Sandbox para desenvolvedores upgrade_to_start_using_in_production_html: 'Atualizar para começar a usar em produção.' download_combined_pdf: Baixar PDF combinado + download_event_log: Baixar log de eventos audit_log: Log de auditoria signers: Signatários not_invited_yet: Ainda não convidado @@ -5338,7 +5351,7 @@ pt: &pt declined_on_time: 'Recusado em %{time}' expire_on_time: 'Expira em %{time}' sign_in_person: Assinar pessoalmente - create_a_new_template_document_form_or_submit_the_existing_one_html: 'Criar um novo modelo de documento ou submeter o existente' + create_a_new_template_document_form_html: 'Criar um novo modelo de documento' send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado. re_send_email: Reenviar e-mail re_send_emails: Reenviar e-mails @@ -5639,6 +5652,8 @@ pt: &pt failed_to_save_the_scan: Não foi possível salvar a digitalização failed_to_scan_the_document: Não foi possível digitalizar o documento document_scanning_is_not_available_on_this_device: A digitalização de documentos não está disponível neste dispositivo + cc_view_only: CC Somente leitura + bcc_completed: BCC Concluído devise: confirmations: confirmed: Seu endereço de e-mail foi confirmado com sucesso. @@ -6487,6 +6502,7 @@ de: &de developer_sandbox: Entwickler-Sandbox. upgrade_to_start_using_in_production_html: 'Upgrade durchführen, um die Produktionsnutzung zu starten.' download_combined_pdf: Kombiniertes PDF herunterladen + download_event_log: Ereignisprotokoll herunterladen audit_log: Prüfprotokoll signers: Unterzeichner not_invited_yet: Noch nicht eingeladen @@ -6498,7 +6514,7 @@ de: &de declined_on_time: 'Abgelehnt am %{time}' expire_on_time: 'Läuft ab am %{time}' sign_in_person: Vor Ort unterschreiben - create_a_new_template_document_form_or_submit_the_existing_one_html: Neue Vorlage erstellen oder bestehende einreichen + create_a_new_template_document_form_html: Neue Vorlage erstellen send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse. re_send_email: E-Mail erneut senden re_send_emails: E-Mails senden @@ -6799,6 +6815,8 @@ de: &de failed_to_save_the_scan: Scan konnte nicht gespeichert werden failed_to_scan_the_document: Dokument konnte nicht gescannt werden document_scanning_is_not_available_on_this_device: Das Scannen von Dokumenten ist auf diesem Gerät nicht verfügbar + cc_view_only: CC Nur Ansicht + bcc_completed: BCC Abgeschlossen devise: confirmations: confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt. @@ -6981,14 +6999,14 @@ pl: decline_reason: Powód odrzucenia provide_a_reason: Podaj powód notify_the_sender_with_the_reason_you_declined: Powiadom nadawcę o powodzie, dla którego odrzuciłeś - form_has_been_declined_on_html: 'Formularz został odrzucony o %{time}' - document_has_been_delegated_on_html: 'Dokument został oddelegowany o %{time}' + form_has_been_declined_on_html: 'Formularz został odrzucony %{time}' + document_has_been_delegated_on_html: 'Dokument został oddelegowany %{time}' name_declined_by_submitter: '"%{name}" odrzucono przez %{submitter}' name_declined_by_submitter_with_the_following_reason: '"%{name}" został odrzucony przez %{submitter} z następującym powodem:' email: E-mail verification_code_code: 'Kod weryfikacyjny: %{code}' digitally_signed_by: Podpis cyfrowy przez - form_expired_at_html: 'Formularz wygasł o %{time}' + form_expired_at_html: 'Formularz wygasł %{time}' role: Rola provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć provide_your_email: Podaj swój e-mail @@ -7123,8 +7141,8 @@ uk: decline_reason: Причина відхилення provide_a_reason: Надайте причину notify_the_sender_with_the_reason_you_declined: Повідомте відправника про причину свого відхилення - form_has_been_declined_on_html: 'Форму було відхилено о %{time}' - document_has_been_delegated_on_html: 'Документ було делеговано о %{time}' + form_has_been_declined_on_html: 'Форму було відхилено %{time}' + document_has_been_delegated_on_html: 'Документ було делеговано %{time}' name_declined_by_submitter: '"%{name}" відхилено %{submitter}' name_declined_by_submitter_with_the_following_reason: '"%{name}" було відхилено %{submitter} з такою причиною:' email: E-mail @@ -8175,6 +8193,7 @@ nl: &nl developer_sandbox: Ontwikkelaars-sandbox. upgrade_to_start_using_in_production_html: Upgrade om in productie te gaan gebruiken. download_combined_pdf: Gecombineerde PDF downloaden + download_event_log: Gebeurtenislogboek downloaden audit_log: Auditlogboek signers: Ondertekenaars not_invited_yet: Nog niet uitgenodigd @@ -8186,7 +8205,7 @@ nl: &nl declined_on_time: Geweigerd op %{time} expire_on_time: Verloopt op %{time} sign_in_person: In persoon ondertekenen - create_a_new_template_document_form_or_submit_the_existing_one_html: Maak een nieuw sjabloon voor een documentformulier of dien het bestaande in + create_a_new_template_document_form_html: Maak een nieuw sjabloon voor een documentformulier send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres. re_send_email: E-mail opnieuw verzenden re_send_emails: E-mails verzenden @@ -8483,6 +8502,8 @@ nl: &nl failed_to_save_the_scan: Scan kon niet worden opgeslagen failed_to_scan_the_document: Document kon niet worden gescand document_scanning_is_not_available_on_this_device: Documenten scannen is niet beschikbaar op dit apparaat + cc_view_only: CC Alleen bekijken + bcc_completed: BCC Voltooid devise: confirmations: confirmed: Uw e-mailadres is succesvol bevestigd. diff --git a/lib/accounts.rb b/lib/accounts.rb index 123747ebd..5ac13f0c0 100644 --- a/lib/accounts.rb +++ b/lib/accounts.rb @@ -5,34 +5,6 @@ module Accounts module_function - def create_duplicate(account) - new_account = account.dup - - new_user = account.users.first.dup - - new_user.uuid = SecureRandom.uuid - new_user.account = new_account - new_user.encrypted_password = SecureRandom.hex - new_user.email = "#{SecureRandom.hex}@docuseal.com" - - account.templates.each do |template| - new_template = template.dup - - new_template.account = new_account - new_template.slug = SecureRandom.base58(14) - - new_template.archived_at = nil - new_template.save! - - Templates::CloneAttachments.call(template: new_template, original_template: template) - end - - new_user.save!(validate: false) - new_account.templates.update_all(folder_id: new_account.default_template_folder.id) - - new_account - end - def users_count(account) rel = User.where(account_id: account.id).or( User.where(account_id: account.account_linked_accounts diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 10117e33d..dd2780537 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -90,6 +90,10 @@ module Submissions maybe_add_invite_submitters(submission, template, attrs[:submitters]) + maybe_add_cc_submitters(submission, params[:cc_recipients], + submitters_order:, user:, params:, + preferences: preferences.merge(submission_preferences)) + assign_submitters_is_viewer(submission) submission.template = nil unless with_template @@ -211,6 +215,25 @@ module Submissions end end + def maybe_add_cc_submitters(submission, recipients, submitters_order:, user:, params:, preferences:) + order = submission.template_submitters.filter_map { |s| s['order'] }.max + + Array.wrap(recipients).each do |recipient| + attrs = recipient.is_a?(String) ? { email: recipient } : recipient.slice(:name, :email, :phone) + + next if attrs.compact_blank.blank? + + uuid = SecureRandom.uuid + + submission.template_submitters << + { 'name' => 'CC', 'uuid' => uuid, 'is_viewer' => true, 'order' => order }.compact + + build_submitter(submission:, attrs:, uuid:, user:, params:, + is_order_sent: submitters_order == 'random', + preferences: preferences.except('email_message_uuid')) + end + end + def submitter_message_preferences(uuid, params) return {} if params[:request_email_per_submitter] != '1' return {} if params[:is_custom_message] != '1' diff --git a/lib/submitters.rb b/lib/submitters.rb index 4dcd1ebd0..d191421d2 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -152,7 +152,7 @@ module Submitters preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa') preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa') - preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed') + preferences['bcc_completed'] = normalize_bcc_completed(params['bcc_completed']) if params.key?('bcc_completed') preferences['reply_to'] = params['reply_to'] if params.key?('reply_to') preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last') preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url') @@ -160,6 +160,14 @@ module Submitters preferences end + def normalize_bcc_completed(value) + if value.is_a?(Array) + value.compact_blank.uniq.join(', ') + else + value + end + end + def fetch_values_for_delegate(submitter) fields = submitter.submission.template_fields || submitter.template.fields default_values = submitter.preferences['default_values'] || {} diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb index e8f138f36..26d4d4530 100644 --- a/lib/templates/process_document.rb +++ b/lib/templates/process_document.rb @@ -90,23 +90,28 @@ module Templates pool = Concurrent::FixedThreadPool.new(concurrency) - promises = - range.map do |page_number| - doc_page = doc.get_page(page_number) + blobs = + range.each_slice(concurrency).flat_map do |page_numbers| + promises = + page_numbers.map do |page_number| + doc_page = doc.get_page(page_number) - hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages) - doc_page.flatten if flatten_pages + hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages) + doc_page.flatten if flatten_pages - bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH) + bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH) - image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar) + image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar) - Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(image, page_number) } - ensure - doc_page&.close + Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(image, page_number) } + ensure + doc_page&.close + end + + Concurrent::Promise.zip(*promises).value! end - Concurrent::Promise.zip(*promises).value!.each do |blob| + blobs.each do |blob| next unless blob ApplicationRecord.no_touching do