diff --git a/app/controllers/api/form_events_controller.rb b/app/controllers/api/form_events_controller.rb index 3e8e2a38..759cd230 100644 --- a/app/controllers/api/form_events_controller.rb +++ b/app/controllers/api/form_events_controller.rb @@ -11,8 +11,9 @@ module Api params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? submitters = paginate( - submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob, - combined_document_attachment: :blob }], + submitters.preload(template: { folder: :parent_folder }, + submission: [:submitters, { audit_trail_attachment: :blob, + combined_document_attachment: :blob }], documents_attachments: :blob, attachments_attachments: :blob), field: :completed_at ) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 0cf7d2ba..77751066 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -14,7 +14,7 @@ module Api submissions = filter_submissions(submissions, params) submissions = paginate(submissions.preload(:created_by_user, :submitters, - template: :folder, + template: { folder: :parent_folder }, combined_document_attachment: :blob, audit_trail_attachment: :blob)) @@ -104,9 +104,10 @@ module Api submissions = submissions.where(slug: params[:slug]) if params[:slug].present? if params[:template_folder].present? - folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id) + folders = + TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder]) - submissions = submissions.joins(:template).where(template: { folder_id: folder_ids }) + submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) }) end if params.key?(:archived) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index a45f5c6c..0ccc5b39 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -7,7 +7,7 @@ module Api def index templates = filter_templates(@templates, params) - templates = paginate(templates.preload(:author, :folder)) + templates = paginate(templates.preload(:author, folder: :parent_folder)) schema_documents = ActiveStorage::Attachment.where(record_id: templates.map(&:id), @@ -92,9 +92,9 @@ module Api templates = templates.where(slug: params[:slug]) if params[:slug].present? if params[:folder].present? - folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id) + folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder]) - templates = templates.where(folder_id: folder_ids) + templates = templates.where(folder_id: folders.pluck(:id)) end templates diff --git a/app/controllers/template_folders_autocomplete_controller.rb b/app/controllers/template_folders_autocomplete_controller.rb index 120fe8dc..2bfdec99 100644 --- a/app/controllers/template_folders_autocomplete_controller.rb +++ b/app/controllers/template_folders_autocomplete_controller.rb @@ -3,14 +3,31 @@ class TemplateFoldersAutocompleteController < ApplicationController load_and_authorize_resource :template_folder, parent: false - LIMIT = 100 + LIMIT = 30 def index - templates_query = Template.accessible_by(current_ability).where(archived_at: nil) + parent_name, name = + if params[:parent_name].present? + [params[:parent_name], params[:q]] + else + params[:q].to_s.split(' /', 2).map(&:squish) + end - template_folders = @template_folders.where(id: templates_query.select(:folder_id)) - template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) + if name + parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil) + else + name = parent_name + end - render json: template_folders.as_json(only: %i[name archived_at]) + template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:), + Template.accessible_by(current_ability)) + + name = name.to_s.downcase + + template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT) + + render json: template_folders.preload(:parent_folder) + .sort_by { |e| e.name.downcase.index(name) || Float::MAX } + .as_json(only: %i[name archived_at], methods: %i[full_name]) end end diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index 850a9046..25e489ce 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController helper_method :selected_order + TEMPLATES_PER_PAGE = 12 + FOLDERS_PER_PAGE = 18 + def show - @templates = @template_folder.templates.active.accessible_by(current_ability) - .preload(:author, :template_accesses) - @templates = Templates.search(current_user, @templates, params[:q]) - @templates = Templates::Order.call(@templates, current_user, selected_order) + @templates = Template.active.accessible_by(current_ability) + .where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])]) + .preload(:author, :template_accesses) + + @template_folders = + @template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id)) + + @template_folders = TemplateFolders.search(@template_folders, params[:q]) + @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order) + + if @templates.exists? + @templates = Templates.search(current_user, @templates, params[:q]) + @templates = Templates::Order.call(@templates, current_user, selected_order) - @pagy, @templates = pagy_auto(@templates, limit: 12) + limit = + if @template_folders.size < 4 + TEMPLATES_PER_PAGE + else + (@template_folders.size < 7 ? 9 : 6) + end + + @pagy, @templates = pagy_auto(@templates, limit:) + + load_related_submissions if params[:q].present? && @templates.blank? + else + @pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE) + + @templates = @templates.none + end end def edit; end @@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController def template_folder_params params.require(:template_folder).permit(:name) end + + def load_related_submissions + @related_submissions = + Submission.accessible_by(current_ability) + .where(archived_at: nil) + .where(template_id: current_account.templates.active + .where(folder: [@template_folder, *@template_folder.subfolders]) + .select(:id)) + .preload(:template_accesses, :created_by_user, + template: :author, + submitters: :start_form_submission_events) + + @related_submissions = Submissions.search(current_user, @related_submissions, params[:q]) + .order(id: :desc) + + @related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5) + end end diff --git a/app/controllers/templates_archived_controller.rb b/app/controllers/templates_archived_controller.rb index 34d2b601..69b16936 100644 --- a/app/controllers/templates_archived_controller.rb +++ b/app/controllers/templates_archived_controller.rb @@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController load_and_authorize_resource :template, parent: false def index - @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) + @templates = @templates.where.not(archived_at: nil) + .preload(:author, :template_accesses, folder: :parent_folder) + .order(id: :desc) + @templates = Templates.search(current_user, @templates, params[:q]) @pagy, @templates = pagy_auto(@templates, limit: 12) + + return unless params[:q].present? && @templates.blank? + + @related_submissions = + Submission.accessible_by(current_ability) + .joins(:template) + .where.not(templates: { archived_at: nil }) + .preload(:template_accesses, :created_by_user, + template: :author, + submitters: :start_form_submission_events) + + @related_submissions = Submissions.search(current_user, @related_submissions, params[:q]) + .order(id: :desc) + + @related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5) end end diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 218c89e7..4497038e 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController helper_method :selected_order def index - @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)) + @template_folders = + TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates) @template_folders = TemplateFolders.search(@template_folders, params[:q]) - @template_folders = sort_template_folders(@template_folders, current_user, selected_order) + @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order) @pagy, @template_folders = pagy( @template_folders, @@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController Templates.search(current_user, rel, params[:q]) end - def sort_template_folders(template_folders, current_user, order) - case order - when 'used_at' - subquery = - Template.left_joins(:submissions) - .group(:folder_id) - .where(account_id: current_user.account_id) - .select( - :folder_id, - Template.arel_table[:updated_at].maximum.as('updated_at_max'), - Submission.arel_table[:created_at].maximum.as('submission_created_at_max') - ) - - template_folders = template_folders.joins( - Template.arel_table - .join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin) - .on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id])) - .join_sources - ) - - template_folders.order( - Arel::Nodes::Case.new - .when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max])) - .then(Template.arel_table[:submission_created_at_max]) - .else(Template.arel_table[:updated_at_max]) - .desc - ) - when 'name' - template_folders.order(name: :asc) - else - template_folders.order(id: :desc) - end - end - def selected_order @selected_order ||= if cookies.permanent[:dashboard_templates_order].blank? || diff --git a/app/controllers/templates_folders_controller.rb b/app/controllers/templates_folders_controller.rb index 3fdcc873..3e83023a 100644 --- a/app/controllers/templates_folders_controller.rb +++ b/app/controllers/templates_folders_controller.rb @@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController def edit; end def update - @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name]) + name = [params[:parent_name], params[:name]].compact_blank.join(' / ') + + @template.folder = TemplateFolders.find_or_create_by_name(current_user, name) if @template.save redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved')) diff --git a/app/javascript/elements/dashboard_dropzone.js b/app/javascript/elements/dashboard_dropzone.js index 888c99ee..886aae37 100644 --- a/app/javascript/elements/dashboard_dropzone.js +++ b/app/javascript/elements/dashboard_dropzone.js @@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement { static [target.static] = [ 'form', 'fileDropzone', + 'folderDropzone', 'fileDropzoneLoading' ] @@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement { window.addEventListener('dragleave', this.onWindowDragleave) this.fileDropzone?.addEventListener('drop', this.onDropFile) + this.folderDropzone?.addEventListener('drop', this.onDropNewFolder) this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el))) this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate)) this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart)) - return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { + return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { el?.addEventListener('dragover', this.onDragover) el?.addEventListener('dragleave', this.onDragleave) }) @@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement { onTemplateDragStart = (e) => { const id = e.target.href.split('/').pop() + this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden')) + this.folderDropzone?.classList?.remove('hidden') + window.flash?.remove() + e.dataTransfer.effectAllowed = 'move' if (id) { @@ -104,7 +110,7 @@ export default targetable(class extends HTMLElement { } else { const formData = new FormData() - formData.append('name', el.innerText.trim()) + formData.append('name', el.dataset.fullName) fetch(`/templates/${templateId}/folder`, { method: 'PUT', @@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement { } } + onDropNewFolder (e) { + e.preventDefault() + + const templateId = e.dataTransfer.getData('template_id') + + const a = document.createElement('a') + + a.href = `/templates/${templateId}/folder/edit?autocomplete=false` + a.dataset.turboFrame = 'modal' + a.classList.add('hidden') + + document.body.append(a) + + a.click() + + a.remove() + } + onDragleave () { this.style.backgroundColor = null @@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement { this.isDrag = true + window.flash?.remove() this.fileDropzone?.classList?.remove('hidden') this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' }) @@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement { this.isDrag = false this.fileDropzone?.classList?.add('hidden') + this.folderDropzone?.classList?.add('hidden') this.hiddenOnDrag.forEach((el) => { el.style.display = null }) diff --git a/app/javascript/elements/folder_autocomplete.js b/app/javascript/elements/folder_autocomplete.js index 2ef977c5..de2a5069 100644 --- a/app/javascript/elements/folder_autocomplete.js +++ b/app/javascript/elements/folder_autocomplete.js @@ -2,6 +2,8 @@ import autocomplete from 'autocompleter' export default class extends HTMLElement { connectedCallback () { + if (this.dataset.enabled === 'false') return + autocomplete({ input: this.input, preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1, @@ -14,12 +16,16 @@ export default class extends HTMLElement { } onSelect = (item) => { - this.input.value = item.name + this.input.value = this.dataset.parentName ? item.name : item.full_name } fetch = (text, resolve) => { const queryParams = new URLSearchParams({ q: text }) + if (this.dataset.parentName) { + queryParams.append('parent_name', this.dataset.parentName) + } + fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => { const items = await resp.json() @@ -34,7 +40,7 @@ export default class extends HTMLElement { div.setAttribute('dir', 'auto') - div.textContent = item.name + div.textContent = this.dataset.parentName ? item.name : item.full_name return div } diff --git a/app/javascript/elements/page_container.js b/app/javascript/elements/page_container.js index d909ae7f..ca5221ba 100644 --- a/app/javascript/elements/page_container.js +++ b/app/javascript/elements/page_container.js @@ -1,14 +1,12 @@ export default class extends HTMLElement { connectedCallback () { - this.image.addEventListener('load', (e) => { - this.image.setAttribute('width', e.target.naturalWidth) - this.image.setAttribute('height', e.target.naturalHeight) + const image = this.querySelector('img') + + image.addEventListener('load', (e) => { + image.setAttribute('width', e.target.naturalWidth) + image.setAttribute('height', e.target.naturalHeight) this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}` }) } - - get image () { - return this.querySelector('img') - } } diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index bc5c255b..e5560800 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -56,11 +56,11 @@
@@ -333,6 +333,13 @@ export default { verification: this.t('verify_id') } }, + isShowSignatureId () { + if ([true, false].includes(this.field.preferences?.with_signature_id)) { + return this.field.preferences.with_signature_id + } else { + return this.withSignatureId + } + }, alignClasses () { if (!this.field.preferences) { return { 'items-center': true } @@ -460,9 +467,6 @@ export default { fontScale () { return 1000 / 612.0 }, - ladscapeScale () { - return 8.5 / 11.0 - }, computedStyle () { const { x, y, w, h } = this.area diff --git a/app/javascript/submission_form/verification_step.vue b/app/javascript/submission_form/verification_step.vue index 57d2b37d..833fa68c 100644 --- a/app/javascript/submission_form/verification_step.vue +++ b/app/javascript/submission_form/verification_step.vue @@ -149,7 +149,8 @@ export default { return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, { method: 'PUT', body: JSON.stringify({ - submitter_slug: this.submitterSlug + submitter_slug: this.submitterSlug, + redirect_url: document.location.href }), headers: { 'Content-Type': 'application/json' } }).then(async (resp) => { diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index 9bf5970c..9622b503 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -140,6 +140,7 @@ :background-color="'white'" :with-required="false" :with-areas="false" + :with-signature-id="withSignatureId" @click-formula="isShowFormulaModal = true" @click-font="isShowFontModal = true" @click-description="isShowDescriptionModal = true" @@ -347,6 +348,11 @@ export default { required: false, default: false }, + withSignatureId: { + type: Boolean, + required: false, + default: null + }, defaultSubmitters: { type: Array, required: false, @@ -414,9 +420,6 @@ export default { fontScale () { return 1040 / 612.0 }, - ladscapeScale () { - return 8.5 / 11.0 - }, isDefaultValuePresent () { if (this.field?.type === 'radio' && this.field?.areas?.length > 1) { return false diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 2dca776a..ef0748f6 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -330,6 +330,7 @@ :input-mode="inputMode" :default-fields="[...defaultRequiredFields, ...defaultFields]" :allow-draw="!onlyDefinedFields || drawField" + :with-signature-id="withSignatureId" :data-document-uuid="document.uuid" :default-submitters="defaultSubmitters" :drag-field-placeholder="fieldsDragFieldRef.value || dragField" @@ -436,6 +437,7 @@ :default-required-fields="defaultRequiredFields" :field-types="fieldTypes" :with-sticky-submitters="withStickySubmitters" + :with-signature-id="withSignatureId" :only-defined-fields="onlyDefinedFields" :editable="editable" :show-tour-start-form="showTourStartForm" @@ -541,6 +543,7 @@ export default { withPayment: this.withPayment, isPaymentConnected: this.isPaymentConnected, withFormula: this.withFormula, + withSignatureId: this.withSignatureId, withConditions: this.withConditions, isInlineSize: this.isInlineSize, defaultDrawFieldType: this.defaultDrawFieldType, @@ -563,6 +566,11 @@ export default { required: false, default: false }, + withSignatureId: { + type: Boolean, + required: false, + default: null + }, backgroundColor: { type: String, required: false, @@ -1068,6 +1076,11 @@ export default { } } + if (type === 'signature' && [true, false].includes(this.withSignatureId)) { + field.preferences ||= {} + field.preferences.with_signature_id = this.withSignatureId + } + this.template.fields.push(field) this.save() @@ -1474,6 +1487,11 @@ export default { field.preferences ||= {} field.preferences.format ||= this.defaultDateFormat } + + if (field.type === 'signature' && [true, false].includes(this.withSignatureId)) { + field.preferences ||= {} + field.preferences.with_signature_id = this.withSignatureId + } } const fieldArea = { diff --git a/app/javascript/template_builder/document.vue b/app/javascript/template_builder/document.vue index 0ca06084..abba5052 100644 --- a/app/javascript/template_builder/document.vue +++ b/app/javascript/template_builder/document.vue @@ -10,6 +10,7 @@ :data-page="index" :areas="areasIndex[index]" :allow-draw="allowDraw" + :with-signature-id="withSignatureId" :is-drag="isDrag" :with-field-placeholder="withFieldPlaceholder" :default-fields="defaultFields" @@ -66,6 +67,11 @@ export default { required: false, default: false }, + withSignatureId: { + type: Boolean, + required: false, + default: null + }, drawFieldType: { type: String, required: false, diff --git a/app/javascript/template_builder/field.vue b/app/javascript/template_builder/field.vue index a190d6cb..f9e1c0e9 100644 --- a/app/javascript/template_builder/field.vue +++ b/app/javascript/template_builder/field.vue @@ -125,6 +125,7 @@ :field="field" :default-field="defaultField" :editable="editable" + :with-signature-id="withSignatureId" :background-color="dropdownBgColor" @click-formula="isShowFormulaModal = true" @click-font="isShowFontModal = true" @@ -302,6 +303,11 @@ export default { type: Object, required: true }, + withSignatureId: { + type: Boolean, + required: false, + default: null + }, withOptions: { type: Boolean, required: false, diff --git a/app/javascript/template_builder/field_settings.vue b/app/javascript/template_builder/field_settings.vue index e36b63d8..b78c6b4e 100644 --- a/app/javascript/template_builder/field_settings.vue +++ b/app/javascript/template_builder/field_settings.vue @@ -320,6 +320,21 @@ {{ t('format') }}
+
  • + +
  • @@ -79,7 +79,7 @@ <%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> -
    <%= text %>
    +
    <%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %>
    @@ -101,7 +101,7 @@ <%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> -
    <%= text %>
    +
    <%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %>
    diff --git a/app/views/icons/_arrow_left.html.erb b/app/views/icons/_arrow_left.html.erb new file mode 100644 index 00000000..6357e74c --- /dev/null +++ b/app/views/icons/_arrow_left.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/icons/_circle_arrow_left.html.erb b/app/views/icons/_circle_arrow_left.html.erb new file mode 100644 index 00000000..4b786c4a --- /dev/null +++ b/app/views/icons/_circle_arrow_left.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/icons/_circle_chevron_left.html.erb b/app/views/icons/_circle_chevron_left.html.erb new file mode 100644 index 00000000..3d12fb49 --- /dev/null +++ b/app/views/icons/_circle_chevron_left.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/icons/_folder_plus.html.erb b/app/views/icons/_folder_plus.html.erb new file mode 100644 index 00000000..4dcc18d1 --- /dev/null +++ b/app/views/icons/_folder_plus.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/icons/_slash.html.erb b/app/views/icons/_slash.html.erb new file mode 100644 index 00000000..4e416c0a --- /dev/null +++ b/app/views/icons/_slash.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/shared/_github.html.erb b/app/views/shared/_github.html.erb index 49e1f380..df415db6 100644 --- a/app/views/shared/_github.html.erb +++ b/app/views/shared/_github.html.erb @@ -1,5 +1,5 @@ - + <%= svg_icon('start', class: 'h-3 w-3') %> 9k diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index 8355f543..32964656 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -1,7 +1,7 @@
    <% end %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 53ab3dcc..0ab61c68 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -2,7 +2,11 @@ <%= render 'submissions/preview_tags' %> <% end %> <% font_scale = 1040.0 / PdfUtils::US_LETTER_W %> -<% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %> +<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %> +<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %> +<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true %> +<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %> +<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
    @@ -108,17 +112,18 @@ <% value = values[field['uuid']] %> <% value ||= field['default_value'] if field['type'] == 'heading' %> <% next if value.blank? %> + <% submitter = submitters_index[field['submitter_uuid']] %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> - <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> + <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %> <% else %> - <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> + <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %> <% end %> <% end %>
    diff --git a/app/views/submissions_archived/index.html.erb b/app/views/submissions_archived/index.html.erb index cad51398..2a63c0c0 100644 --- a/app/views/submissions_archived/index.html.erb +++ b/app/views/submissions_archived/index.html.erb @@ -1,14 +1,14 @@ <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
    - <%= link_to root_path do %> - ← - <%= t('back_to_active') %> + <%= link_to root_path, class: 'flex items-center' do %> + <%= svg_icon('chevron_left', class: 'w-5 h-5') %> + <%= t('back_to_active') %> <% end %>
    -
    +
    -

    <%= t('submissions') %> <%= t('archived') %>

    +

    <%= t('submissions') %> <%= t('archived') %>

    <% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %> diff --git a/app/views/submissions_filters/_applied_filters.html.erb b/app/views/submissions_filters/_applied_filters.html.erb index 44df190c..293a3bad 100644 --- a/app/views/submissions_filters/_applied_filters.html.erb +++ b/app/views/submissions_filters/_applied_filters.html.erb @@ -10,6 +10,17 @@ <% end %>
    <% end %> +<% if params[:folder].present? %> +
    + <%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> + <%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %> + <%= params[:folder] %> + <% end %> + <%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= svg_icon('x', class: 'w-5 h-5') %> + <% end %> +
    +<% end %> <% if params[:author].present? %>
    <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 2ef9d197..d6dbc629 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -84,7 +84,7 @@ <% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %> <% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %> - <%= render 'submissions/value', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %> + <%= render 'submissions/value', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %> <% end %>
    diff --git a/app/views/template_folders/_folder.html.erb b/app/views/template_folders/_folder.html.erb index 7c35e950..8413e045 100644 --- a/app/views/template_folders/_folder.html.erb +++ b/app/views/template_folders/_folder.html.erb @@ -1,5 +1,5 @@ <% is_long = folder.name.size > 32 %> -
    + <% if !is_long %> <%= svg_icon('folder', class: 'w-6 h-6') %> <% end %> diff --git a/app/views/template_folders/show.html.erb b/app/views/template_folders/show.html.erb index 31b43790..2f6ac8ea 100644 --- a/app/views/template_folders/show.html.erb +++ b/app/views/template_folders/show.html.erb @@ -1,24 +1,27 @@
    - <%= link_to root_path do %> - ← - <%= t('home') %> + <%= link_to @template_folder.parent_folder ? folder_path(@template_folder.parent_folder) : root_path, class: 'flex items-center' do %> + <%= svg_icon('chevron_left', class: 'w-5 h-5') %> + <%= @template_folder.parent_folder&.name || t('home') %> <% end %>
    <%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %> - + <% end %> <%= render 'templates/dashboard_dropzone', style: 'height: 137px' %> + <% unless @template_folder.parent_folder %> + <%= render 'templates/dashboard_folder_dropzone', style: 'height: 137px' %> + <% end %>

    <%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %> <%= @template_folder.name %> - <% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %> + <% if can?(:update, @template_folder) && @template_folder.full_name != TemplateFolder::DEFAULT_NAME %> <%= svg_icon('pencil', class: 'w-7 h-7') %> @@ -27,18 +30,23 @@ <% end %>

    - <% if params[:q].present? || @pagy.pages > 1 %> + <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %> <%= render 'shared/search_input' %> <% end %> <% if can?(:create, ::Template) %> - <%= render 'templates/upload_button', folder_name: @template_folder.name %> - <%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %> + <%= render 'templates/upload_button', folder_name: @template_folder.full_name %> + <%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <% end %> <% end %>
    + <% if @template_folders.present? %> +
    + <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> +
    + <% end %> <% if @pagy.count.nil? || @pagy.count > 0 %>
    <%= render partial: 'templates/template', collection: @templates %> @@ -48,12 +56,21 @@ <%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %> <% end %> <% end %> - <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %> + <%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %> <% elsif params[:q].present? %>
    <%= t('templates_not_found') %>
    + <% if @related_submissions.present? %> +

    + <%= t('submissions') %> +

    +
    + <%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %> +
    + <%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q], folder: @template_folder.full_name) %> + <% end %> <% end %> diff --git a/app/views/templates/_dashboard_folder_dropzone.html.erb b/app/views/templates/_dashboard_folder_dropzone.html.erb new file mode 100644 index 00000000..01a511f0 --- /dev/null +++ b/app/views/templates/_dashboard_folder_dropzone.html.erb @@ -0,0 +1,20 @@ + diff --git a/app/views/templates/_template.html.erb b/app/views/templates/_template.html.erb index 0ef7c9db..1495be02 100644 --- a/app/views/templates/_template.html.erb +++ b/app/views/templates/_template.html.erb @@ -1,6 +1,6 @@
    -
    +
    <% if template.template_accesses.present? %> <%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %> <% end %> @@ -22,7 +22,7 @@ <% if template.archived_at? %> <%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %> - <%= template.folder.name %> + <%= template.folder.full_name %> <% end %>

    diff --git a/app/views/templates/_title.html.erb b/app/views/templates/_title.html.erb index 28d2e037..99f11044 100644 --- a/app/views/templates/_title.html.erb +++ b/app/views/templates/_title.html.erb @@ -16,12 +16,12 @@
    <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %> - <%= @template.folder.name %> + <%= @template.folder.full_name %> <% if can?(:update, template) %> - + <%= svg_icon('pencil_share', class: 'w-5 h-5') %> diff --git a/app/views/templates/new.html.erb b/app/views/templates/new.html.erb index 98c6551a..c55ef24a 100644 --- a/app/views/templates/new.html.erb +++ b/app/views/templates/new.html.erb @@ -16,7 +16,7 @@ <%= svg_icon('folder', class: 'w-6 h-6') %> - + <%= t('change_folder') %> diff --git a/app/views/templates_archived/index.html.erb b/app/views/templates_archived/index.html.erb index f126f10b..995a63ad 100644 --- a/app/views/templates_archived/index.html.erb +++ b/app/views/templates_archived/index.html.erb @@ -1,12 +1,12 @@
    - <%= link_to root_path do %> - ← - <%= t('back_to_active') %> + <%= link_to root_path, class: 'flex items-center' do %> + <%= svg_icon('chevron_left', class: 'w-5 h-5') %> + <%= t('back_to_active') %> <% end %>
    -

    <%= t('document_templates_html') %> <%= t('archived') %>

    +

    <%= t('document_templates_html') %> <%= t('archived') %>

    <% if params[:q].present? || @pagy.pages > 1 %> <%= render 'shared/search_input', placeholder: "#{t('search')}..." %> @@ -22,5 +22,14 @@ <%= t('templates_not_found') %>
    + <% if @related_submissions.present? %> +

    + <%= t('submissions') %> +

    +
    + <%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %> +
    + <%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_archived_index_path(q: params[:q]) %> + <% end %> <% end %> <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %> diff --git a/app/views/templates_archived_submissions/index.html.erb b/app/views/templates_archived_submissions/index.html.erb index 0536d8a9..fe45b8bd 100644 --- a/app/views/templates_archived_submissions/index.html.erb +++ b/app/views/templates_archived_submissions/index.html.erb @@ -1,15 +1,15 @@ <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> <% with_filters = @pagy.pages > 1 || params[:q].present? || filter_params.present? %> <%= render 'templates/title', template: @template %> -
    - <%= link_to template_path(@template) do %> - ← - <%= t('back_to_active') %> - <% end %> -
    -
    +
    +
    + <%= link_to template_path(@template), class: 'flex items-center' do %> + <%= svg_icon('chevron_left', class: 'w-5 h-5') %> + <%= t('back_to_active') %> + <% end %> +

    <%= t('submissions') %> <%= t('archived') %>

    diff --git a/app/views/templates_dashboard/index.html.erb b/app/views/templates_dashboard/index.html.erb index fcc17758..d8509885 100644 --- a/app/views/templates_dashboard/index.html.erb +++ b/app/views/templates_dashboard/index.html.erb @@ -11,6 +11,7 @@ <% unless show_dropzone %> <%= render 'templates/dashboard_dropzone', style: 'height: 114px' %> <% end %> + <%= render 'templates/dashboard_folder_dropzone', style: 'height: 114px' %>
    <% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
    diff --git a/app/views/templates_folders/edit.html.erb b/app/views/templates_folders/edit.html.erb index fe41c0fd..733dafb6 100644 --- a/app/views/templates_folders/edit.html.erb +++ b/app/views/templates_folders/edit.html.erb @@ -1,7 +1,39 @@ +<% with_subfolder = @template.folder.name != TemplateFolder::DEFAULT_NAME && params[:subfolder] != 'false' %> <%= render 'shared/turbo_modal', title: t('move_into_folder') do %> - <%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> -
    - + <% if with_subfolder %> + <%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'subfolder_form', autocomplete: :off } do |f| %> + <%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %> + +
    + +
    +
    +
    + + <%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true %> + +
    +
    + <%= f.button button_title(title: t('move'), disabled_with: t('moving')), class: 'base-button' %> +
    + <% end %> + <% end %> + <%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'folder_form', autocomplete: :off, class: "mt-6 #{'hidden' if with_subfolder}" } do |f| %> +
    + <%= f.text_field :name, required: true, placeholder: "#{t('new_folder_name')}...", class: 'base-input w-full', autofocus: true %>
    diff --git a/config/initializers/rouge.rb b/config/initializers/rouge.rb index bf00d89a..e2c69ce7 100644 --- a/config/initializers/rouge.rb +++ b/config/initializers/rouge.rb @@ -8,6 +8,7 @@ module Rouge module Lexers autoload :JSON, 'rouge/lexers/json' + autoload :Shell, 'rouge/lexers/shell' end autoload :Formatter, 'rouge/formatter' diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 71e846ab..0bd59e7f 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -460,6 +460,10 @@ en: &en users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.' move_into_folder: Move Into Folder new_folder_name: New Folder Name + new_subfolder_name: New Subfolder Name + change_parent_folder: Change Parent Folder + folder: Folder + create_a_new_folder: Create a New Folder exit_preview: Exit Preview general: General recipients: Recipients @@ -1326,6 +1330,10 @@ es: &es users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".' move_into_folder: Mover a la carpeta new_folder_name: Nuevo nombre de la carpeta + new_subfolder_name: Nuevo nombre de subcarpeta + change_parent_folder: Cambiar carpeta principal + folder: Carpeta + create_a_new_folder: Crear una nueva carpeta exit_preview: Salir de la vista previa general: General recipients: Destinatarios @@ -2191,6 +2199,10 @@ it: &it users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".' move_into_folder: Sposta nella cartella new_folder_name: Nuovo nome della cartella + new_subfolder_name: Nuovo nome della sottocartella + change_parent_folder: Cambia cartella principale + folder: Cartella + create_a_new_folder: Crea una nuova cartella exit_preview: "Esci dall'anteprima" general: Generale recipients: Destinatari @@ -3058,6 +3070,10 @@ fr: &fr users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".' move_into_folder: Déplacer dans le dossier new_folder_name: Nouveau nom du dossier + new_subfolder_name: Nouveau nom du sous-dossier + change_parent_folder: Changer le dossier parent + folder: Dossier + create_a_new_folder: Créer un nouveau dossier exit_preview: "Quitter l'aperçu" general: Général recipients: Destinataires @@ -3925,6 +3941,10 @@ pt: &pt users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".' move_into_folder: Mover para pasta new_folder_name: Novo nome da pasta + new_subfolder_name: Novo nome da subpasta + change_parent_folder: Alterar pasta pai + folder: Pasta + create_a_new_folder: Criar uma nova pasta exit_preview: Sair da pré-visualização general: Geral recipients: Destinatários @@ -4792,6 +4812,10 @@ de: &de users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".' move_into_folder: In Ordner verschieben new_folder_name: Neuer Ordnername + new_subfolder_name: Neuer Unterordnername + change_parent_folder: Übergeordneten Ordner ändern + folder: Ordner + create_a_new_folder: Neuen Ordner erstellen exit_preview: Vorschau beenden general: Allgemein recipients: Empfänger diff --git a/db/migrate/20250718121133_add_parent_folder_id_to_template_folders.rb b/db/migrate/20250718121133_add_parent_folder_id_to_template_folders.rb new file mode 100644 index 00000000..b0df85bd --- /dev/null +++ b/db/migrate/20250718121133_add_parent_folder_id_to_template_folders.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddParentFolderIdToTemplateFolders < ActiveRecord::Migration[8.0] + def change + add_reference :template_folders, :parent_folder, foreign_key: { to_table: :template_folders }, index: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fcb693a..136d878a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do +ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "plpgsql" @@ -356,8 +356,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do t.datetime "archived_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "parent_folder_id" t.index ["account_id"], name: "index_template_folders_on_account_id" t.index ["author_id"], name: "index_template_folders_on_author_id" + t.index ["parent_folder_id"], name: "index_template_folders_on_parent_folder_id" end create_table "template_sharings", force: :cascade do |t| @@ -498,6 +500,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do add_foreign_key "submitters", "submissions" add_foreign_key "template_accesses", "templates" add_foreign_key "template_folders", "accounts" + add_foreign_key "template_folders", "template_folders", column: "parent_folder_id" add_foreign_key "template_folders", "users", column: "author_id" add_foreign_key "template_sharings", "templates" add_foreign_key "templates", "accounts" diff --git a/lib/submissions/filter.rb b/lib/submissions/filter.rb index 27a1c375..a6677bf4 100644 --- a/lib/submissions/filter.rb +++ b/lib/submissions/filter.rb @@ -5,6 +5,7 @@ module Submissions ALLOWED_PARAMS = %w[ author status + folder completed_at_from completed_at_to created_at_from @@ -24,6 +25,7 @@ module Submissions filters = normalize_filter_params(params, current_user) submissions = filter_by_author(submissions, filters, current_user) + submissions = filter_by_folder(submissions, filters, current_user) submissions = filter_by_status(submissions, filters) submissions = filter_by_created_at(submissions, filters) @@ -34,6 +36,7 @@ module Submissions return submissions if filters[:author].blank? user = current_user.account.users.find_by(email: filters[:author]) + submissions.where(created_by_user_id: user&.id || -1) end @@ -87,6 +90,17 @@ module Submissions submissions end + def filter_by_folder(submissions, filters, current_user) + return submissions if filters[:folder].blank? + + folders = + TemplateFolders.filter_by_full_name(current_user.account.template_folders, filters[:folder]) + + folders += folders.preload(:subfolders).flat_map(&:subfolders) + + submissions.joins(:template).where(templates: { folder_id: folders.map(&:id) }) + end + def filter_by_completed_at(submissions, filters) return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present? diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 2e5567bd..81095648 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -261,8 +261,6 @@ module Submissions e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present? end - submitter_field_counters = Hash.new { 0 } - info_rows = [ [ composer.document.layout.formatted_text_box( @@ -298,25 +296,53 @@ module Submissions composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } }) + submitter_field_counters = Hash.new { 0 } + grouped_value_field_names = {} + skip_grouped_field_uuids = {} + + submission.template_fields.each do |field| + next unless field['type'].in?(%w[signature initials]) + + submitter_field_counters[field['type']] += 1 + + next if field['submitter_uuid'] != submitter.uuid + + value = submitter.values[field['uuid']] + + field_name = field['title'].presence || field['name'].presence || + "#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" + + if grouped_value_field_names[value] + skip_grouped_field_uuids[field['uuid']] = true + + grouped_value_field_names[value] += ", #{field_name}" + else + grouped_value_field_names[value] = field_name + end + end + + submitter_field_counters = Hash.new { 0 } + submission.template_fields.filter_map do |field| + submitter_field_counters[field['type']] += 1 + next if field['submitter_uuid'] != submitter.uuid next if field['type'] == 'heading' next if !with_audit_values && !field['type'].in?(%w[signature initials]) - - submitter_field_counters[field['type']] += 1 + next if skip_grouped_field_uuids[field['uuid']] value = submitter.values[field['uuid']] next if Array.wrap(value).compact_blank.blank? - field_name = field['title'].presence || field['name'].to_s + field_name = grouped_value_field_names[value].presence || field['title'].presence || field['name'].to_s [ composer.formatted_text_box( [ { text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence || - "#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}\n".upcase, + "#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}".upcase, font_size: 6 } ].compact_blank, diff --git a/lib/submissions/generate_preview_attachments.rb b/lib/submissions/generate_preview_attachments.rb index e55c14f6..c1e201ca 100644 --- a/lib/submissions/generate_preview_attachments.rb +++ b/lib/submissions/generate_preview_attachments.rb @@ -14,11 +14,14 @@ module Submissions configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::WITH_SIGNATURE_ID, - AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY]) + AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, + AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true + with_signature_id_reason = + configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten) @@ -31,7 +34,8 @@ module Submissions submitters.preload(attachments_attachments: :blob).each_with_index do |s, index| GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index, with_signature_id:, is_flatten:, with_headings: index.zero?, - with_submitter_timezone:) + with_submitter_timezone:, + with_signature_id_reason:) end template = submission.template diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 3eeb2265..03b5e29b 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -139,11 +139,14 @@ module Submissions def generate_pdfs(submitter) configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::WITH_SIGNATURE_ID, - AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY]) + AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, + AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true + with_signature_id_reason = + configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten) @@ -188,11 +191,12 @@ module Submissions end fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:, - with_submitter_timezone:) + with_submitter_timezone:, + with_signature_id_reason:) end def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil, - with_submitter_timezone: false) + with_submitter_timezone: false, with_signature_id_reason: true) cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center) attachments_data_cache = {} @@ -274,6 +278,10 @@ module Submissions field_type = 'file' if field_type == 'image' && !submitter.attachments.find { |a| a.uuid == value }.image? + if field_type == 'signature' && field.dig('preferences', 'with_signature_id').in?([true, false]) + with_signature_id = field['preferences']['with_signature_id'] + end + case field_type when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) } attachment = submitter.attachments.find { |a| a.uuid == value } @@ -295,10 +303,15 @@ module Submissions timezone = submitter.account.timezone timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone - "#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \ - "#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ + if with_signature_id_reason + "#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \ + "#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ + "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ + "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" + else "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ - "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" + "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" + end end reason_text = HexaPDF::Layout::TextFragment.create(reason_string, diff --git a/lib/submitters/form_configs.rb b/lib/submitters/form_configs.rb index 9c1fa06f..84c5fc65 100644 --- a/lib/submitters/form_configs.rb +++ b/lib/submitters/form_configs.rb @@ -13,6 +13,8 @@ module Submitters AccountConfig::REUSE_SIGNATURE_KEY, AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, AccountConfig::ALLOW_TYPED_SIGNATURE, + AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, + AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze module_function @@ -31,20 +33,15 @@ module Submitters with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true + with_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true + with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY) - attrs = { completed_button:, - with_typed_signature:, - with_confetti:, - reuse_signature:, - with_decline:, - with_partial_download:, - policy_links:, - enforce_signing_order:, - completed_message:, - require_signing_reason:, - prefill_signature:, - with_signature_id: } + attrs = { completed_button:, with_typed_signature:, with_confetti:, + reuse_signature:, with_decline:, with_partial_download:, + policy_links:, enforce_signing_order:, completed_message:, + require_signing_reason:, prefill_signature:, with_submitter_timezone:, + with_signature_id_reason:, with_signature_id: } keys.each do |key| attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value diff --git a/lib/template_folders.rb b/lib/template_folders.rb index 9a3738ea..1ec3631e 100644 --- a/lib/template_folders.rb +++ b/lib/template_folders.rb @@ -3,15 +3,87 @@ module TemplateFolders module_function + def filter_by_full_name(template_folders, name) + parent_name, name = name.split(' / ', 2).map(&:squish) + + if name.present? + parent_folder = template_folders.where(parent_folder_id: nil).find_by(name: parent_name) + else + name = parent_name + end + + template_folders.where(name:, parent_folder:) + end + def search(folders, keyword) return folders if keyword.blank? folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%")) end + def filter_active_folders(template_folders, templates) + folder_exists = + templates.active.where(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id])) + .select(1).limit(1).arel.exists + + subfolders_arel = TemplateFolder.arel_table.alias('subfolders') + + subfolder_exists = + TemplateFolder.from(subfolders_arel) + .where(subfolders_arel[:parent_folder_id].eq(TemplateFolder.arel_table[:id])) + .where( + templates.active.where(Template.arel_table[:folder_id].eq(subfolders_arel[:id])).arel.exists + ).select(1).limit(1).arel.exists + + template_folders.where(folder_exists).or(template_folders.where(subfolder_exists)) + end + + def sort(template_folders, current_user, order) + case order + when 'used_at' + subquery = + Template.left_joins(:submissions) + .group(:folder_id) + .where(account_id: current_user.account_id) + .select( + :folder_id, + Template.arel_table[:updated_at].maximum.as('updated_at_max'), + Submission.arel_table[:created_at].maximum.as('submission_created_at_max') + ) + + template_folders = template_folders.joins( + Template.arel_table + .join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin) + .on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id])) + .join_sources + ) + + template_folders.order( + Arel::Nodes::Case.new + .when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max])) + .then(Template.arel_table[:submission_created_at_max]) + .else(Template.arel_table[:updated_at_max]) + .desc + ) + when 'name' + template_folders.order(name: :asc) + else + template_folders.order(id: :desc) + end + end + def find_or_create_by_name(author, name) return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME - author.account.template_folders.create_with(author:, account: author.account).find_or_create_by(name:) + parent_name, name = name.split(' / ', 2).map(&:squish) + + if name.present? + parent_folder = author.account.template_folders.create_with(author:) + .find_or_create_by(name: parent_name, parent_folder_id: nil) + else + name = parent_name + end + + author.account.template_folders.create_with(author:).find_or_create_by(name:, parent_folder:) end end