diff --git a/.rubocop.yml b/.rubocop.yml index bda7960e..9be296e5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -84,7 +84,7 @@ RSpec/AnyInstance: Enabled: false Metrics/BlockNesting: - Max: 5 + Max: 6 Rails/I18nLocaleTexts: Enabled: false @@ -106,3 +106,10 @@ Rails/StrongParametersExpect: Rails/RedirectBackOrTo: Enabled: false + +Rails/UnknownEnv: + Environments: + - development + - test + - production + - local diff --git a/Dockerfile b/Dockerfile index f6412c78..b1341fcf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ ENV RAILS_ENV=production ENV BUNDLE_WITHOUT="development:test" ENV LD_PRELOAD=/lib/libgcompat.so.0 ENV OPENSSL_CONF=/etc/openssl_legacy.cnf +ENV VIPS_MAX_COORD=10000 WORKDIR /app diff --git a/Gemfile b/Gemfile index 61c84807..74ba1afb 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,6 @@ gem 'pretender' gem 'puma', require: false gem 'rack' gem 'rails' -gem 'rails_autolink' gem 'rails-i18n' gem 'rotp' gem 'rouge', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 962c2364..ae90555e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,7 +219,6 @@ GEM concurrent-ruby (~> 1.1) webrick (~> 1.7) websocket-driver (~> 0.7) - ffi (1.17.3) ffi (1.17.3-aarch64-linux-gnu) ffi (1.17.3-aarch64-linux-musl) ffi (1.17.3-arm64-darwin) @@ -338,22 +337,20 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.0-aarch64-linux-gnu) + nokogiri (1.19.1-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.0-aarch64-linux-musl) + nokogiri (1.19.1-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.0-arm64-darwin) + nokogiri (1.19.1-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.0-x86_64-linux-gnu) + nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.0-x86_64-linux-musl) + nokogiri (1.19.1-x86_64-linux-musl) racc (~> 1.4) numo-narray-alt (0.9.13) oj (3.16.13) bigdecimal (>= 3.0) ostruct (>= 0.2) - onnxruntime (0.10.1) - ffi onnxruntime (0.10.1-aarch64-linux) ffi onnxruntime (0.10.1-arm64-darwin) @@ -372,7 +369,6 @@ GEM parser (3.3.10.1) ast (~> 2.4.1) racc - pg (1.6.3) pg (1.6.3-aarch64-linux) pg (1.6.3-aarch64-linux-musl) pg (1.6.3-arm64-darwin) @@ -405,7 +401,7 @@ GEM puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.4) + rack (3.2.5) rack-proxy (0.7.7) rack rack-session (2.1.1) @@ -439,10 +435,6 @@ GEM rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - rails_autolink (1.1.8) - actionview (> 3.1) - activesupport (> 3.1) - railties (> 3.1) railties (8.1.2) actionpack (= 8.1.2) activesupport (= 8.1.2) @@ -662,7 +654,6 @@ DEPENDENCIES rack rails rails-i18n - rails_autolink rotp rouge rqrcode diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 22af24d9..77b4e615 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -172,7 +172,10 @@ module Api Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters) submitters.each do |submitter| - SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at? + if submitter.completed_at? + Submitters::SubmitValues.maybe_invite_via_field(submitter, request) + SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) + end end submissions diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index e56eb8b8..f28bf5e1 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -34,6 +34,7 @@ module Api render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:) end + # rubocop:disable Metrics/MethodLength def update if @submitter.completed_at? return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content @@ -60,7 +61,10 @@ module Api @submitter.submission.save! - SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at? + if @submitter.completed_at? + Submitters::SubmitValues.maybe_invite_via_field(@submitter, request) + SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) + end end if @submitter.completed_at? @@ -78,6 +82,7 @@ module Api render json: { error: e.message }, status: :unprocessable_content end + # rubocop:enable Metrics/MethodLength def submitter_params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c8211b7f..c3f1dd42 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -107,7 +107,8 @@ module Api :external_id, :shared_link, { - submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]], + submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid + optional_invite_by_uuid linked_to_uuid email order]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 50823ca7..592c006d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -42,10 +42,6 @@ class ApplicationController < ActionController::Base end def default_url_options - if request.domain == 'docuseal.com' - return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' } - end - Docuseal.default_url_options end diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index eb216bc5..39dee165 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -65,7 +65,7 @@ class SubmissionsDownloadController < ApplicationController key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value Submitters.select_attachments_for_download(submitter).map do |attachment| - ActiveStorage::Blob.proxy_url( + ActiveStorage::Blob.proxy_path( attachment.blob, expires_at: FILES_TTL.from_now.to_i, filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) @@ -83,7 +83,7 @@ class SubmissionsDownloadController < ApplicationController filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value - ActiveStorage::Blob.proxy_url( + ActiveStorage::Blob.proxy_path( attachment.blob, expires_at: FILES_TTL.from_now.to_i, filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index 3ebdc5e2..af9cbeb4 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -33,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController end urls = attachments.map do |attachment| - ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) + ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i) end render json: urls diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index bcc848ae..413e2b9a 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -19,7 +19,9 @@ class SubmitFormInviteController < ApplicationController next unless attrs next if attrs[:email].blank? - submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) + email = Submissions.normalize_email(attrs[:email]) + + submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id) SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) end diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index b29a18f6..51fc4111 100644 --- a/app/controllers/template_documents_controller.rb +++ b/app/controllers/template_documents_controller.rb @@ -6,7 +6,7 @@ class TemplateDocumentsController < ApplicationController FILES_TTL = 5.minutes def index - render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) } + render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) } end def create diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 39b45044..0d6db6f7 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -97,7 +97,8 @@ class TemplatesController < ApplicationController :name, { schema: [[:attachment_uuid, :google_drive_file_id, :name, { conditions: [%i[field_uuid value action operation]] }]], - submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]], + submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid + invite_by_uuid optional_invite_by_uuid email order]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb index 17a4bbb8..64fa58b2 100644 --- a/app/controllers/templates_recipients_controller.rb +++ b/app/controllers/templates_recipients_controller.rb @@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController def submitters_params permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid - invite_by_uuid linked_to_uuid email option order]] } + invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] } params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s| next if s[:uuid].blank? @@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController s[:order] = s[:order].to_i if s[:order].present? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank? + s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank? normalize_option_value(s) end @@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController attrs.delete(:email) attrs.delete(:linked_to_uuid) attrs.delete(:invite_by_uuid) + attrs.delete(:invite_via_field_uuid) attrs.delete(:optional_invite_by_uuid) when /\Alinked_to_(.*)\z/ attrs[:linked_to_uuid] = ::Regexp.last_match(-1) diff --git a/app/javascript/application.js b/app/javascript/application.js index 49cd9516..8889609f 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone' import RequiredCheckboxGroup from './elements/required_checkbox_group' import PageContainer from './elements/page_container' import EmailEditor from './elements/email_editor' +import MarkdownEditor from './elements/markdown_editor' import MountOnClick from './elements/mount_on_click' import RemoveOnEvent from './elements/remove_on_event' import ScrollTo from './elements/scroll_to' @@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('page-container', PageContainer) safeRegisterElement('email-editor', EmailEditor) +safeRegisterElement('markdown-editor', MarkdownEditor) safeRegisterElement('mount-on-click', MountOnClick) safeRegisterElement('remove-on-event', RemoveOnEvent) safeRegisterElement('scroll-to', ScrollTo) diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 47eeb4f6..fd02cecf 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled { .font-courier { font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco; } + +markdown-editor [contenteditable] p { + margin-bottom: 18px; +} diff --git a/app/javascript/elements/markdown_editor.js b/app/javascript/elements/markdown_editor.js new file mode 100644 index 00000000..17431277 --- /dev/null +++ b/app/javascript/elements/markdown_editor.js @@ -0,0 +1,385 @@ +import { target, targetable } from '@github/catalyst/lib/targetable' +import { actionable } from '@github/catalyst/lib/actionable' + +function loadTiptap () { + return Promise.all([ + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view') + ]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({ + Editor: core.Editor, + Extension: core.Extension, + Bold: bold.default || bold, + Italic: italic.default || italic, + Paragraph: paragraph.default || paragraph, + Text: text.default || text, + HardBreak: hardBreak.default || hardBreak, + Document: document.default || document, + Link: link.default || link, + Underline: underline.default || underline, + UndoRedo: extensions.UndoRedo, + Markdown: markdown.Markdown, + Plugin: pmState.Plugin, + Decoration: pmView.Decoration, + DecorationSet: pmView.DecorationSet + })) +} + +class LinkTooltip { + constructor (container, editor) { + this.container = container + this.editor = editor + + const template = document.createElement('template') + + template.innerHTML = container.dataset.linkTooltipHtml + + this.tooltip = template.content.firstElementChild + + this.input = this.tooltip.querySelector('input') + this.saveButton = this.tooltip.querySelector('[data-role="link-save"]') + this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]') + + container.style.position = 'relative' + container.appendChild(this.tooltip) + } + + isVisible () { + return !this.tooltip.classList.contains('hidden') + } + + normalizeUrl (url) { + if (!url) return url + if (/^{/i.test(url)) return url + if (/^https?:\/\//i.test(url)) return url + if (/^mailto:/i.test(url)) return url + + return `https://${url}` + } + + show (url, pos, { focus = false } = {}) { + this.input.value = url || '' + this.removeButton.classList.toggle('hidden', !url) + + this.tooltip.classList.remove('hidden') + + const coords = this.editor.view.coordsAtPos(pos) + const containerRect = this.container.getBoundingClientRect() + + this.tooltip.style.left = `${coords.left - containerRect.left}px` + this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px` + + if (focus) this.input.focus() + + this.saveHandler = () => { + const inputUrl = this.input.value.trim() + + if (inputUrl) { + this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run() + } + + this.hide() + } + + this.removeHandler = () => { + this.editor.chain().focus().extendMarkRange('link').unsetLink().run() + + this.hide() + } + + this.keyHandler = (e) => { + if (e.key === 'Enter') { + e.preventDefault() + this.saveHandler() + } else if (e.key === 'Escape') { + e.preventDefault() + this.hide() + } + } + + this.saveButton.addEventListener('click', this.saveHandler, { once: true }) + this.removeButton.addEventListener('click', this.removeHandler, { once: true }) + this.input.addEventListener('keydown', this.keyHandler) + } + + hide () { + if (this.saveHandler) { + this.saveButton.removeEventListener('click', this.saveHandler) + this.saveHandler = null + } + + if (this.removeHandler) { + this.removeButton.removeEventListener('click', this.removeHandler) + this.removeHandler = null + } + + if (this.keyHandler) { + this.input.removeEventListener('keydown', this.keyHandler) + this.keyHandler = null + } + + this.tooltip.classList.add('hidden') + this.currentMark = null + } +} + +export default actionable(targetable(class extends HTMLElement { + static [target.static] = [ + 'textarea', + 'editorElement', + 'boldButton', + 'italicButton', + 'underlineButton', + 'linkButton' + ] + + async connectedCallback () { + if (!this.textarea || !this.editorElement) return + + this.textarea.style.display = 'none' + this.adjustShortcutsForPlatform() + + const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap() + + const buildDecorations = (doc) => { + const decorations = [] + const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g + + doc.descendants((node, pos) => { + if (!node.isText) return + + let match + + while ((match = regex.exec(node.text)) !== null) { + decorations.push( + Decoration.inline(pos + match.index, pos + match.index + match[0].length, { + class: 'bg-amber-100 py-0.5 px-1 rounded' + }) + ) + } + }) + + return DecorationSet.create(doc, decorations) + } + + const VariableHighlight = Extension.create({ + name: 'variableHighlight', + addProseMirrorPlugins () { + return [new Plugin({ + state: { + init (_, { doc }) { + return buildDecorations(doc) + }, + apply (tr, oldSet) { + return tr.docChanged ? buildDecorations(tr.doc) : oldSet + } + }, + props: { + decorations (state) { + return this.getState(state) + } + } + })] + } + }) + + this.editor = new Editor({ + element: this.editorElement, + extensions: [ + Markdown, + Document, + Paragraph, + Text, + Bold, + Italic, + HardBreak.extend({ + addKeyboardShortcuts () { + return { + Enter: () => this.editor.commands.setHardBreak() + } + } + }), + UndoRedo, + Link.extend({ + inclusive: true, + addKeyboardShortcuts: () => ({ + 'Mod-k': () => { + this.toggleLink() + + return true + } + }) + }).configure({ + openOnClick: false, + HTMLAttributes: { + class: 'link', + 'data-turbo': 'false', + style: 'color: #2563eb; text-decoration: underline; cursor: text;' + } + }), + Underline, + VariableHighlight + ], + content: (this.textarea.value || '').trim().replace(/ *\n/g, '
'), + contentType: 'markdown', + editorProps: { + attributes: { + style: 'min-height: 220px', + dir: 'auto', + class: 'p-3 outline-none focus:outline-none' + } + }, + onUpdate: ({ editor }) => { + this.textarea.value = editor.getMarkdown() + this.textarea.dispatchEvent(new Event('input', { bubbles: true })) + }, + onSelectionUpdate: ({ editor }) => { + this.updateToolbarState() + this.handleLinkTooltip(editor) + }, + onBlur: () => { + setTimeout(() => { + if (!this.linkTooltip.tooltip.contains(document.activeElement)) { + this.linkTooltip.hide() + } + }, 0) + } + }) + + this.linkTooltip = new LinkTooltip(this, this.editor) + } + + adjustShortcutsForPlatform () { + if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) { + this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => { + const tip = tooltip.getAttribute('data-tip') + + if (tip && tip.includes('Ctrl')) { + tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘')) + } + }) + } + } + + bold (e) { + e.preventDefault() + + this.editor.chain().focus().toggleBold().run() + this.updateToolbarState() + } + + italic (e) { + e.preventDefault() + + this.editor.chain().focus().toggleItalic().run() + this.updateToolbarState() + } + + underline (e) { + e.preventDefault() + + this.editor.chain().focus().toggleUnderline().run() + this.updateToolbarState() + } + + linkSelection (e) { + e.preventDefault() + + this.toggleLink() + this.updateToolbarState() + } + + undo (e) { + e.preventDefault() + + this.editor.chain().focus().undo().run() + this.updateToolbarState() + } + + redo (e) { + e.preventDefault() + + this.editor.chain().focus().redo().run() + this.updateToolbarState() + } + + updateToolbarState () { + this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold')) + this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic')) + this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline')) + this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link')) + } + + handleLinkTooltip (editor) { + const { from } = editor.state.selection + const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link') + + if (!mark) { + if (this.linkTooltip.isVisible()) this.linkTooltip.hide() + + return + } + + if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return + + let linkStart = from + const start = editor.state.doc.resolve(from).start() + + for (let i = from - 1; i >= start; i--) { + if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) { + linkStart = i + } else { + break + } + } + + this.linkTooltip.hide() + this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart) + this.linkTooltip.currentMark = mark + } + + toggleLink () { + if (this.editor.isActive('link')) { + this.linkTooltip.hide() + this.editor.chain().focus().extendMarkRange('link').unsetLink().run() + this.updateToolbarState() + } else { + const { from } = this.editor.state.selection + + this.linkTooltip.hide() + this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true }) + } + } + + insertVariable (e) { + const variable = e.target.closest('[data-variable]')?.dataset.variable + + if (variable) { + const { from, to } = this.editor.state.selection + + if (variable.includes('link') && from !== to) { + this.editor.chain().focus().setLink({ href: `{${variable}}` }).run() + } else { + this.editor.chain().focus().insertContent(`{${variable}}`).run() + } + } + } + + disconnectedCallback () { + this.linkTooltip.hide() + + if (this.editor) { + this.editor.destroy() + } + } +})) diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js index e9ee3075..5ff6b7c3 100644 --- a/app/javascript/elements/toggle_attribute.js +++ b/app/javascript/elements/toggle_attribute.js @@ -1,12 +1,18 @@ export default class extends HTMLElement { connectedCallback () { this.input.addEventListener('change', (event) => { + if (!this.target) return + + const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value + const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true + if (this.dataset.attribute) { - this.target[this.dataset.attribute] = event.target.checked + this.target[this.dataset.attribute] = value === dataValue } if (this.dataset.className) { - this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value) + this.target.classList.toggle(this.dataset.className, value !== dataValue) + if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') { this.target.disabled = event.target.value !== this.dataset.value } diff --git a/app/javascript/elements/toggle_classes.js b/app/javascript/elements/toggle_classes.js index ab96f293..332ac84f 100644 --- a/app/javascript/elements/toggle_classes.js +++ b/app/javascript/elements/toggle_classes.js @@ -1,10 +1,18 @@ export default class extends HTMLElement { connectedCallback () { - const button = this.querySelector('a, button') + const button = this.querySelector('a, button, label') + + const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button button.addEventListener('click', () => { this.dataset.classes.split(' ').forEach((cls) => { - button.classList.toggle(cls) + if (this.dataset.action === 'remove') { + target.classList.remove(cls) + } else if (this.dataset.action === 'add') { + target.classList.add(cls) + } else { + target.classList.toggle(cls) + } }) }) } diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 70567992..b9da958d 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -1,7 +1,7 @@ const en = { kba: 'KBA', please_upload_an_image_file: 'Please upload an image file', - must_be_characters_length: 'Must be {number} characters length', + must_be_characters_length: 'Must be {number} characters long', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.', verify_id: 'Verify ID', identity_verification: 'Identity verification', @@ -97,6 +97,7 @@ const en = { upload: 'Upload', files: 'Files', signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.', + browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.', wait_countdown_seconds: 'Wait {countdown} seconds' } @@ -199,6 +200,7 @@ const es = { upload: 'Subir', files: 'Archivos', signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.', + browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.', wait_countdown_seconds: 'Espera {countdown} segundos' } @@ -301,6 +303,7 @@ const it = { upload: 'Carica', files: 'File', signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.', + browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.', wait_countdown_seconds: 'Attendi {countdown} secondi' } @@ -403,6 +406,7 @@ const de = { upload: 'Hochladen', files: 'Dateien', signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.', + browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.', wait_countdown_seconds: 'Bitte {countdown} Sekunden warten' } @@ -505,6 +509,7 @@ const fr = { upload: 'Téléverser', files: 'Fichiers', signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.', + browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.', wait_countdown_seconds: 'Veuillez patienter {countdown} secondes' } @@ -607,6 +612,7 @@ const pl = { upload: 'Przesyłanie', files: 'Pliki', signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.', + browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.', wait_countdown_seconds: 'Poczekaj {countdown} sekund' } @@ -709,6 +715,7 @@ const uk = { upload: 'Завантажити', files: 'Файли', signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.', + browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.', wait_countdown_seconds: 'Зачекайте {countdown} секунд' } @@ -811,6 +818,7 @@ const cs = { upload: 'Nahrát', files: 'Soubory', signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.', + browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.', wait_countdown_seconds: 'Počkejte {countdown} sekund' } @@ -913,6 +921,7 @@ const pt = { upload: 'Carregar', files: 'Arquivos', signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.', + browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.', wait_countdown_seconds: 'Aguarde {countdown} segundos' } @@ -1015,6 +1024,7 @@ const he = { upload: 'העלאה', files: 'קבצים', signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.', + browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.', wait_countdown_seconds: 'המתן {countdown} שניות' } @@ -1117,6 +1127,7 @@ const nl = { upload: 'Uploaden', files: 'Bestanden', signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.', + browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.', wait_countdown_seconds: 'Wacht {countdown} seconden' } @@ -1219,6 +1230,7 @@ const ar = { upload: 'تحميل', files: 'الملفات', signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.', + browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.', wait_countdown_seconds: 'انتظر {countdown} ثانية' } @@ -1321,6 +1333,7 @@ const ko = { upload: '업로드', files: '파일', signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.', + browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.', wait_countdown_seconds: '{countdown}초 기다리세요' } @@ -1423,6 +1436,7 @@ const ja = { upload: 'アップロード', files: 'ファイル', signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。', + browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。', wait_countdown_seconds: '{countdown} 秒お待ちください' } diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 8e1cc1b5..814b9c12 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -150,6 +150,7 @@