From 9dac90e6cc248b4e89376827d94dd8b9c601fb49 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 14 Jul 2025 13:29:39 +0300 Subject: [PATCH 01/10] cleanup --- app/javascript/submission_form/area.vue | 3 --- app/javascript/template_builder/area.vue | 3 --- 2 files changed, 6 deletions(-) diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index bc5c255b..24701976 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -460,9 +460,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/template_builder/area.vue b/app/javascript/template_builder/area.vue index 9bf5970c..423a6082 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -414,9 +414,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 From 4f48c4fd9f4bae33168c11c30c09389837acd6fa Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 14 Jul 2025 13:36:34 +0300 Subject: [PATCH 02/10] fix gh font --- app/views/shared/_github.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 6f7b9b1ebf78039d6ed197b88d0c50dbe7082cd7 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 14 Jul 2025 13:43:26 +0300 Subject: [PATCH 03/10] code highligh --- app/views/api_settings/index.html.erb | 6 +++--- config/initializers/rouge.rb | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/api_settings/index.html.erb b/app/views/api_settings/index.html.erb index 7defd3c2..c5a761a8 100644 --- a/app/views/api_settings/index.html.erb +++ b/app/views/api_settings/index.html.erb @@ -53,7 +53,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') %>
@@ -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/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' From 708b8afdf553defaf6661e6f9430cf684de21246 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 14 Jul 2025 17:13:04 +0300 Subject: [PATCH 04/10] optimize --- app/javascript/elements/page_container.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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') - } } From 8bf727c43c54d6757253a2f8d04e8ddc5c2db49d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 15 Jul 2025 09:19:49 +0300 Subject: [PATCH 05/10] adjust font auto --- app/javascript/template_builder/font_modal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/template_builder/font_modal.vue b/app/javascript/template_builder/font_modal.vue index 86ee657a..ef0f74b1 100644 --- a/app/javascript/template_builder/font_modal.vue +++ b/app/javascript/template_builder/font_modal.vue @@ -163,7 +163,7 @@ class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview" :style="{ color: preferences.color || 'black', - fontSize: (preferences.font_size || 12) + 'pt', + fontSize: (preferences.font_size || 11) + 'pt', }" :class="textClasses" > From ec9293bac078bf0db75e46ab3370906790104268 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 17 Jul 2025 10:54:44 +0300 Subject: [PATCH 06/10] with signature id --- app/javascript/submission_form/area.vue | 13 +++++++++--- app/javascript/template_builder/area.vue | 6 ++++++ app/javascript/template_builder/builder.vue | 18 +++++++++++++++++ app/javascript/template_builder/document.vue | 6 ++++++ app/javascript/template_builder/field.vue | 6 ++++++ .../template_builder/field_settings.vue | 20 +++++++++++++++++++ app/javascript/template_builder/fields.vue | 6 ++++++ app/javascript/template_builder/i18n.js | 6 ++++++ app/javascript/template_builder/page.vue | 6 ++++++ .../generate_result_attachments.rb | 4 ++++ 10 files changed, 88 insertions(+), 3 deletions(-) diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 24701976..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 } diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index 423a6082..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, 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') }}
+
  • + +
  • (type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) } attachment = submitter.attachments.find { |a| a.uuid == value } From 06a1e2992d7ae4db4d2c6799ba7b493821d973de Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 17 Jul 2025 18:33:24 +0300 Subject: [PATCH 07/10] fix redirect url --- app/javascript/submission_form/verification_step.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) => { From 69d63ecffe87275baa57fc997e36a0030f7e5655 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 18 Jul 2025 10:30:37 +0300 Subject: [PATCH 08/10] with signature id reason --- app/models/account_config.rb | 1 + app/views/submissions/_value.html.erb | 19 ++++++++++------- app/views/submissions/show.html.erb | 13 ++++++++---- app/views/submit_form/show.html.erb | 2 +- .../generate_preview_attachments.rb | 8 +++++-- .../generate_result_attachments.rb | 21 +++++++++++++------ lib/submitters/form_configs.rb | 21 ++++++++----------- 7 files changed, 52 insertions(+), 33 deletions(-) diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 60dcbf51..66b48577 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -41,6 +41,7 @@ class AccountConfig < ApplicationRecord FORCE_SSO_AUTH_KEY = 'force_sso_auth' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' WITH_SIGNATURE_ID = 'with_signature_id' + WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason' WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index 24c712b2..81158cdb 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -16,15 +16,18 @@
    ID: <%= attachment.uuid %>
    + <% if local_assigns[:with_signature_id_reason] != false %> +
    + <% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %> + <% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %> + <% if submitter.email %> + <<%= submitter.email %>> + <% end %> +
    + <% end %>
    - <% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %> - <% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %> - <% if submitter.email %> - <<%= submitter.email %>> - <% end %> -
    -
    - <%= l(attachment.created_at.in_time_zone(local_assigns[:timezone]), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(local_assigns[:timezone], attachment.created_at) %> + <% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %> + <%= l(attachment.created_at.in_time_zone(timezone), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
  • <% 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/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/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 0884b395..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 = {} @@ -299,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 From ec944a2283cf80a9a25ba65abd12b5303e869ae7 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 20 Jul 2025 08:28:19 +0300 Subject: [PATCH 09/10] add subfolders --- app/controllers/api/form_events_controller.rb | 5 +- app/controllers/api/submissions_controller.rb | 7 +- app/controllers/api/templates_controller.rb | 6 +- ...emplate_folders_autocomplete_controller.rb | 27 +++++-- .../template_folders_controller.rb | 53 +++++++++++-- .../templates_archived_controller.rb | 20 ++++- .../templates_dashboard_controller.rb | 39 +--------- .../templates_folders_controller.rb | 4 +- app/javascript/elements/dashboard_dropzone.js | 30 +++++++- .../elements/folder_autocomplete.js | 10 ++- app/models/template.rb | 6 +- app/models/template_folder.rb | 32 +++++--- app/views/icons/_arrow_left.html.erb | 3 + app/views/icons/_circle_arrow_left.html.erb | 3 + app/views/icons/_circle_chevron_left.html.erb | 3 + app/views/icons/_folder_plus.html.erb | 3 + app/views/icons/_slash.html.erb | 3 + app/views/shared/_settings_nav.html.erb | 2 +- app/views/submissions_archived/index.html.erb | 10 +-- .../_applied_filters.html.erb | 11 +++ app/views/template_folders/_folder.html.erb | 2 +- app/views/template_folders/show.html.erb | 35 ++++++--- .../_dashboard_folder_dropzone.html.erb | 20 +++++ app/views/templates/_template.html.erb | 4 +- app/views/templates/_title.html.erb | 4 +- app/views/templates/new.html.erb | 2 +- app/views/templates_archived/index.html.erb | 17 ++++- .../index.html.erb | 14 ++-- app/views/templates_dashboard/index.html.erb | 1 + app/views/templates_folders/edit.html.erb | 38 +++++++++- config/locales/i18n.yml | 24 ++++++ ...dd_parent_folder_id_to_template_folders.rb | 7 ++ db/schema.rb | 5 +- lib/submissions/filter.rb | 14 ++++ lib/template_folders.rb | 74 ++++++++++++++++++- 35 files changed, 430 insertions(+), 108 deletions(-) create mode 100644 app/views/icons/_arrow_left.html.erb create mode 100644 app/views/icons/_circle_arrow_left.html.erb create mode 100644 app/views/icons/_circle_chevron_left.html.erb create mode 100644 app/views/icons/_folder_plus.html.erb create mode 100644 app/views/icons/_slash.html.erb create mode 100644 app/views/templates/_dashboard_folder_dropzone.html.erb create mode 100644 db/migrate/20250718121133_add_parent_folder_id_to_template_folders.rb 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/models/template.rb b/app/models/template.rb index 59f2c36d..a03bd5bc 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -72,12 +72,14 @@ class Template < ApplicationRecord scope :active, -> { where(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) } - delegate :name, to: :folder, prefix: true - def application_key external_id end + def folder_name + folder.full_name + end + private def maybe_set_default_folder diff --git a/app/models/template_folder.rb b/app/models/template_folder.rb index ea9401f6..58539a2e 100644 --- a/app/models/template_folder.rb +++ b/app/models/template_folder.rb @@ -4,36 +4,50 @@ # # Table name: template_folders # -# id :bigint not null, primary key -# archived_at :datetime -# name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint not null -# author_id :bigint not null +# id :bigint not null, primary key +# archived_at :datetime +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# author_id :bigint not null +# parent_folder_id :bigint # # Indexes # -# index_template_folders_on_account_id (account_id) -# index_template_folders_on_author_id (author_id) +# index_template_folders_on_account_id (account_id) +# index_template_folders_on_author_id (author_id) +# index_template_folders_on_parent_folder_id (parent_folder_id) # # Foreign Keys # # fk_rails_... (account_id => accounts.id) # fk_rails_... (author_id => users.id) +# fk_rails_... (parent_folder_id => template_folders.id) # class TemplateFolder < ApplicationRecord DEFAULT_NAME = 'Default' belongs_to :author, class_name: 'User' belongs_to :account + belongs_to :parent_folder, class_name: 'TemplateFolder', optional: true has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder + has_many :subfolders, class_name: 'TemplateFolder', foreign_key: :parent_folder_id, inverse_of: :parent_folder, + dependent: :destroy has_many :active_templates, -> { where(archived_at: nil) }, class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder scope :active, -> { where(archived_at: nil) } + def full_name + if parent_folder_id? + [parent_folder.name, name].join(' / ') + else + name + end + end + def default? name == DEFAULT_NAME end 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/_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 @@