From e11f4bddbeb9a591766df124f31a0351b2b07148 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 21 Sep 2026 16:36:09 +0300 Subject: [PATCH] review fixes --- .erb_lint.yml | 5 + app/controllers/users_controller.rb | 2 +- app/javascript/application.js | 3 +- app/javascript/elements/dashboard_dropzone.js | 92 ++++++++---- app/javascript/elements/native_modal.js | 16 ++- app/javascript/elements/open_modal_mobile.js | 10 +- app/javascript/elements/submit_form.js | 20 ++- app/javascript/native.js | 7 +- app/javascript/template_builder/builder.vue | 54 ++++--- app/javascript/template_builder/i18n.js | 7 + app/views/accounts/show.html.erb | 2 +- app/views/devise/passwords/new.html.erb | 11 +- app/views/email_smtp_settings/index.html.erb | 2 +- app/views/layouts/_head_tags.html.erb | 2 +- .../_native_path_configuration.json.erb | 2 + .../notifications_settings/index.html.erb | 2 +- .../personalization_settings/show.html.erb | 2 +- app/views/profile/index.html.erb | 2 +- app/views/shared/_native_navbar.html.erb | 7 + app/views/shared/_settings_nav.html.erb | 33 ++--- app/views/shared/_turbo_drawer.html.erb | 2 +- app/views/shared/_turbo_modal.html.erb | 2 +- app/views/shared/_turbo_modal_large.html.erb | 2 +- app/views/sms_settings/index.html.erb | 2 +- app/views/sso_settings/index.html.erb | 2 +- app/views/storage_settings/index.html.erb | 4 +- app/views/submissions/show.html.erb | 10 +- app/views/submissions_archived/index.html.erb | 2 +- .../submissions_dashboard/index.html.erb | 2 +- .../_applied_filters.html.erb | 6 +- app/views/template_folders/show.html.erb | 10 ++ app/views/templates/_submission.html.erb | 6 +- app/views/templates/_title.html.erb | 10 +- app/views/templates/show.html.erb | 2 +- .../index.html.erb | 6 +- app/views/users/index.html.erb | 132 +++++++++--------- app/views/webhook_events/show.html.erb | 2 +- config/locales/i18n.yml | 2 +- tailwind.config.js | 3 - tailwind.dynamic.config.js | 3 - 40 files changed, 299 insertions(+), 192 deletions(-) diff --git a/.erb_lint.yml b/.erb_lint.yml index 1a76afa12..50576fd57 100644 --- a/.erb_lint.yml +++ b/.erb_lint.yml @@ -1,6 +1,11 @@ --- EnableDefaultLinters: true linters: + AllowedScriptType: + enabled: true + allowed_types: + - text/javascript + - application/json ErbSafety: enabled: false HardCodedString: diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0b6113816..d2371aa55 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -20,7 +20,7 @@ class UsersController < ApplicationController respond_to do |format| format.html do - @pagy, @users = pagy(@users) + @pagy, @users = pagy(@users, limit: turbo_native_app? ? 1_000 : Pagy::OPTIONS[:limit]) end if current_ability.can?(:manage, current_account) diff --git a/app/javascript/application.js b/app/javascript/application.js index e95852523..7dee5edf9 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -281,7 +281,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { } onSync = (e) => { - const { schema, documents, fields, submitters } = e.detail + const { schema, documents, fields, submitters, pendingFieldAttachmentUuids } = e.detail const selectedAreas = this.component.selectedAreasRef.value.map((area) => { const field = this.component.template.fields.find((f) => f.areas?.includes(area)) @@ -293,6 +293,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { this.component.selectedAreasRef.value = selectedAreas.map((item) => item && fields.find((f) => f.uuid === item.uuid)?.areas?.[item.index]).filter(Boolean) this.component.selectedSubmitter = submitters.find((s) => s.uuid === selectedSubmitterUuid) || submitters[0] + this.component.pendingFieldAttachmentUuids = pendingFieldAttachmentUuids } onScrollTo = (e) => { diff --git a/app/javascript/elements/dashboard_dropzone.js b/app/javascript/elements/dashboard_dropzone.js index 2ee1d8fc6..8aa75c0f6 100644 --- a/app/javascript/elements/dashboard_dropzone.js +++ b/app/javascript/elements/dashboard_dropzone.js @@ -26,17 +26,41 @@ export default targetable(class extends HTMLElement { window.addEventListener('dragleave', this.onWindowDragleave) - this.fileDropzone?.addEventListener('drop', this.onDropFile) - this.folderDropzone?.addEventListener('drop', this.onDropNewFolder) + this.addEventListener('drop', this.onDrop) + this.addEventListener('dragstart', this.onDragstart) + this.addEventListener('dragover', this.onDragover) + this.addEventListener('dragleave', this.onDragleave) + } - 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)) + dropTarget (e) { + return e.target.closest([ + '[data-target="dashboard-dropzone.fileDropzone"]', + '[data-target="dashboard-dropzone.folderDropzone"]', + '[data-targets="dashboard-dropzone.folderCards"]', + '[data-targets="dashboard-dropzone.templateCards"]' + ].join(', ')) + } - return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { - el?.addEventListener('dragover', this.onDragover) - el?.addEventListener('dragleave', this.onDragleave) - }) + onDrop = (e) => { + const el = this.dropTarget(e) + + if (!el) return + + if (el === this.fileDropzone) { + this.onDropFile(e) + } else if (el === this.folderDropzone) { + this.onDropNewFolder(e) + } else if (el.dataset.targets === 'dashboard-dropzone.folderCards') { + this.onDropFolder(e, el) + } else { + this.onDropTemplate(e, el) + } + } + + onDragstart = (e) => { + if (e.target.closest('[data-targets="dashboard-dropzone.templateCards"]')) { + this.onTemplateDragStart(e) + } } disconnectedCallback () { @@ -128,7 +152,7 @@ export default targetable(class extends HTMLElement { } } - onDropTemplate = (e) => { + onDropTemplate = (e, el) => { e.preventDefault() if (e.dataTransfer.files.length) { @@ -136,10 +160,10 @@ export default targetable(class extends HTMLElement { loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute') loading.innerHTML = loadingIconHtml - e.target.appendChild(loading) - e.target.classList.add('opacity-50') + el.appendChild(loading) + el.classList.add('opacity-50') - const id = e.target.href.split('/').pop() + const id = el.href.split('/').pop() this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`) } @@ -173,16 +197,20 @@ export default targetable(class extends HTMLElement { } } - onDragover (e) { - if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') { - this.style.backgroundColor = '#F7F3F0' + onDragover = (e) => { + const el = this.dropTarget(e) - if (this.classList.contains('before:border-base-300')) { - this.classList.remove('before:border-base-300') - this.classList.add('before:border-base-content/30') - } else if (this.classList.contains('border-base-300')) { - this.classList.remove('border-base-300') - this.classList.add('border-base-content/30') + if (!el) return + + if (e.dataTransfer?.types?.includes('Files') || el.dataset.targets !== 'dashboard-dropzone.templateCards') { + el.style.backgroundColor = '#F7F3F0' + + if (el.classList.contains('before:border-base-300')) { + el.classList.remove('before:border-base-300') + el.classList.add('before:border-base-content/30') + } else if (el.classList.contains('border-base-300')) { + el.classList.remove('border-base-300') + el.classList.add('border-base-content/30') } } } @@ -205,15 +233,19 @@ export default targetable(class extends HTMLElement { a.remove() } - onDragleave () { - this.style.backgroundColor = null + onDragleave = (e) => { + const el = this.dropTarget(e) - if (this.classList.contains('before:border-base-content/30')) { - this.classList.remove('before:border-base-content/30') - this.classList.add('before:border-base-300') - } else if (this.classList.contains('border-base-content/30')) { - this.classList.remove('border-base-content/30') - this.classList.add('border-base-300') + if (!el) return + + el.style.backgroundColor = null + + if (el.classList.contains('before:border-base-content/30')) { + el.classList.remove('before:border-base-content/30') + el.classList.add('before:border-base-300') + } else if (el.classList.contains('border-base-content/30')) { + el.classList.remove('border-base-content/30') + el.classList.add('border-base-300') } } diff --git a/app/javascript/elements/native_modal.js b/app/javascript/elements/native_modal.js index dc8f35d8b..e412c5af4 100644 --- a/app/javascript/elements/native_modal.js +++ b/app/javascript/elements/native_modal.js @@ -8,7 +8,7 @@ export default class extends HTMLElement { document.removeEventListener('turbo:submit-end', this.onSubmit) } - onSubmit = (e) => { + onSubmit = async (e) => { if (!e.detail.success) return if (e.detail.fetchResponse?.redirected && e.detail.fetchResponse.location.href !== window.location.href) return if (e.detail.formSubmission?.formElement?.method === 'get') return @@ -16,7 +16,19 @@ export default class extends HTMLElement { if (e.detail.formSubmission?.formElement?.dataset?.closeOnSubmit === 'false') return if (e.detail.formSubmission?.formElement?.closest('native-event[data-on="submit"]')) return - window.webkit?.messageHandlers?.modal?.postMessage({ action: 'submitted' }) + const flash = await this.flashFrom(e.detail.fetchResponse) + + window.webkit?.messageHandlers?.modal?.postMessage({ action: 'submitted', ...flash }) + } + + async flashFrom (fetchResponse) { + const html = await fetchResponse?.responseHTML + const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash') + + if (flash?.dataset.alert) return { alert: flash.dataset.alert } + if (flash?.dataset.notice) return { notice: flash.dataset.notice } + + return {} } onClick = () => { diff --git a/app/javascript/elements/open_modal_mobile.js b/app/javascript/elements/open_modal_mobile.js index a326af919..21ae835c6 100644 --- a/app/javascript/elements/open_modal_mobile.js +++ b/app/javascript/elements/open_modal_mobile.js @@ -1,13 +1,17 @@ export default class extends HTMLElement { connectedCallback () { + this.addEventListener('pointerdown', this.onPointerdown) + } + + onPointerdown = (event) => { if (this.dataset.disabled === 'true') return if (/Hotwire Native/.test(navigator.userAgent)) return if (!this.isMobile()) return if (window.innerWidth >= 768) return - this.querySelectorAll('a[data-turbo-frame]').forEach((link) => { - link.dataset.turboFrame = '_top' - }) + const link = event.target.closest('a[data-turbo-frame]') + + if (link) link.dataset.turboFrame = '_top' } isMobile () { diff --git a/app/javascript/elements/submit_form.js b/app/javascript/elements/submit_form.js index e936c0bdd..db83b5f35 100644 --- a/app/javascript/elements/submit_form.js +++ b/app/javascript/elements/submit_form.js @@ -1,5 +1,19 @@ export default class extends HTMLElement { connectedCallback () { + if (this.lastElementChild) { + this.init() + } else { + queueMicrotask(() => this.init()) + } + } + + disconnectedCallback () { + if (this.interval) { + clearInterval(this.interval) + } + } + + init () { const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form if (this.dataset.interval) { @@ -24,10 +38,4 @@ export default class extends HTMLElement { form.requestSubmit() } } - - disconnectedCallback () { - if (this.interval) { - clearInterval(this.interval) - } - } } diff --git a/app/javascript/native.js b/app/javascript/native.js index 87afa5e32..7de1f76a1 100644 --- a/app/javascript/native.js +++ b/app/javascript/native.js @@ -50,8 +50,11 @@ export function nativeIcon (name) { } const handlers = window.webkit?.messageHandlers +const isNativeLoaded = window.isNativeLoaded + +if (handlers?.native && window === window.top && !isNativeLoaded) { + window.isNativeLoaded = true -if (handlers?.native && window === window.top) { handlers.native.postMessage({ type: 'reset' }) const reportPathConfiguration = () => { @@ -150,7 +153,7 @@ if (handlers?.native && window === window.top) { }) } -if (handlers?.native && window.Turbo) { +if (handlers?.native && window.Turbo && !isNativeLoaded) { const staleSnapshots = new Set() let isRestoring = false diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 688183013..d537a6297 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -7,7 +7,7 @@ @drop="isDragFile = false" >
-
+
diff --git a/app/views/personalization_settings/show.html.erb b/app/views/personalization_settings/show.html.erb index f039d4503..90566562f 100644 --- a/app/views/personalization_settings/show.html.erb +++ b/app/views/personalization_settings/show.html.erb @@ -26,5 +26,5 @@ <%= render 'form_customization_settings' %> -
+
diff --git a/app/views/profile/index.html.erb b/app/views/profile/index.html.erb index eef076f67..5a5229d0f 100644 --- a/app/views/profile/index.html.erb +++ b/app/views/profile/index.html.erb @@ -107,5 +107,5 @@ <% end %> -
+
diff --git a/app/views/shared/_native_navbar.html.erb b/app/views/shared/_native_navbar.html.erb index fd97d7a5e..997f6492f 100644 --- a/app/views/shared/_native_navbar.html.erb +++ b/app/views/shared/_native_navbar.html.erb @@ -27,5 +27,12 @@ <%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %> + <% unless controller_name.in?(%w[templates_archived submissions_archived]) %> + "> + + + <%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %> + + <% end %> <% end %> <% end %> diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index d3f458396..ae8e9d29d 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -1,5 +1,6 @@ <% expanded = local_assigns[:expanded] %> -
+<% nav_link_data = turbo_native_app? && !expanded ? { turbo_action: 'replace' } : {} %> +
<% unless expanded || turbo_native_app? %>
@@ -26,60 +27,60 @@
  • <% end %>
  • - <%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • - <%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
  • - <%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% unless Docuseal.multitenant? %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
  • - <%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user && (!turbo_native_ios? || can?(:manage, :sms_verification)) %>
  • - <%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %> + <%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% end %> <% if can?(:read, AccountConfig) %>
  • - <%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
  • - <%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% if can?(:read, AccountConfig) %>
  • - <%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% if can?(:read, User) %>
  • - <%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %> + <%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> - <%= render 'shared/settings_nav_extra' %> + <%= render 'shared/settings_nav_extra', nav_link_data: %> <% if Docuseal.demo? || !Docuseal.multitenant? %> <% if can?(:read, AccessToken) %>
  • - <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> + <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% end %> <% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %> <% if can?(:read, WebhookUrl) %>
  • - <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> + <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% end %> @@ -108,15 +109,15 @@ <% end %> <% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user && (!turbo_native_ios? || can?(:manage, :saml_sso)) %>
  • - <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %> + <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> <% if !Docuseal.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %>
  • - <%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %> + <%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
  • <% end %> - <%= render 'shared/settings_nav_extra2' %> + <%= render 'shared/settings_nav_extra2', nav_link_data: %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %>
  • diff --git a/app/views/shared/_turbo_drawer.html.erb b/app/views/shared/_turbo_drawer.html.erb index c8928aeee..eaa9418c6 100644 --- a/app/views/shared/_turbo_drawer.html.erb +++ b/app/views/shared/_turbo_drawer.html.erb @@ -1,5 +1,5 @@ <% if turbo_native_app? && !turbo_frame_request? %> - <% content_for(:html_title, local_assigns[:title]) if local_assigns[:title] %> + <% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %> <%= yield %> diff --git a/app/views/shared/_turbo_modal.html.erb b/app/views/shared/_turbo_modal.html.erb index 8edb991d2..f7af42efa 100644 --- a/app/views/shared/_turbo_modal.html.erb +++ b/app/views/shared/_turbo_modal.html.erb @@ -1,6 +1,6 @@ <% if turbo_native_ios? || (turbo_native_app? && !turbo_frame_request?) %> <% content_for(:native_modal, 'true') %> - <% content_for(:html_title, local_assigns[:title]) if local_assigns[:title] %> + <% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %> <%= yield %> diff --git a/app/views/shared/_turbo_modal_large.html.erb b/app/views/shared/_turbo_modal_large.html.erb index 96e4530ca..1ae76d786 100644 --- a/app/views/shared/_turbo_modal_large.html.erb +++ b/app/views/shared/_turbo_modal_large.html.erb @@ -18,7 +18,7 @@ <% elsif turbo_native_app? %> - <% content_for(:html_title, local_assigns[:title]) if local_assigns[:title] %> + <% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %> <%= yield %> diff --git a/app/views/sms_settings/index.html.erb b/app/views/sms_settings/index.html.erb index b865f4e16..6e7caa26b 100644 --- a/app/views/sms_settings/index.html.erb +++ b/app/views/sms_settings/index.html.erb @@ -5,5 +5,5 @@

    SMS

    <%= render 'placeholder' %>
  • -
    +
    diff --git a/app/views/sso_settings/index.html.erb b/app/views/sso_settings/index.html.erb index fb13a57fd..5b911da70 100644 --- a/app/views/sso_settings/index.html.erb +++ b/app/views/sso_settings/index.html.erb @@ -5,5 +5,5 @@

    SAML SSO

    <%= render 'placeholder' %>
    -
    +
    diff --git a/app/views/storage_settings/index.html.erb b/app/views/storage_settings/index.html.erb index b459f1f7f..6d4e8f346 100644 --- a/app/views/storage_settings/index.html.erb +++ b/app/views/storage_settings/index.html.erb @@ -7,7 +7,7 @@ <% value = @encrypted_config.value || { 'service' => 'disk' } %> <% configs = value['configs'] || {} %> - <%= form_for @encrypted_config, url: settings_storage_index_path, method: :post, html: { autocomplete: 'off', class: 'w-full' } do |f| %> + <%= form_for @encrypted_config, url: settings_storage_index_path, method: :post, html: { autocomplete: 'off', class: 'w-full' }, data: { turbo_action: 'advance' } do |f| %> <% options = [%w[Disk disk], %w[AWS aws_s3], %w[GCP google], %w[Azure azure]] %>