Compare commits

..

34 Commits

Author SHA1 Message Date
Alex Turchyn 13fa87c449
Merge from docusealco/wip
4 weeks ago
Pete Matsyburka 246ccfb049 fix highlight
4 weeks ago
Pete Matsyburka cbffb1e5b8 cloudflare r2 fix
4 weeks ago
Pete Matsyburka c4b007411d radio select in formula
4 weeks ago
Pete Matsyburka 04264fe937 fix reason field
4 weeks ago
Pete Matsyburka 0c37ad12b7 add variables key
4 weeks ago
Pete Matsyburka 8c5298ed2e refactor link tooltip
4 weeks ago
Pete Matsyburka 262118e047 update deps
4 weeks ago
Pete Matsyburka e5b63ea2b0 fix multi party email form
4 weeks ago
Pete Matsyburka 746757d402 rtl in editor
4 weeks ago
Pete Matsyburka e6e640328b use br for email content
4 weeks ago
Pete Matsyburka 845782a69c link input width
4 weeks ago
Pete Matsyburka fc6baa1b3b adjust autolink
4 weeks ago
Pete Matsyburka 47822ecc15 remove autolink
4 weeks ago
Pete Matsyburka 3d0c7f1118 markdown in preferences
4 weeks ago
Pete Matsyburka e9c8e4d325 adjust editor
4 weeks ago
Alex Turchyn 39cc82ce0c add markdown editor
4 weeks ago
Pete Matsyburka 451f83421a fix cell render
4 weeks ago
Pete Matsyburka 5073f2eefb adjsut sign yourself
4 weeks ago
Pete Matsyburka 231fff5508 canvas blocked message
4 weeks ago
Pete Matsyburka f1d146eca3 fix typos
4 weeks ago
Pete Matsyburka b6635fcc4f change user only when root
4 weeks ago
Pete Matsyburka fb5e13ee4c adjust recipients form
4 weeks ago
Pete Matsyburka ba84741a64 invite party via field
4 weeks ago
Pete Matsyburka 65c275ac17 rename
4 weeks ago
Pete Matsyburka 825322d489 adjust for custom domain
4 weeks ago
Pete Matsyburka 739e2abdf8 vips size limit
1 month ago
Pete Matsyburka 1d2394e31e size limit
1 month ago
Pete Matsyburka 1b41af798d update
1 month ago
Pete Matsyburka 118f4a231b detailed time format
1 month ago
Pete Matsyburka e48652f425 fix rubocop
1 month ago
Pete Matsyburka 848f01edf8 add spec
1 month ago
Pete Matsyburka 43fbc42770 fix reason field
1 month ago
Pete Matsyburka 2c736a0eed validate readonly field
1 month ago

@ -84,7 +84,7 @@ RSpec/AnyInstance:
Enabled: false Enabled: false
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 5 Max: 6
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
@ -106,3 +106,10 @@ Rails/StrongParametersExpect:
Rails/RedirectBackOrTo: Rails/RedirectBackOrTo:
Enabled: false Enabled: false
Rails/UnknownEnv:
Environments:
- development
- test
- production
- local

@ -48,6 +48,7 @@ ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
ENV LD_PRELOAD=/lib/libgcompat.so.0 ENV LD_PRELOAD=/lib/libgcompat.so.0
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
ENV VIPS_MAX_COORD=10000
WORKDIR /app WORKDIR /app

@ -33,7 +33,6 @@ gem 'pretender'
gem 'puma', require: false gem 'puma', require: false
gem 'rack' gem 'rack'
gem 'rails' gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n' gem 'rails-i18n'
gem 'rotp' gem 'rotp'
gem 'rouge', require: false gem 'rouge', require: false

@ -219,7 +219,6 @@ GEM
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (~> 0.7) websocket-driver (~> 0.7)
ffi (1.17.3)
ffi (1.17.3-aarch64-linux-gnu) ffi (1.17.3-aarch64-linux-gnu)
ffi (1.17.3-aarch64-linux-musl) ffi (1.17.3-aarch64-linux-musl)
ffi (1.17.3-arm64-darwin) ffi (1.17.3-arm64-darwin)
@ -338,22 +337,20 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.0-aarch64-linux-gnu) nokogiri (1.19.1-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.0-aarch64-linux-musl) nokogiri (1.19.1-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.0-arm64-darwin) nokogiri (1.19.1-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.0-x86_64-linux-gnu) nokogiri (1.19.1-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.0-x86_64-linux-musl) nokogiri (1.19.1-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
numo-narray-alt (0.9.13) numo-narray-alt (0.9.13)
oj (3.16.13) oj (3.16.13)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
onnxruntime (0.10.1)
ffi
onnxruntime (0.10.1-aarch64-linux) onnxruntime (0.10.1-aarch64-linux)
ffi ffi
onnxruntime (0.10.1-arm64-darwin) onnxruntime (0.10.1-arm64-darwin)
@ -372,7 +369,6 @@ GEM
parser (3.3.10.1) parser (3.3.10.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.6.3)
pg (1.6.3-aarch64-linux) pg (1.6.3-aarch64-linux)
pg (1.6.3-aarch64-linux-musl) pg (1.6.3-aarch64-linux-musl)
pg (1.6.3-arm64-darwin) pg (1.6.3-arm64-darwin)
@ -405,7 +401,7 @@ GEM
puma (7.2.0) puma (7.2.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.4) rack (3.2.5)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.1.1) rack-session (2.1.1)
@ -439,10 +435,6 @@ GEM
rails-i18n (8.1.0) rails-i18n (8.1.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
rails_autolink (1.1.8)
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (8.1.2) railties (8.1.2)
actionpack (= 8.1.2) actionpack (= 8.1.2)
activesupport (= 8.1.2) activesupport (= 8.1.2)
@ -662,7 +654,6 @@ DEPENDENCIES
rack rack
rails rails
rails-i18n rails-i18n
rails_autolink
rotp rotp
rouge rouge
rqrcode rqrcode

@ -172,7 +172,10 @@ module Api
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters) Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
submitters.each do |submitter| 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 end
submissions submissions

@ -34,6 +34,7 @@ module Api
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:) render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
end end
# rubocop:disable Metrics/MethodLength
def update def update
if @submitter.completed_at? if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
@ -60,7 +61,10 @@ module Api
@submitter.submission.save! @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 end
if @submitter.completed_at? if @submitter.completed_at?
@ -78,6 +82,7 @@ module Api
render json: { error: e.message }, status: :unprocessable_content render json: { error: e.message }, status: :unprocessable_content
end end
# rubocop:enable Metrics/MethodLength
def submitter_params def submitter_params
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params

@ -107,7 +107,8 @@ module Api
:external_id, :external_id,
:shared_link, :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, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,

@ -42,10 +42,6 @@ class ApplicationController < ActionController::Base
end end
def default_url_options 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 Docuseal.default_url_options
end end

@ -65,7 +65,7 @@ class SubmissionsDownloadController < ApplicationController
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_path(
attachment.blob, attachment.blob,
expires_at: FILES_TTL.from_now.to_i, expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) 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, filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_path(
attachment.blob, attachment.blob,
expires_at: FILES_TTL.from_now.to_i, expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)

@ -33,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController
end end
urls = attachments.map do |attachment| 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 end
render json: urls render json: urls

@ -19,7 +19,9 @@ class SubmitFormInviteController < ApplicationController
next unless attrs next unless attrs
next if attrs[:email].blank? 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 }) SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
end end

@ -6,7 +6,7 @@ class TemplateDocumentsController < ApplicationController
FILES_TTL = 5.minutes FILES_TTL = 5.minutes
def index 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 end
def create def create

@ -97,7 +97,8 @@ class TemplatesController < ApplicationController
:name, :name,
{ schema: [[:attachment_uuid, :google_drive_file_id, :name, { schema: [[:attachment_uuid, :google_drive_file_id, :name,
{ conditions: [%i[field_uuid value action operation]] }]], { 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, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,

@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
def submitters_params def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid 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| params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank? next if s[:uuid].blank?
@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
s[:order] = s[:order].to_i if s[:order].present? s[:order] = s[:order].to_i if s[:order].present?
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? 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(: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) normalize_option_value(s)
end end
@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
attrs.delete(:email) attrs.delete(:email)
attrs.delete(:linked_to_uuid) attrs.delete(:linked_to_uuid)
attrs.delete(:invite_by_uuid) attrs.delete(:invite_by_uuid)
attrs.delete(:invite_via_field_uuid)
attrs.delete(:optional_invite_by_uuid) attrs.delete(:optional_invite_by_uuid)
when /\Alinked_to_(.*)\z/ when /\Alinked_to_(.*)\z/
attrs[:linked_to_uuid] = ::Regexp.last_match(-1) attrs[:linked_to_uuid] = ::Regexp.last_match(-1)

@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group' import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container' import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor' import EmailEditor from './elements/email_editor'
import MarkdownEditor from './elements/markdown_editor'
import MountOnClick from './elements/mount_on_click' import MountOnClick from './elements/mount_on_click'
import RemoveOnEvent from './elements/remove_on_event' import RemoveOnEvent from './elements/remove_on_event'
import ScrollTo from './elements/scroll_to' import ScrollTo from './elements/scroll_to'
@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer) safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('markdown-editor', MarkdownEditor)
safeRegisterElement('mount-on-click', MountOnClick) safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('remove-on-event', RemoveOnEvent) safeRegisterElement('remove-on-event', RemoveOnEvent)
safeRegisterElement('scroll-to', ScrollTo) safeRegisterElement('scroll-to', ScrollTo)

@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
.font-courier { .font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco; font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
} }
markdown-editor [contenteditable] p {
margin-bottom: 18px;
}

@ -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, '<br>'),
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()
}
}
}))

@ -1,12 +1,18 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.input.addEventListener('change', (event) => { 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) { if (this.dataset.attribute) {
this.target[this.dataset.attribute] = event.target.checked this.target[this.dataset.attribute] = value === dataValue
} }
if (this.dataset.className) { 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') { if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
this.target.disabled = event.target.value !== this.dataset.value this.target.disabled = event.target.value !== this.dataset.value
} }

@ -1,10 +1,18 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { 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', () => { button.addEventListener('click', () => {
this.dataset.classes.split(' ').forEach((cls) => { 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)
}
}) })
}) })
} }

@ -1,7 +1,7 @@
const en = { const en = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Please upload an image file', 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.', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID', verify_id: 'Verify ID',
identity_verification: 'Identity verification', identity_verification: 'Identity verification',
@ -97,6 +97,7 @@ const en = {
upload: 'Upload', upload: 'Upload',
files: 'Files', files: 'Files',
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.', 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' wait_countdown_seconds: 'Wait {countdown} seconds'
} }
@ -199,6 +200,7 @@ const es = {
upload: 'Subir', upload: 'Subir',
files: 'Archivos', files: 'Archivos',
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.', 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' wait_countdown_seconds: 'Espera {countdown} segundos'
} }
@ -301,6 +303,7 @@ const it = {
upload: 'Carica', upload: 'Carica',
files: 'File', files: 'File',
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.', 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' wait_countdown_seconds: 'Attendi {countdown} secondi'
} }
@ -403,6 +406,7 @@ const de = {
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien', files: 'Dateien',
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.', 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' wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
} }
@ -505,6 +509,7 @@ const fr = {
upload: 'Téléverser', upload: 'Téléverser',
files: 'Fichiers', files: 'Fichiers',
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.', 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' wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
} }
@ -607,6 +612,7 @@ const pl = {
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki',
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.', 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' wait_countdown_seconds: 'Poczekaj {countdown} sekund'
} }
@ -709,6 +715,7 @@ const uk = {
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: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд' wait_countdown_seconds: 'Зачекайте {countdown} секунд'
} }
@ -811,6 +818,7 @@ const cs = {
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory', files: 'Soubory',
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.', 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' wait_countdown_seconds: 'Počkejte {countdown} sekund'
} }
@ -913,6 +921,7 @@ const pt = {
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos', files: 'Arquivos',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.', 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' wait_countdown_seconds: 'Aguarde {countdown} segundos'
} }
@ -1015,6 +1024,7 @@ const he = {
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: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
wait_countdown_seconds: 'המתן {countdown} שניות' wait_countdown_seconds: 'המתן {countdown} שניות'
} }
@ -1117,6 +1127,7 @@ const nl = {
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden', files: 'Bestanden',
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.', 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' wait_countdown_seconds: 'Wacht {countdown} seconden'
} }
@ -1219,6 +1230,7 @@ const ar = {
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: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
wait_countdown_seconds: 'انتظر {countdown} ثانية' wait_countdown_seconds: 'انتظر {countdown} ثانية'
} }
@ -1321,6 +1333,7 @@ const ko = {
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: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요' wait_countdown_seconds: '{countdown}초 기다리세요'
} }
@ -1423,6 +1436,7 @@ const ja = {
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: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
wait_countdown_seconds: '{countdown} 秒お待ちください' wait_countdown_seconds: '{countdown} 秒お待ちください'
} }

@ -150,6 +150,7 @@
<script> <script>
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isCanvasBlocked } from './validate_signature'
import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
@ -419,7 +420,15 @@ export default {
} }
}).catch((error) => { }).catch((error) => {
if (this.field.required === true) { if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (isCanvasBlocked()) {
alert(this.t('browser_privacy_settings_block_canvas'))
if (window.Rollbar) {
window.Rollbar.info('Canvas blocked')
}
} else {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
}
return reject(error) return reject(error)
} else { } else {

@ -309,7 +309,7 @@
<script> <script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature' import { isValidSignatureCanvas, isCanvasBlocked } from './validate_signature'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import FileDropzone from './dropzone' import FileDropzone from './dropzone'
@ -790,7 +790,15 @@ export default {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) { if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
if (this.field.required === true || this.pad.toData().length > 0) { if (this.field.required === true || this.pad.toData().length > 0) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (isCanvasBlocked()) {
alert(this.t('browser_privacy_settings_block_canvas'))
if (window.Rollbar) {
window.Rollbar.info('Canvas blocked')
}
} else {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
}
return Promise.reject(new Error('Image too small or simple')) return Promise.reject(new Error('Image too small or simple'))
} else { } else {
@ -846,7 +854,15 @@ export default {
} }
}).catch((error) => { }).catch((error) => {
if (this.field.required === true) { if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (isCanvasBlocked()) {
alert(this.t('browser_privacy_settings_block_canvas'))
if (window.Rollbar) {
window.Rollbar.info('Canvas blocked')
}
} else {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
}
return reject(error) return reject(error)
} else { } else {

@ -35,4 +35,24 @@ function isValidSignatureCanvas (data) {
return validStrokes.length > 0 return validStrokes.length > 0
} }
export { isValidSignatureCanvas } function isCanvasBlocked () {
try {
const testCanvas = document.createElement('canvas')
testCanvas.width = 2
testCanvas.height = 2
const ctx = testCanvas.getContext('2d')
ctx.fillStyle = 'rgb(255, 0, 0)'
ctx.fillRect(0, 0, 2, 2)
const pixel = ctx.getImageData(0, 0, 1, 1).data
return pixel[0] !== 255 || pixel[1] !== 0 || pixel[2] !== 0 || pixel[3] !== 255
} catch (e) {
return true
}
}
export { isValidSignatureCanvas, isCanvasBlocked }

@ -81,7 +81,7 @@
/> />
<template v-else> <template v-else>
<form <form
v-if="withSignYourselfButton && template.submitters.length < 2" v-if="withSignYourselfButton && undefinedSubmitters.length < 2"
target="_blank" target="_blank"
data-turbo="false" data-turbo="false"
class="inline" class="inline"
@ -938,6 +938,16 @@ export default {
language () { language () {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
}, },
undefinedSubmitters () {
return this.template.submitters.filter((submitter) => {
return !submitter.invite_by_uuid &&
!submitter.optional_invite_by_uuid &&
!submitter.invite_via_field_uuid &&
!submitter.linked_to_uuid &&
!submitter.is_requester &&
!submitter.email
})
},
withPrefillable () { withPrefillable () {
if (this.template.fields) { if (this.template.fields) {
return this.template.fields.some((f) => f.prefillable) return this.template.fields.some((f) => f.prefillable)

@ -161,7 +161,7 @@ export default {
computed: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) { if (f !== this.field && this.isNumberField(f) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
acc.push(f) acc.push(f)
} }
@ -176,9 +176,12 @@ export default {
this.formula = this.humanizeFormula(this.field.preferences.formula || '') this.formula = this.humanizeFormula(this.field.preferences.formula || '')
}, },
methods: { methods: {
isNumberField (field) {
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
},
humanizeFormula (text) { humanizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, uuid) => { return text.replace(/{{(.*?)}}/g, (match, uuid) => {
const foundField = this.fields.find((f) => f.uuid === uuid) const foundField = this.template.fields.find((f) => f.uuid === uuid)
if (foundField) { if (foundField) {
return `{{${foundField.name || this.buildDefaultName(foundField)}}}` return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
@ -189,7 +192,7 @@ export default {
}, },
normalizeFormula (text) { normalizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, name) => { return text.replace(/{{(.*?)}}/g, (match, name) => {
const foundField = this.fields.find((f) => { const foundField = this.template.fields.find((f) => {
return (f.name || this.buildDefaultName(f)).trim() === name.trim() return (f.name || this.buildDefaultName(f)).trim() === name.trim()
}) })

@ -193,7 +193,7 @@ const en = {
learn_more: 'Learn more', learn_more: 'Learn more',
and: 'and', and: 'and',
or: 'or', or: 'or',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
start_tour: 'Start Tour', start_tour: 'Start Tour',
or_add_from: 'Or add from', or_add_from: 'Or add from',
sync: 'Sync', sync: 'Sync',

@ -14,12 +14,7 @@ class SubmitterMailer < ApplicationMailer
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end end
template_submitters_index = template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
if @email_message.blank?
build_submitter_preferences_index(@submitter)
else
{}
end
@body = @email_message&.body.presence || @body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence || template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@ -36,6 +31,8 @@ class SubmitterMailer < ApplicationMailer
reply_to = build_submitter_reply_to(@submitter) reply_to = build_submitter_reply_to(@submitter)
maybe_set_custom_domain(@submitter)
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
subject = build_invite_subject(@subject, @email_config, submitter) subject = build_invite_subject(@subject, @email_config, submitter)
@ -133,6 +130,8 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_documents_copy', @submitter) assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true) reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
maybe_set_custom_domain(@submitter)
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
subject = subject =
@subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy) @subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy)
@ -262,4 +261,10 @@ class SubmitterMailer < ApplicationMailer
def fetch_config_email_body(email_config, _submitter = nil) def fetch_config_email_body(email_config, _submitter = nil)
email_config ? email_config.value['body'].presence : nil email_config ? email_config.value['body'].presence : nil
end end
def maybe_set_custom_domain(submitter)
if Docuseal.multitenant? && (config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain))
@custom_domain = config.value
end
end
end end

@ -49,6 +49,7 @@ class AccountConfig < ApplicationRecord
WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SENDER_KEY = 'with_audit_sender' WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
WITH_TIMESTAMP_SECONDS_KEY = 'with_timestamp_seconds'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature' REUSE_SIGNATURE_KEY = 'reuse_signature'
WITH_FIELD_LABELS_KEY = 'with_field_labels' WITH_FIELD_LABELS_KEY = 'with_field_labels'
@ -57,6 +58,13 @@ class AccountConfig < ApplicationRecord
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields' TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
POLICY_LINKS_KEY = 'policy_links' POLICY_LINKS_KEY = 'policy_links'
EMAIL_VARIABLES = {
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
}.freeze
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => lambda { SUBMITTER_INVITATION_EMAIL_KEY => lambda {
{ {

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 14l-4 -4l4 -4" />
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
</svg>

After

Width:  |  Height:  |  Size: 354 B

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 14l4 -4l-4 -4" />
<path d="M19 10h-11a4 4 0 1 0 0 8h1" />
</svg>

After

Width:  |  Height:  |  Size: 356 B

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6l0 -7" />
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

@ -0,0 +1,6 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 5l6 0" />
<path d="M7 19l6 0" />
<path d="M14 5l-4 14" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 5v5a5 5 0 0 0 10 0v-5" />
<path d="M5 19h14" />
</svg>

After

Width:  |  Height:  |  Size: 345 B

@ -1,11 +1,5 @@
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= ff.label :body, t('body'), class: 'label' %>
<%= ff.label :body, t('body'), class: 'label' %> <% variables = AccountConfig::EMAIL_VARIABLES[local_assigns[:config].key] %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>"> <%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:config].value['body'], variables: variables %>
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
</autoresize-textarea>
</div> </div>

@ -0,0 +1,70 @@
<% link_tooltip_html = capture do %>
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
</button>
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
<% end %>
<markdown-editor data-link-tooltip-html="<%= link_tooltip_html.squish %>">
<div class="border border-base-content/20 rounded-2xl bg-white">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div>
<div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button>
</div>
</div>
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div>
</div>
<% end %>
</div>
<div data-target="markdown-editor.editorElement"></div>
</div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</markdown-editor>

@ -12,7 +12,7 @@
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['subject'].scan(/{.*?}/).join(', ') %>"> <span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>

@ -1,5 +1,5 @@
<% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %> <% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %>
<div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2"> <div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2">
<%= auto_link(MarkdownToHtml.call(configs.value)) %> <%= MarkdownToHtml.call(configs.value) %>
</div> </div>
<% end %> <% end %>

@ -1,6 +1,6 @@
<% has_phone_field = false %> <% has_phone_field = false %>
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="space-y-4">
<div class="space-y-4"> <div class="space-y-4">
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items"> <div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">

@ -1,5 +1,5 @@
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<submitter-item class="form-control"> <submitter-item class="form-control">
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>"> <emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">

@ -1,5 +1,5 @@
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="space-y-4">
<div class="space-y-4"> <div class="space-y-4">
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items"> <div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">

@ -55,15 +55,9 @@
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= f.label :message, t('body'), class: 'label' %>
<%= f.label :message, t('body'), class: 'label' %> <% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>"> <%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %> <% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer"> <label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %> <%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
@ -97,15 +91,8 @@
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= ff.label :message, t('body'), class: 'label' %>
<%= ff.label :message, t('body'), class: 'label' %> <%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div> </div>
</div> </div>
<% end %> <% end %>

@ -28,7 +28,8 @@
<% end %> <% end %>
<div> <div>
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %> <% 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) %> <% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
<%= l(attachment.created_at.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
</div> </div>
</div> </div>
<% end %> <% end %>

@ -2,10 +2,11 @@
<%= render 'submissions/preview_tags' %> <%= render 'submissions/preview_tags' %>
<% end %> <% end %>
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %> <% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
<% 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]) %> <% 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, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY]) %>
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %> <% 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 && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %> <% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %>
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %> <% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %> <% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
<div style="max-width: 1600px" class="mx-auto pl-4"> <div style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100"> <div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
@ -20,7 +21,7 @@
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %> <%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
<% end %> <% end %>
<% if @submission.audit_trail.present? %> <% if @submission.audit_trail.present? %>
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank"> <a href="<%= ActiveStorage::Blob.proxy_path(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
<%= svg_icon('external_link', class: 'w-6 h-6') %> <%= svg_icon('external_link', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('audit_log') %></span> <span class="hidden md:inline"><%= t('audit_log') %></span>
</a> </a>
@ -125,7 +126,7 @@
</span> </span>
</span> </span>
<% else %> <% else %>
<%= render 'submissions/value', page_width: width, page_height: height, 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: %> <%= render 'submissions/value', page_width: width, page_height: height, 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_timestamp_seconds:, with_signature_id_reason: %>
<% end %> <% end %>
<% elsif field['readonly'] != true && submitter && !submitter.completed_at? %> <% elsif field['readonly'] != true && submitter && !submitter.completed_at? %>
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %> <% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>

@ -80,7 +80,7 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %> <% 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['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', page_width: width, page_height: height, 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] %> <%= render 'submissions/value', page_width: width, page_height: height, 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_timestamp_seconds: @form_configs[:with_timestamp_seconds], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
<% end %> <% end %>
</div> </div>
</page-container> </page-container>
@ -91,7 +91,7 @@
<% if @form_configs[:policy_links].present? %> <% if @form_configs[:policy_links].present? %>
<div class="text-center md:text-neutral-500 md:pr-3 md:pb-3 md:text-sm md:text-left mt-2 md:mt-0 md:fixed md:bottom-0 md:right-0"> <div class="text-center md:text-neutral-500 md:pr-3 md:pb-3 md:text-sm md:text-left mt-2 md:mt-0 md:fixed md:bottom-0 md:right-0">
<div class="md:max-w-[8rem] flex flex-wrap md:flex-col justify-center md:justify-start md:items-start space-x-2 md:space-x-0"> <div class="md:max-w-[8rem] flex flex-wrap md:flex-col justify-center md:justify-start md:items-start space-x-2 md:space-x-0">
<%= auto_link(MarkdownToHtml.call(@form_configs[:policy_links])) %> <%= MarkdownToHtml.call(@form_configs[:policy_links]) %>
</div> </div>
</div> </div>
<% end %> <% end %>

@ -1 +1 @@
<%= auto_link(simple_format(MarkdownToHtml.call(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig]))))) %> <%= MarkdownToHtml.call(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])) %>

@ -5,7 +5,7 @@
<p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %> <p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>
<p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p> <p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p>
<p> <p>
<%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %> <%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact) %>
</p> </p>
<p> <p>
<%= t('thanks') %>,<br><%= @current_account.name %> <%= t('thanks') %>,<br><%= @current_account.name %>

@ -1,12 +1,12 @@
<% if @body.present? %> <% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %> <%= render 'custom_content', content: @body, submitter: @submitter %>
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %> <% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p> <p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<% end %> <% end %>
<% else %> <% else %>
<p><%= t('hi_there') %>,</p> <p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p> <p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p> <p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p> <p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p> <p>
<%= t('thanks') %>,<br><%= @current_account.name %> <%= t('thanks') %>,<br><%= @current_account.name %>

@ -81,7 +81,7 @@
<% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %> <% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<div class="flex-1 md:flex-none md:w-36 flex"> <div class="flex-1 md:flex-none md:w-36 flex">
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]"> <a href="<%= submit_form_path(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<% if t('sign_now').length < 12 %> <% if t('sign_now').length < 12 %>
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
@ -167,7 +167,7 @@
<% elsif !template&.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %> <% elsif !template&.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<div class="relative flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]"> <a href="<%= submit_form_path(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<% if t('sign_now').length < 12 %> <% if t('sign_now').length < 12 %>
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>

@ -94,7 +94,7 @@
<span class="mr-1"><%= t('send_to_recipients') %></span> <span class="mr-1"><%= t('send_to_recipients') %></span>
<% end %> <% end %>
<% end %> <% end %>
<% if @template.submitters.size == 1 %> <% if Templates.filter_undefined_submitters(@template.submitters).size == 1 %>
<%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %> <%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <span class="mr-1"><%= t('sign_it_yourself') %></span>

@ -7,8 +7,8 @@
<div class="space-y-3 divide-y"> <div class="space-y-3 divide-y">
<% template.submitters.each_with_index do |submitter, index| %> <% template.submitters.each_with_index do |submitter, index| %>
<div class="<%= 'pt-3' if index.positive? %>"> <div class="<%= 'pt-3' if index.positive? %>">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %> <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %> <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %>
<%= ff.hidden_field :uuid %> <%= ff.hidden_field :uuid %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
@ -26,35 +26,71 @@
<% end %> <% end %>
</div> </div>
<% if template.submitters.size == 2 %> <% if template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> <%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, multiple: true, autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid = SecureRandom.uuid, disabled: is_email_disabled = ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present?, class: "base-input w-full #{'hidden' if ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present?}" %>
<% if index == 1 %>
<% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %>
<disable-hidden id="<%= invite_by_selector_id = SecureRandom.uuid %>" class="<%= 'hidden' if ff.object.invite_by_uuid.blank? && ff.object.optional_invite_by_uuid.blank? && ff.object.invite_via_field_uuid.blank? %>">
<toggle-attribute data-target-id="<%= invite_via_field_selector_id = SecureRandom.uuid %>" data-class-name="hidden" data-value="invite_via_field">
<%= ff.select :option, [*(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, (invite_fields.present? ? [t('invite_via_form_field'), 'invite_via_field'] : nil)].compact, {}, class: 'base-select' %>
</toggle-attribute>
<% if invite_fields.present? %>
<disable-hidden id="<%= invite_via_field_selector_id %>" class="mt-2 <%= 'hidden' if ff.object.invite_via_field_uuid.blank? %>">
<%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, ff.object.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select mt-3' %>
</disable-hidden>
<% end %>
</disable-hidden>
<% end %>
<% else %> <% else %>
<% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email"> <toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %> <toggle-attribute data-target-id="<%= invite_field_wrapper_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="invite_via_field">
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }, (invite_fields.present? ? [t('invite_via_form_field'), 'invite_via_field'] : nil)].compact, {}, class: 'base-select mb-3' %>
</toggle-attribute>
</toggle-attribute> </toggle-attribute>
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %> <%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<% if invite_fields.present? %>
<disable-hidden id="<%= invite_field_wrapper_uuid %>" class="<%= 'hidden' if item.option != 'invite_via_field' %>">
<%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, item.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select', required: true %>
</disable-hidden>
<% end %>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size == 2 %> <% if template.submitters.size == 2 %>
<checkbox-group class="mt-3 flex items-center space-x-4"> <checkbox-group class="mt-3 flex items-center space-x-4">
<% if local_assigns[:with_submission_requester] != false %> <% if index == 0 && local_assigns[:with_submission_requester] != false %>
<label class="flex items-center space-x-2 cursor-pointer"> <label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled"> <toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-attribute="disabled">
<%= ff.check_box :is_requester, class: 'base-checkbox' %> <%= ff.check_box :is_requester, class: 'base-checkbox' %>
</toggle-attribute> </toggle-attribute>
<span class="select-none"> <span class="select-none">
<%= t('submission_requester') %> <%= t('submission_requester') %>
</span> </span>
</label> </label>
<% end %> <% elsif index == 1 %>
<% if index == 1 %> <% if local_assigns[:with_submission_requester] != false %>
<toggle-classes data-target-id="<%= invite_by_selector_id %>" class="flex" data-classes="hidden" data-action="add">
<toggle-classes data-target-id="<%= email_field_uuid %>" class="flex" data-classes="hidden" data-action="remove">
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-attribute="disabled">
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
</toggle-attribute>
<span class="select-none">
<%= t('submission_requester') %>
</span>
</label>
</toggle-classes>
</toggle-classes>
<% end %>
<label class="flex items-center space-x-2 cursor-pointer"> <label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled"> <toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-class-name="hidden" data-value="false">
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex"> <toggle-attribute data-target-id="<%= invite_by_selector_id %>" class="flex" data-class-name="hidden">
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, template.submitters.first['uuid'], '' %> <toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-attribute="disabled">
</indeterminate-checkbox> <%= ff.check_box :invite_checbox, { class: 'base-checkbox', checked: ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present? }, true, '' %>
</toggle-attribute>
</toggle-attribute>
</toggle-attribute> </toggle-attribute>
<span class="select-none"> <span class="select-none">
<%= t('invite_by_name', name: template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span> <%= t('invite_by_name', name: template.submitters.first['name']) %>
</span> </span>
</label> </label>
<% end %> <% end %>

@ -20,15 +20,8 @@
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %> <%= render 'personalization_settings/markdown_editor', name: ff.field_name(:completed_notification_email_body), value: ff.object.completed_notification_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY] %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
</autoresize-textarea>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>

@ -20,15 +20,8 @@
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %> <%= render 'personalization_settings/markdown_editor', name: ff.field_name(:documents_copy_email_body), value: ff.object.documents_copy_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY] %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
</autoresize-textarea>
</div> </div>
<% if can?(:manage, :reply_to) %> <% if can?(:manage, :reply_to) %>
<div class="form-control"> <div class="form-control">

@ -27,15 +27,8 @@
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<%= ff.label :request_email_body, t('email_body'), class: 'label' %> <%= render 'personalization_settings/markdown_editor', name: ff.field_name(:request_email_body), value: ff.object.request_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
</autoresize-textarea>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -52,38 +45,28 @@
<% end %> <% end %>
</ul> </ul>
</toggle-visible> </toggle-visible>
<%= f.fields_for :preferences do |ff| %> <% @template.submitters.each_with_index do |submitter, index| %>
<% @template.submitters.each_with_index do |submitter, index| %> <div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>"> <% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %> <% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %> <% submitter_email_values = submitter_email_preferences_values || template_email_preferences_values.presence || default_template_email_preferences_values %>
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || template_email_preferences_values.presence || default_template_email_preferences_values)), index: nil do |fff| %> <%= hidden_field_tag 'template[preferences][submitters][][uuid]', submitter['uuid'] %>
<%= fff.hidden_field :uuid, value: submitter['uuid'] %> <div class="form-control">
<div class="form-control"> <div class="flex justify-between">
<div class="flex justify-between"> <label class="label"><%= t('email_subject') %></label>
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %> <% if submitter_email_preferences_values.present? %>
<% if submitter_email_preferences_values.present? %> <label for="submitter_invitation_email_reset_link" class="label underline">
<label for="submitter_invitation_email_reset_link" class="label underline"> <%= t('reset_default') %>
<%= t('reset_default') %> </label>
</label> <% end %>
<% end %> </div>
</div> <%= text_field_tag 'template[preferences][submitters][][request_email_subject]', submitter_email_values.first, required: true, class: 'base-input', dir: 'auto', autocomplete: 'off' %>
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div> </div>
<% end %> <div class="form-control">
<label class="label"><%= t('email_body') %></label>
<%= render 'personalization_settings/markdown_editor', name: 'template[preferences][submitters][][request_email_body]', value: submitter_email_values.last, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
</div>
</div>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

@ -4,7 +4,7 @@
<%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %> <%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
<div class="form-control"> <div class="form-control">
<%= f.hidden_field :prefillable, value: 'true' %> <%= f.hidden_field :prefillable, value: 'true' %>
<%= f.label :field_uuid, t(:invite_form_fields), class: 'label' %> <%= f.label :field_uuid, t(:sender_form_fields), class: 'label' %>
<div class="join w-full"> <div class="join w-full">
<%= select_tag :field_uuid, options_for_select(select_fields), prompt: t(:select_field), class: 'base-select w-full join-item', dir: 'auto', required: true %> <%= select_tag :field_uuid, options_for_select(select_fields), prompt: t(:select_field), class: 'base-select w-full join-item', dir: 'auto', required: true %>
<%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %> <%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %>

@ -17,9 +17,9 @@
</label> </label>
<% end %> <% end %>
<div class="flex gap-2 mt-3"> <div class="flex gap-2 mt-3">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly> <input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full" autocomplete="off" readonly>
<check-on-click data-element-id="template_shared_link"> <check-on-click data-element-id="template_shared_link">
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</check-on-click> </check-on-click>
</div> </div>
<% end %> <% end %>

@ -60,7 +60,7 @@ if ENV['RAILS_ENV'] == 'production'
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil) ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
end end
unless Process.euid == 2000 if Process.uid.zero?
begin begin
test_file = "#{ENV.fetch('WORKDIR', '.')}/test" test_file = "#{ENV.fetch('WORKDIR', '.')}/test"

@ -10,6 +10,7 @@ ActiveSupport.on_load(:active_storage_attachment) do
end end
end end
# rubocop:disable Metrics/BlockLength
ActiveSupport.on_load(:active_storage_blob) do ActiveSupport.on_load(:active_storage_blob) do
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
attribute :io_data, :string, default: '' attribute :io_data, :string, default: ''
@ -22,6 +23,12 @@ ActiveSupport.on_load(:active_storage_blob) do
) )
end end
def self.proxy_path(blob, expires_at: nil, filename: nil)
Rails.application.routes.url_helpers.blobs_proxy_path(
signed_uuid: blob.signed_uuid(expires_at:), filename: filename || blob.filename
)
end
def uuid def uuid
super || begin super || begin
new_uuid = SecureRandom.uuid new_uuid = SecureRandom.uuid
@ -40,6 +47,7 @@ ActiveSupport.on_load(:active_storage_blob) do
service.delete(key) service.delete(key)
end end
end end
# rubocop:enable Metrics/BlockLength
ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production? ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production?

@ -44,7 +44,7 @@ en: &en
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.' click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
edit_order: Edit Order edit_order: Edit Order
expirable_file_download_links: Expirable file download links expirable_file_download_links: Expirable file download links
invite_form_fields: Invite form fields sender_form_fields: Sender form fields
default_parties: Default parties default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
stripe_integration: Stripe Integration stripe_integration: Stripe Integration
@ -218,7 +218,7 @@ en: &en
copy: Copy copy: Copy
copied: Copied copied: Copied
rotate: Rotate rotate: Rotate
remove_existing_api_token_and_generated_a_new_one_are_you_sure_: Remove existing API token and generated a new one. Are you sure? remove_existing_api_token_and_generated_a_new_one_are_you_sure_: Remove existing API token and generate a new one. Are you sure?
request_signature_multiple_submitters_with_default_values: Request signature, multiple submitters with default values request_signature_multiple_submitters_with_default_values: Request signature, multiple submitters with default values
request_signature_single_submitter: Request signature, single submitter request_signature_single_submitter: Request signature, single submitter
template_details: Template details template_details: Template details
@ -330,8 +330,8 @@ en: &en
initials: Initials initials: Initials
update_initials: Update Initials update_initials: Update Initials
unable_to_save_initials: Unable to save initials. unable_to_save_initials: Unable to save initials.
initials_has_been_saved: Initials has been saved. initials_has_been_saved: Initials have been saved.
initials_has_been_removed: Initials has been removed. initials_has_been_removed: Initials have been removed.
change_password: Change Password change_password: Change Password
two_factor_authentication: Two-Factor Authentication two_factor_authentication: Two-Factor Authentication
2fa_is_not_configured: 2FA is not configured 2fa_is_not_configured: 2FA is not configured
@ -388,7 +388,7 @@ en: &en
from: From from: From
account_sid: Account SID account_sid: Account SID
send_sms_via_webhook: Send SMS via webhook send_sms_via_webhook: Send SMS via webhook
webhook_integration_allows_to_send_sms_using_any_provider: Webhook integration allows to send SMS using any provider webhook_integration_allows_to_send_sms_using_any_provider: Webhook integration allows you to send SMS using any provider
test: Test test: Test
single_sign_on_with_saml_2_0: Single Sign On with SAML 2.0 single_sign_on_with_saml_2_0: Single Sign On with SAML 2.0
force_sso_disable_login_with_email_and_password: Force SSO (disable login with email and password) force_sso_disable_login_with_email_and_password: Force SSO (disable login with email and password)
@ -420,7 +420,7 @@ en: &en
send_signature_request_emails_without_limits_with_docuseal_pro: Send signature request emails without limits with DocuSeal Pro send_signature_request_emails_without_limits_with_docuseal_pro: Send signature request emails without limits with DocuSeal Pro
count_emails_used: '%{count} emails used' count_emails_used: '%{count} emails used'
has_been_connected: has been connected has_been_connected: has been connected
sms_not_configured: SMS not Configure sms_not_configured: SMS not Configured
configure_sms_settings_in_order_to_send_text_messages_: 'Configure SMS settings in order to send text messages:' configure_sms_settings_in_order_to_send_text_messages_: 'Configure SMS settings in order to send text messages:'
go_to_sms_settings: Go to SMS settings go_to_sms_settings: Go to SMS settings
back_to_active: Back to Active back_to_active: Back to Active
@ -500,6 +500,7 @@ en: &en
submission_requester: Submission requester submission_requester: Submission requester
specified_email: Specified email specified_email: Specified email
invite_by_name: 'Invite by %{name}' invite_by_name: 'Invite by %{name}'
invite_via_form_field: Invite via Form Field
same_as_name: 'Same as %{name}' same_as_name: 'Same as %{name}'
default_email: Default Email default_email: Default Email
processing: Processing processing: Processing
@ -596,7 +597,7 @@ en: &en
upload_file: Upload file upload_file: Upload file
upgrade_your_plan_to_invite_more_users_contact_email: 'Upgrade your plan to invite more users (contact %{email}).' upgrade_your_plan_to_invite_more_users_contact_email: 'Upgrade your plan to invite more users (contact %{email}).'
contact_your_admin_email_to_invite_more_users: 'Contact your admin %{email} to invite more users.' contact_your_admin_email_to_invite_more_users: 'Contact your admin %{email} to invite more users.'
contact_your_administrator_to_add_new_users: Contact your administrator to add new user contact_your_administrator_to_add_new_users: Contact your administrator to add new users
one_hour: 1 hour one_hour: 1 hour
two_hours: 2 hours two_hours: 2 hours
four_hours: 4 hours four_hours: 4 hours
@ -625,7 +626,7 @@ en: &en
personalize_email_content: Personalize email content personalize_email_content: Personalize email content
automated_reminders: Automated reminders automated_reminders: Automated reminders
bulk_send_from_spreadsheet: Bulk send from spreadsheet bulk_send_from_spreadsheet: Bulk send from spreadsheet
identify_verification_via_sms: Identify verification via SMS identify_verification_via_sms: Identity verification via SMS
start_with_pro: Start with Pro start_with_pro: Start with Pro
user_month: user / month user_month: user / month
developer_sandbox: Developer Sandbox. developer_sandbox: Developer Sandbox.
@ -737,7 +738,7 @@ en: &en
find_suitable_zapier_templates_to_automate_your_workflow: Find suitable Zapier templates to automate your workflow. find_suitable_zapier_templates_to_automate_your_workflow: Find suitable Zapier templates to automate your workflow.
get_started: Get started get_started: Get started
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Click here</a> to learn more about user roles and permissions.' click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Click here</a> to learn more about user roles and permissions.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request email.' count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request emails.'
test_mode_emails_limit_will_be_reset_within_24_hours: Test mode emails limit will be reset within 24 hours. test_mode_emails_limit_will_be_reset_within_24_hours: Test mode emails limit will be reset within 24 hours.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: On a scale of 1 to 10, how satisfied are you with the DocuSeal product? on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: On a scale of 1 to 10, how satisfied are you with the DocuSeal product?
tell_us_more_about_your_experience: Tell us more about your experience tell_us_more_about_your_experience: Tell us more about your experience
@ -755,7 +756,7 @@ en: &en
manage_plan: Manage plan manage_plan: Manage plan
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: This submission has multiple signers, which prevents the use of a sharing link as it's unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details. this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: This submission has multiple signers, which prevents the use of a sharing link as it's unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details.
welcome_to_docuseal: Welcome to DocuSeal welcome_to_docuseal: Welcome to DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create an send your first document start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create and send your first document
start_tour: Start Tour start_tour: Start Tour
name_a_z: Name A-Z name_a_z: Name A-Z
recently_used: Recently used recently_used: Recently used
@ -815,7 +816,7 @@ en: &en
connect_gmail_or_outlook: Connect Gmail or Outlook connect_gmail_or_outlook: Connect Gmail or Outlook
connect_your_email_to_bulk_send: Connect your email to bulk send connect_your_email_to_bulk_send: Connect your email to bulk send
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connect your Gmail or Outlook account or add SMTP settings to bulk send. connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connect your Gmail or Outlook account or add SMTP settings to bulk send.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails it requires to connect Gmail or Outlook. are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails you need to connect Gmail or Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".' you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
@ -886,6 +887,13 @@ en: &en
if_you_didnt_request_this_you_can_ignore_this_email: "If you didn't request this, please ignore this email." if_you_didnt_request_this_you_can_ignore_this_email: "If you didn't request this, please ignore this email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Your password won't change until you open the link above and set a new one." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Your password won't change until you open the link above and set a new one."
too_many_requests_try_again_later: Too many requests, try again later. too_many_requests_try_again_later: Too many requests, try again later.
bold: Bold
italic: Italic
underline: Underline
undo: Undo
redo: Redo
add_variable: Add variable
enter_a_url_or_variable_name: Enter a URL or variable name
devise: devise:
confirmations: confirmations:
confirmed: Your email address has been successfully confirmed. confirmed: Your email address has been successfully confirmed.
@ -1002,6 +1010,16 @@ en: &en
events: events:
range_with_total: "%{from}-%{to} of %{count} events" range_with_total: "%{from}-%{to} of %{count} events"
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
variables:
account_name: Account name
submitter_link: Submitter link
template_name: Template name
submission_submitters: Submitters list
submission_link: Submission link
documents_link: Documents link
time:
formats:
detailed: "%B %d, %Y %H:%M:%S"
es: &es es: &es
knowledge_based_authentication: Autenticación basada en el conocimiento knowledge_based_authentication: Autenticación basada en el conocimiento
@ -1022,7 +1040,7 @@ es: &es
party: Parte party: Parte
edit_order: Edita Pedido edit_order: Edita Pedido
select: Seleccionar select: Seleccionar
invite_form_fields: Invitar campos del formulario sender_form_fields: Campos del formulario del remitente
pro: Pro pro: Pro
default_parties: Partes predeterminadas default_parties: Partes predeterminadas
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
@ -1487,6 +1505,7 @@ es: &es
submission_requester: Solicitante del envío submission_requester: Solicitante del envío
specified_email: Correo electrónico especificado specified_email: Correo electrónico especificado
invite_by_name: 'Invitar por %{name}' invite_by_name: 'Invitar por %{name}'
invite_via_form_field: Invitar a través de campo del formulario
same_as_name: 'Igual que %{name}' same_as_name: 'Igual que %{name}'
default_email: Correo electrónico predeterminado default_email: Correo electrónico predeterminado
processing: Procesando processing: Procesando
@ -1870,6 +1889,13 @@ es: &es
if_you_didnt_request_this_you_can_ignore_this_email: "Si no solicitaste esto, puedes ignorar este correo electrónico." if_you_didnt_request_this_you_can_ignore_this_email: "Si no solicitaste esto, puedes ignorar este correo electrónico."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Tu contraseña no cambiará hasta que abras el enlace anterior y establezcas una nueva." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Tu contraseña no cambiará hasta que abras el enlace anterior y establezcas una nueva."
too_many_requests_try_again_later: Demasiadas solicitudes. Intenta de nuevo más tarde. too_many_requests_try_again_later: Demasiadas solicitudes. Intenta de nuevo más tarde.
bold: Negrita
italic: Cursiva
underline: Subrayado
undo: Deshacer
redo: Rehacer
add_variable: Agregar variable
enter_a_url_or_variable_name: Ingrese una URL o nombre de variable
devise: devise:
confirmations: confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente. confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
@ -1986,6 +2012,16 @@ es: &es
events: events:
range_with_total: "%{from}-%{to} de %{count} eventos" range_with_total: "%{from}-%{to} de %{count} eventos"
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
variables:
account_name: Nombre de la cuenta
submitter_link: Enlace del firmante
template_name: Nombre de la plantilla
submission_submitters: Lista de firmantes
submission_link: Enlace del envío
documents_link: Enlace de los documentos
time:
formats:
detailed: "%-d de %B de %Y %H:%M:%S"
it: &it it: &it
knowledge_based_authentication: Autenticazione basata sulla conoscenza knowledge_based_authentication: Autenticazione basata sulla conoscenza
@ -2006,7 +2042,7 @@ it: &it
party: Parte party: Parte
edit_order: Modifica Ordine edit_order: Modifica Ordine
select: Seleziona select: Seleziona
invite_form_fields: Invita campi modulo sender_form_fields: Campi del modulo del mittente
pro: Pro pro: Pro
default_parties: Parti predefiniti default_parties: Parti predefiniti
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token" authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
@ -2471,6 +2507,7 @@ it: &it
submission_requester: "Richiedente dell'invio" submission_requester: "Richiedente dell'invio"
specified_email: Email specificata specified_email: Email specificata
invite_by_name: 'Invito da %{name}' invite_by_name: 'Invito da %{name}'
invite_via_form_field: Invito tramite campo del modulo
same_as_name: 'Uguale a %{name}' same_as_name: 'Uguale a %{name}'
default_email: Email predefinita default_email: Email predefinita
processing: Elaborazione in corso processing: Elaborazione in corso
@ -2855,6 +2892,13 @@ it: &it
if_you_didnt_request_this_you_can_ignore_this_email: "Se non hai richiesto questo, puoi ignorare questa email." if_you_didnt_request_this_you_can_ignore_this_email: "Se non hai richiesto questo, puoi ignorare questa email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "La tua password non cambierà finché non apri il link sopra e ne imposti una nuova." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "La tua password non cambierà finché non apri il link sopra e ne imposti una nuova."
too_many_requests_try_again_later: Troppe richieste. Riprova più tardi. too_many_requests_try_again_later: Troppe richieste. Riprova più tardi.
bold: Grassetto
italic: Corsivo
underline: Sottolineato
undo: Annulla
redo: Ripeti
add_variable: Aggiungi variabile
enter_a_url_or_variable_name: Inserisci un URL o nome variabile
devise: devise:
confirmations: confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo. confirmed: Il tuo indirizzo email è stato confermato con successo.
@ -2971,6 +3015,16 @@ it: &it
events: events:
range_with_total: "%{from}-%{to} di %{count} eventi" range_with_total: "%{from}-%{to} di %{count} eventi"
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
variables:
account_name: Nome dell'account
submitter_link: Link del firmatario
template_name: Nome del modello
submission_submitters: Lista dei firmatari
submission_link: Link dell'invio
documents_link: Link dei documenti
time:
formats:
detailed: "%d %B %Y %H:%M:%S"
fr: &fr fr: &fr
knowledge_based_authentication: Authentification basée sur la connaissance knowledge_based_authentication: Authentification basée sur la connaissance
@ -2991,7 +3045,7 @@ fr: &fr
party: Partie party: Partie
edit_order: Modifier lordre edit_order: Modifier lordre
select: Sélectionner select: Sélectionner
invite_form_fields: Champs du formulaire dinvitation sender_form_fields: Champs du formulaire de lexpéditeur
pro: Pro pro: Pro
default_parties: Parties par défaut default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton
@ -3456,6 +3510,7 @@ fr: &fr
submission_requester: Demandeur de soumission submission_requester: Demandeur de soumission
specified_email: Email spécifié specified_email: Email spécifié
invite_by_name: Inviter par %{name} invite_by_name: Inviter par %{name}
invite_via_form_field: Inviter via champ du formulaire
same_as_name: Identique à %{name} same_as_name: Identique à %{name}
default_email: Email par défaut default_email: Email par défaut
processing: Traitement en cours processing: Traitement en cours
@ -3836,6 +3891,13 @@ fr: &fr
if_you_didnt_request_this_you_can_ignore_this_email: "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail." if_you_didnt_request_this_you_can_ignore_this_email: "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Votre mot de passe ne changera pas tant que vous naurez pas ouvert le lien ci-dessus et défini un nouveau mot de passe." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Votre mot de passe ne changera pas tant que vous naurez pas ouvert le lien ci-dessus et défini un nouveau mot de passe."
too_many_requests_try_again_later: Trop de demandes. Réessayez plus tard. too_many_requests_try_again_later: Trop de demandes. Réessayez plus tard.
bold: Gras
italic: Italique
underline: Souligné
undo: Annuler
redo: Rétablir
add_variable: Ajouter une variable
enter_a_url_or_variable_name: Entrez une URL ou un nom de variable
devise: devise:
confirmations: confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès. confirmed: Votre adresse e-mail a été confirmée avec succès.
@ -3952,6 +4014,16 @@ fr: &fr
events: events:
range_with_total: "%{from}-%{to} sur %{count} événements" range_with_total: "%{from}-%{to} sur %{count} événements"
range_without_total: "%{from}-%{to} événements" range_without_total: "%{from}-%{to} événements"
variables:
account_name: Nom du compte
submitter_link: Lien du signataire
template_name: Nom du modèle
submission_submitters: Liste des signataires
submission_link: Lien de la soumission
documents_link: Lien des documents
time:
formats:
detailed: "%A %d %B %Y %Hh%Mm%Ss"
pt: &pt pt: &pt
knowledge_based_authentication: Autenticação baseada em conhecimento knowledge_based_authentication: Autenticação baseada em conhecimento
@ -3972,7 +4044,7 @@ pt: &pt
party: Parte party: Parte
edit_order: Edita Pedido edit_order: Edita Pedido
select: Selecionar select: Selecionar
invite_form_fields: Convidar campos do formulário sender_form_fields: Campos do formulário do remetente
pro: Pro pro: Pro
default_parties: Partes padrão default_parties: Partes padrão
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
@ -4437,6 +4509,7 @@ pt: &pt
submission_requester: Solicitante de submissão submission_requester: Solicitante de submissão
specified_email: E-mail especificado specified_email: E-mail especificado
invite_by_name: 'Convidado por %{name}' invite_by_name: 'Convidado por %{name}'
invite_via_form_field: Convidar via campo do formulário
same_as_name: 'Igual a %{name}' same_as_name: 'Igual a %{name}'
default_email: E-mail padrão default_email: E-mail padrão
processing: Processando processing: Processando
@ -4820,6 +4893,13 @@ pt: &pt
if_you_didnt_request_this_you_can_ignore_this_email: "Se você não solicitou isso, pode ignorar este e-mail." if_you_didnt_request_this_you_can_ignore_this_email: "Se você não solicitou isso, pode ignorar este e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Sua senha não será alterada até que você abra o link acima e defina uma nova." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Sua senha não será alterada até que você abra o link acima e defina uma nova."
too_many_requests_try_again_later: Muitas solicitações. Tente novamente mais tarde. too_many_requests_try_again_later: Muitas solicitações. Tente novamente mais tarde.
bold: Negrito
italic: Itálico
underline: Sublinhado
undo: Desfazer
redo: Refazer
add_variable: Adicionar variável
enter_a_url_or_variable_name: Digite uma URL ou nome de variável
devise: devise:
confirmations: confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso. confirmed: Seu endereço de e-mail foi confirmado com sucesso.
@ -4936,6 +5016,16 @@ pt: &pt
events: events:
range_with_total: "%{from}-%{to} de %{count} eventos" range_with_total: "%{from}-%{to} de %{count} eventos"
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
variables:
account_name: Nome da conta
submitter_link: Link do signatário
template_name: Nome do modelo
submission_submitters: Lista de signatários
submission_link: Link da submissão
documents_link: Link dos documentos
time:
formats:
detailed: "%A, %d de %B de %Y, %H:%M:%Sh"
de: &de de: &de
knowledge_based_authentication: Wissensbasierte Authentifizierung knowledge_based_authentication: Wissensbasierte Authentifizierung
@ -4965,7 +5055,7 @@ de: &de
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Klicken Sie hier</label>, um eine E-Mail zum Zurücksetzen des Passworts zu senden.' click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Klicken Sie hier</label>, um eine E-Mail zum Zurücksetzen des Passworts zu senden.'
edit_order: Bestellung bearbeiten edit_order: Bestellung bearbeiten
expirable_file_download_links: Ablaufbare Datei-Download-Links expirable_file_download_links: Ablaufbare Datei-Download-Links
invite_form_fields: Einladungsformular-Felder sender_form_fields: Absenderformular-Felder
default_parties: Standardparteien default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Eingebettete Formularvorschau mit Token authentifizieren authenticate_embedded_form_preview_with_token: Eingebettete Formularvorschau mit Token authentifizieren
stripe_integration: Stripe-Integration stripe_integration: Stripe-Integration
@ -5421,6 +5511,7 @@ de: &de
submission_requester: Anfragende Person submission_requester: Anfragende Person
specified_email: Angegebene E-Mail specified_email: Angegebene E-Mail
invite_by_name: 'Einladung von %{name}' invite_by_name: 'Einladung von %{name}'
invite_via_form_field: Einladung über Formularfeld
same_as_name: 'Gleich wie %{name}' same_as_name: 'Gleich wie %{name}'
default_email: Standard-E-Mail default_email: Standard-E-Mail
processing: Verarbeitung processing: Verarbeitung
@ -5804,6 +5895,13 @@ de: &de
if_you_didnt_request_this_you_can_ignore_this_email: "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren." if_you_didnt_request_this_you_can_ignore_this_email: "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Ihr Passwort wird erst geändert, wenn Sie den obigen Link öffnen und ein neues festlegen." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Ihr Passwort wird erst geändert, wenn Sie den obigen Link öffnen und ein neues festlegen."
too_many_requests_try_again_later: Zu viele Anfragen. Versuchen Sie es später erneut. too_many_requests_try_again_later: Zu viele Anfragen. Versuchen Sie es später erneut.
bold: Fett
italic: Kursiv
underline: Unterstrichen
undo: Rückgängig
redo: Wiederholen
add_variable: Variable hinzufügen
enter_a_url_or_variable_name: Geben Sie eine URL oder einen Variablennamen ein
devise: devise:
confirmations: confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt. confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
@ -5920,6 +6018,16 @@ de: &de
events: events:
range_with_total: "%{from}-%{to} von %{count} Ereignissen" range_with_total: "%{from}-%{to} von %{count} Ereignissen"
range_without_total: "%{from}-%{to} Ereignisse" range_without_total: "%{from}-%{to} Ereignisse"
variables:
account_name: Kontoname
submitter_link: Link des Unterzeichners
template_name: Vorlagenname
submission_submitters: Liste der Unterzeichner
submission_link: Link der Einreichung
documents_link: Link der Dokumente
time:
formats:
detailed: "%A, %d. %B %Y, %H:%M:%S Uhr"
pl: pl:
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
@ -6337,7 +6445,7 @@ nl: &nl
click_here_to_send_a_reset_password_email_html: <label class="link" for="resend_password_button">Klik hier</label> om een e-mail voor wachtwoordherstel te verzenden. click_here_to_send_a_reset_password_email_html: <label class="link" for="resend_password_button">Klik hier</label> om een e-mail voor wachtwoordherstel te verzenden.
edit_order: Volgorde bewerken edit_order: Volgorde bewerken
expirable_file_download_links: Verlopende downloadlinks voor bestanden expirable_file_download_links: Verlopende downloadlinks voor bestanden
invite_form_fields: Velden van uitnodigingsformulier sender_form_fields: Velden van afzenderformulier
default_parties: Standaard partijen default_parties: Standaard partijen
authenticate_embedded_form_preview_with_token: Preview van ingesloten formulier authenticeren met token authenticate_embedded_form_preview_with_token: Preview van ingesloten formulier authenticeren met token
stripe_integration: Stripe-integratie stripe_integration: Stripe-integratie
@ -6794,6 +6902,7 @@ nl: &nl
submission_requester: Aanvrager van inzending submission_requester: Aanvrager van inzending
specified_email: Opgegeven e-mail specified_email: Opgegeven e-mail
invite_by_name: Uitnodigen door %{name} invite_by_name: Uitnodigen door %{name}
invite_via_form_field: Uitnodigen via formulierveld
same_as_name: Zelfde als %{name} same_as_name: Zelfde als %{name}
default_email: Standaard e-mail default_email: Standaard e-mail
processing: Verwerken processing: Verwerken
@ -7173,6 +7282,13 @@ nl: &nl
if_you_didnt_request_this_you_can_ignore_this_email: "Als je dit niet hebt aangevraagd, kun je deze e-mail negeren." if_you_didnt_request_this_you_can_ignore_this_email: "Als je dit niet hebt aangevraagd, kun je deze e-mail negeren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Je wachtwoord wordt niet gewijzigd totdat je de bovenstaande link opent en een nieuw wachtwoord instelt." your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Je wachtwoord wordt niet gewijzigd totdat je de bovenstaande link opent en een nieuw wachtwoord instelt."
too_many_requests_try_again_later: Te veel verzoeken. Probeer het later opnieuw. too_many_requests_try_again_later: Te veel verzoeken. Probeer het later opnieuw.
bold: Vet
italic: Cursief
underline: Onderstreept
undo: Ongedaan maken
redo: Opnieuw
add_variable: Variabele toevoegen
enter_a_url_or_variable_name: Voer een URL of variabelenaam in
devise: devise:
confirmations: confirmations:
confirmed: Je e-mailadres is succesvol bevestigd. confirmed: Je e-mailadres is succesvol bevestigd.
@ -7289,6 +7405,16 @@ nl: &nl
events: events:
range_with_total: "%{from}-%{to} van %{count} gebeurtenissen" range_with_total: "%{from}-%{to} van %{count} gebeurtenissen"
range_without_total: "%{from}-%{to} gebeurtenissen" range_without_total: "%{from}-%{to} gebeurtenissen"
variables:
account_name: Accountnaam
submitter_link: Link van de ondertekenaar
template_name: Sjabloonnaam
submission_submitters: Lijst van ondertekenaars
submission_link: Link van de inzending
documents_link: Link van de documenten
time:
formats:
detailed: "%d %B %Y %H:%M:%S"
ar: ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@ -7586,12 +7712,18 @@ en-US:
date: date:
formats: formats:
default: "%m/%d/%Y" default: "%m/%d/%Y"
time:
formats:
detailed: "%B %d, %Y %I:%M:%S %p"
en-GB: en-GB:
<<: *en <<: *en
date: date:
formats: formats:
default: "%d/%m/%Y" default: "%d/%m/%Y"
time:
formats:
detailed: "%d %B, %Y %H:%M:%S"
es-ES: es-ES:
<<: *es <<: *es

@ -29,7 +29,12 @@ module LoadActiveStorageConfigs
service_configurations = ActiveSupport::ConfigurationFile.parse(STORAGE_YML_PATH) service_configurations = ActiveSupport::ConfigurationFile.parse(STORAGE_YML_PATH)
service_configurations[service].merge!(configs) if configs.present? service_configurations[service].merge!(configs) if configs.present?
service_configurations[service][:force_path_style] = true if configs&.dig('endpoint').present? if configs&.dig('endpoint').present?
service_configurations[service][:force_path_style] = true
if configs['endpoint'].include?('cloudflarestorage.com')
service_configurations[service][:request_checksum_calculation] = 'when_required'
end
end
if service == 'google' if service == 'google'
service_configurations[service][:credentials] = JSON.parse(configs.fetch('credentials', '{}')) service_configurations[service][:credentials] = JSON.parse(configs.fetch('credentials', '{}'))

@ -1,13 +1,132 @@
# frozen_string_literal: true # frozen_string_literal: true
module MarkdownToHtml module MarkdownToHtml
LINK_REGEXP = %r{\[([^\]]+)\]\((https?://[^)]+)\)} TAGS = {
'' => %w[<em> </em>],
'*' => %w[<strong> </strong>],
'~' => %w[<s> </s>]
}.freeze
INLINE_TOKENIZER = /(\[)|(\]\(([^)]+?)\))|(?:`([^`].*?)`)|(\*\*\*|\*\*|\*|~~)/
ALLOWED_TAGS = %w[p br strong b em i u a].freeze
ALLOWED_ATTRIBUTES = %w[href].freeze
module_function module_function
def call(text) def call(markdown)
text.gsub(LINK_REGEXP) do return '' if markdown.blank?
ApplicationController.helpers.link_to(Regexp.last_match(1), Regexp.last_match(2))
text = auto_link_urls(markdown)
html = render_markdown(text)
ActionController::Base.helpers.sanitize(html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)
end
BRACKETS = { ')' => '(', ']' => '[', '}' => '{' }.freeze
def auto_link_urls(text)
link_parts = text.split(%r{((?:https?://|www\.)[^\s<>\u00A0"]+)})
link_parts.map.with_index do |part, index|
if part.match?(%r{\A(?:https?://|www\.)}) && !(index > 0 && link_parts[index - 1]&.match?(/\]\(\s*\z/))
url_part = part.dup
punctuation = []
while url_part.sub!(%r{[^\p{Word}/\-=;]\z}, '')
punctuation.push(::Regexp.last_match(0))
opening = BRACKETS[punctuation.last]
next unless opening && url_part.count(opening) > url_part.count(punctuation.last)
url_part << punctuation.pop
break
end
trail = punctuation.reverse.join
url = url_part.start_with?('www.') ? "https://#{url_part}" : url_part
"[#{url_part}](#{url})#{trail}"
else
part
end
end.join
end
def render_markdown(text)
text = text.gsub(/\+\+([^+]+)\+\+/, '<u>\1</u>')
paragraphs = text.split(/\n{2,}/)
html = paragraphs.filter_map do |para|
content = para.strip
next if content.empty?
next '<p><br></p>' if ['&nbsp;', '&amp;nbsp;'].include?(content)
content = content.gsub(/ *\n/, '<br>')
"<p>#{parse_inline(content)}</p>"
end.join
html.presence || '<p></p>'
end
# rubocop:disable Metrics
def parse_inline(text)
context = []
out = ''
last = 0
tag = lambda do |t|
desc = TAGS[t[1] || '']
return t unless desc
is_end = context.last == t
is_end ? context.pop : context.push(t)
desc[is_end ? 1 : 0]
end end
flush = lambda do
str = ''
str += tag.call(context.last) while context.any?
str
end
while last <= text.length && (m = INLINE_TOKENIZER.match(text, last))
prev = text[last...m.begin(0)]
last = m.end(0)
chunk = m[0]
if m[4]
chunk = "<code>#{ERB::Util.html_escape(m[4])}</code>"
elsif m[2]
out = out.sub(/\A(.*)<a>/m, "\\1<a href=\"#{ERB::Util.html_escape(m[3])}\">")
out = out.gsub('<a>', '[')
chunk = "#{flush.call}</a>"
elsif m[1]
chunk = '<a>'
elsif m[5]
chunk =
if m[5] == '***'
if context.include?('*') && context.include?('**')
tag.call('*') + tag.call('**')
else
tag.call('**') + tag.call('*')
end
else
tag.call(m[5])
end
end
out += prev.to_s + chunk
end
(out + text[last..].to_s + flush.call).gsub('<a>', '[')
end end
# rubocop:enable Metrics
end end

@ -89,8 +89,10 @@ module ReplaceEmailVariables
# rubocop:enable Metrics # rubocop:enable Metrics
def build_documents_links_text(submitter, sig = nil) def build_documents_links_text(submitter, sig = nil)
url_options = build_url_options_for(submitter)
Rails.application.routes.url_helpers.submissions_preview_url( Rails.application.routes.url_helpers.submissions_preview_url(
submitter.submission.slug, { sig:, **Docuseal.default_url_options }.compact submitter.submission.slug, { sig:, **url_options }.compact
) )
end end
@ -139,14 +141,9 @@ module ReplaceEmailVariables
end end
def build_submitter_link(submitter, tracking_event_type) def build_submitter_link(submitter, tracking_event_type)
if tracking_event_type == 'click_email' url_options = build_url_options_for(submitter, is_email: tracking_event_type == 'click_email')
url_options =
if EMAIL_HOST.present?
{ host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
else
Docuseal.default_url_options
end
if tracking_event_type == 'click_email'
Rails.application.routes.url_helpers.submit_form_url( Rails.application.routes.url_helpers.submit_form_url(
slug: submitter.slug, slug: submitter.slug,
t: SubmissionEvents.build_tracking_param(submitter, 'click_email'), t: SubmissionEvents.build_tracking_param(submitter, 'click_email'),
@ -156,11 +153,22 @@ module ReplaceEmailVariables
Rails.application.routes.url_helpers.submit_form_url( Rails.application.routes.url_helpers.submit_form_url(
slug: submitter.slug, slug: submitter.slug,
c: SubmissionEvents.build_tracking_param(submitter, 'click_sms'), c: SubmissionEvents.build_tracking_param(submitter, 'click_sms'),
**Docuseal.default_url_options **url_options
) )
end end
end end
def build_url_options_for(submitter, is_email: true)
if Docuseal.multitenant? &&
(config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain))
{ host: config.value, protocol: 'https' }
elsif is_email && EMAIL_HOST.present?
{ host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
else
Docuseal.default_url_options
end
end
def build_submission_link(submission) def build_submission_link(submission)
Rails.application.routes.url_helpers.submission_url(submission, **Docuseal.default_url_options) Rails.application.routes.url_helpers.submission_url(submission, **Docuseal.default_url_options)
end end

@ -56,7 +56,9 @@ module Submissions
template_submitter = template_submitters.find { |e| e['uuid'] == uuid } template_submitter = template_submitters.find { |e| e['uuid'] == uuid }
end end
template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid') template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid',
'invite_via_field_uuid')
template_submitter['order'] = submitter_attrs['order'] if submitter_attrs['order'].present? template_submitter['order'] = submitter_attrs['order'] if submitter_attrs['order'].present?
submission.template_submitters << template_submitter submission.template_submitters << template_submitter
@ -113,7 +115,10 @@ module Submissions
item = item.merge('invite_by_uuid' => invite_by_uuid) if invite_by_uuid item = item.merge('invite_by_uuid' => invite_by_uuid) if invite_by_uuid
end end
next if item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? next if item['invite_by_uuid'].blank? &&
item['optional_invite_by_uuid'].blank? &&
item['invite_via_field_uuid'].blank?
next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] } next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] }
item = item.merge('order' => submitter_attr['order']) if submitter_attr && submitter_attr['order'].present? item = item.merge('order' => submitter_attr['order']) if submitter_attr && submitter_attr['order'].present?

@ -116,6 +116,7 @@ module Submissions
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY, configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_FILE_LINKS_KEY, AccountConfig::WITH_FILE_LINKS_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_AUDIT_SENDER_KEY, AccountConfig::WITH_AUDIT_SENDER_KEY,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY]) AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
@ -126,6 +127,7 @@ module Submissions
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
with_audit_sender = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SENDER_KEY }&.value == true with_audit_sender = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SENDER_KEY }&.value == true
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
timezone = account.timezone timezone = account.timezone
timezone = last_submitter.timezone || account.timezone if with_submitter_timezone timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
@ -489,8 +491,10 @@ module Submissions
end end
end end
time_format = with_timestamp_seconds ? :detailed : :long
[ [
"#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: :long, locale: account.locale)} " \ "#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: time_format, locale: account.locale)} " \
"#{TimeUtils.timezone_abbr(timezone, event.event_timestamp)}", "#{TimeUtils.timezone_abbr(timezone, event.event_timestamp)}",
composer.document.layout.formatted_text_box(text_box) composer.document.layout.formatted_text_box(text_box)
] ]

@ -15,6 +15,7 @@ module Submissions
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_FILE_LINKS_KEY, AccountConfig::WITH_FILE_LINKS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
@ -22,6 +23,7 @@ module Submissions
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false 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_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
with_signature_id_reason = with_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
@ -37,7 +39,7 @@ module Submissions
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index, GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
with_signature_id:, is_flatten:, with_headings: index.zero?, with_signature_id:, is_flatten:, with_headings: index.zero?,
with_submitter_timezone:, with_file_links:, with_submitter_timezone:, with_file_links:,
with_signature_id_reason:) with_signature_id_reason:, with_timestamp_seconds:)
end end
template = submission.template template = submission.template

@ -140,11 +140,13 @@ module Submissions
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_FILE_LINKS_KEY, AccountConfig::WITH_FILE_LINKS_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true 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 is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
with_signature_id_reason = with_signature_id_reason =
@ -195,11 +197,13 @@ module Submissions
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:, fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:,
with_submitter_timezone:, with_submitter_timezone:,
with_file_links:, with_file_links:,
with_timestamp_seconds:,
with_signature_id_reason:) with_signature_id_reason:)
end end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil, def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil) with_submitter_timezone: false, with_signature_id_reason: true,
with_timestamp_seconds: false, with_file_links: nil)
cell_layouters = Hash.new do |hash, valign| cell_layouters = Hash.new do |hash, valign|
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center) hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
end end
@ -320,13 +324,15 @@ module Submissions
timezone = submitter.account.timezone timezone = submitter.account.timezone
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
time_format = with_timestamp_seconds ? :detailed : :long
if with_signature_id_reason || field.dig('preferences', 'reasons').present? if with_signature_id_reason || field.dig('preferences', 'reasons').present?
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \ "#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \ "#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
else else
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
end end
end end
@ -569,7 +575,11 @@ module Submissions
fill_color:, fill_color:,
font_size:) font_size:)
line_height = layouter.fit([text], cell_width, height).lines.first.height line = layouter.fit([text], width, height).lines.first
line_height = line.height
cell_width = [line.width, cell_width].max
if preferences_font_size.blank? && line_height > (area['h'] * height) if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char, text = HexaPDF::Layout::TextFragment.create(char,

@ -15,6 +15,7 @@ module Submitters
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
@ -35,6 +36,7 @@ module Submitters
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == 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 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_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true
with_timestamp_seconds = find_safe_value(configs, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY) == true
with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false
with_field_labels = find_safe_value(configs, AccountConfig::WITH_FIELD_LABELS_KEY) != false with_field_labels = find_safe_value(configs, AccountConfig::WITH_FIELD_LABELS_KEY) != false
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY) policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
@ -43,7 +45,7 @@ module Submitters
reuse_signature:, with_decline:, with_partial_download:, reuse_signature:, with_decline:, with_partial_download:,
policy_links:, enforce_signing_order:, completed_message:, policy_links:, enforce_signing_order:, completed_message:,
require_signing_reason:, prefill_signature:, with_submitter_timezone:, require_signing_reason:, prefill_signature:, with_submitter_timezone:,
with_signature_id_reason:, with_signature_id:, with_field_labels: } with_signature_id_reason:, with_signature_id:, with_field_labels:, with_timestamp_seconds: }
keys.each do |key| keys.each do |key|
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value

@ -6,6 +6,7 @@ module Submitters
RequiredFieldError = Class.new(StandardError) RequiredFieldError = Class.new(StandardError)
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/ VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
PHONE_REGEXP = /[+\d()\s-]+/
NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze
STRFTIME_MAP = { STRFTIME_MAP = {
@ -45,14 +46,18 @@ module Submitters
assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true' assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true'
ApplicationRecord.transaction do ApplicationRecord.transaction do
maybe_set_signature_reason!(values, submitter, params) reason_field = maybe_set_signature_reason!(values, submitter, params)
validate_values!(values, submitter, params, request) validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request)
if (touch_attachment_uuid = params[:touch_attachment_uuid].presence) if (touch_attachment_uuid = params[:touch_attachment_uuid].presence)
ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at) ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at)
end end
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true' if params[:completed] == 'true'
maybe_invite_via_field(submitter, request)
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
end
submitter.save! submitter.save!
end end
@ -107,7 +112,9 @@ module Submitters
signature_field['preferences'] ||= {} signature_field['preferences'] ||= {}
signature_field['preferences']['reason_field_uuid'] = reason_field_uuid signature_field['preferences']['reason_field_uuid'] = reason_field_uuid
unless submitter.submission.template_fields.find { |e| e['uuid'] == reason_field_uuid } reason_field = submitter.submission.template_fields.find { |e| e['uuid'] == reason_field_uuid }
unless reason_field
reason_field = { 'type' => 'text', reason_field = { 'type' => 'text',
'uuid' => reason_field_uuid, 'uuid' => reason_field_uuid,
'name' => I18n.t(:reason), 'name' => I18n.t(:reason),
@ -119,6 +126,8 @@ module Submitters
end end
submitter.submission.save! submitter.submission.save!
reason_field
end end
def normalized_values(params) def normalized_values(params)
@ -403,7 +412,54 @@ module Submitters
end end
end end
def validate_value!(_value, _field, _params, _submitter, _request) def maybe_invite_via_field(submitter, request)
submission = submitter.submission
is_invited = false
submission.template_submitters.each do |s|
field_uuid = s['invite_via_field_uuid']
next if field_uuid.blank?
field = submission.template_fields.find { |e| e['uuid'] == field_uuid }
next unless field
next unless field['submitter_uuid'] == submitter.uuid
next if submission.submitters.exists?(uuid: s['uuid'])
value = submitter.values[field_uuid]
next if value.blank?
if value.include?('@')
email = Submissions.normalize_email(value)
elsif value.match?(PHONE_REGEXP)
phone = value.gsub(/[^+\d]/, '')
end
next if email.blank? && phone.blank?
submission.submitters.create!(uuid: s['uuid'], email:, phone:, account_id: submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
is_invited = true
end
submission.update!(submitters_order: :preserved) if is_invited
submitter
end
def validate_value!(_value, field, _params, submitter, _request)
if field['readonly'] == true
Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar)
raise ValidationError, 'Read-only field'
end
true true
end end
end end

@ -72,6 +72,7 @@ module Templates
def filter_undefined_submitters(template_submitters) def filter_undefined_submitters(template_submitters)
template_submitters.to_a.select do |item| template_submitters.to_a.select do |item|
item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? && item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? &&
item['invite_via_field_uuid'].blank? &&
item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank? item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank?
end end
end end

@ -4,7 +4,7 @@ module Templates
module Clone module Clone
module_function module_function
# rubocop:disable Metrics, Style/CombinableLoops # rubocop:disable Metrics
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil) def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
template = original_template.account.templates.new template = original_template.account.templates.new
@ -49,20 +49,6 @@ module Templates
submitter['uuid'] = new_submitter_uuid submitter['uuid'] = new_submitter_uuid
end end
cloned_submitters.each do |submitter|
if submitter['optional_invite_by_uuid'].present?
submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']]
end
if submitter['invite_by_uuid'].present?
submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']]
end
if submitter['linked_to_uuid'].present?
submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']]
end
end
cloned_preferences['submitters'].to_a.each do |submitter| cloned_preferences['submitters'].to_a.each do |submitter|
submitter['uuid'] = submitter_uuids_replacements[submitter['uuid']] submitter['uuid'] = submitter_uuids_replacements[submitter['uuid']]
end end
@ -97,8 +83,26 @@ module Templates
end end
end end
cloned_submitters.each do |submitter|
if submitter['optional_invite_by_uuid'].present?
submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']]
end
if submitter['invite_by_uuid'].present?
submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']]
end
if submitter['linked_to_uuid'].present?
submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']]
end
if submitter['invite_via_field_uuid'].present?
submitter['invite_via_field_uuid'] = field_uuids_replacements[submitter['invite_via_field_uuid']]
end
end
[cloned_submitters, cloned_fields, cloned_schema, cloned_preferences] [cloned_submitters, cloned_fields, cloned_schema, cloned_preferences]
end end
# rubocop:enable Metrics, Style/CombinableLoops # rubocop:enable Metrics
end end
end end

@ -18,6 +18,7 @@ module Templates
].freeze ].freeze
ANNOTATIONS_SIZE_LIMIT = 6.megabytes ANNOTATIONS_SIZE_LIMIT = 6.megabytes
MAX_ZIP_SIZE = 100.megabytes
InvalidFileType = Class.new(StandardError) InvalidFileType = Class.new(StandardError)
PdfEncrypted = Class.new(StandardError) PdfEncrypted = Class.new(StandardError)
@ -72,9 +73,15 @@ module Templates
Array.wrap(files).each do |file| Array.wrap(files).each do |file|
if file.content_type == ZIP_CONTENT_TYPE || file.content_type == X_ZIP_CONTENT_TYPE if file.content_type == ZIP_CONTENT_TYPE || file.content_type == X_ZIP_CONTENT_TYPE
total_size = 0
Zip::File.open(file.tempfile).each do |entry| Zip::File.open(file.tempfile).each do |entry|
next if entry.directory? next if entry.directory?
total_size += entry.size
raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE
tempfile = Tempfile.new(entry.name) tempfile = Tempfile.new(entry.name)
tempfile.binmode tempfile.binmode
entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) } entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) }

@ -14,6 +14,18 @@
"@hotwired/turbo-rails": "^7.3.0", "@hotwired/turbo-rails": "^7.3.0",
"@specious/htmlflow": "^1.1.0", "@specious/htmlflow": "^1.1.0",
"@tabler/icons-vue": "^2.47.0", "@tabler/icons-vue": "^2.47.0",
"@tiptap/core": "^3.19.0",
"@tiptap/extension-bold": "^3.19.0",
"@tiptap/extension-document": "^3.19.0",
"@tiptap/extension-hard-break": "^3.19.0",
"@tiptap/extension-italic": "^3.19.0",
"@tiptap/extension-link": "^3.19.0",
"@tiptap/extension-paragraph": "^3.19.0",
"@tiptap/extension-text": "^3.19.0",
"@tiptap/extension-underline": "^3.19.0",
"@tiptap/extensions": "^3.19.0",
"@tiptap/markdown": "^3.19.0",
"@tiptap/pm": "^3.19.0",
"autocompleter": "^9.1.0", "autocompleter": "^9.1.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",

@ -626,6 +626,34 @@ RSpec.describe 'Signing Form' do
end end
end end
context 'when the signature step with signing reason' do
let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
before do
create(:account_config, account:, key: AccountConfig::REQUIRE_SIGNING_REASON_KEY, value: true)
end
it 'completes the form with signing reason selected' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
draw_canvas
select 'Approved'
click_button 'Sign and Complete'
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Signature')).to be_present
end
end
context 'when the number step' do context 'when the number step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[number]) } let(:template) { create(:template, account:, author:, only_field_types: %w[number]) }
let(:submission) { create(:submission, template:) } let(:submission) { create(:submission, template:) }

@ -1743,6 +1743,11 @@
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41" resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41"
integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ== integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ==
"@remirror/core-constants@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
"@sinclair/typebox@^0.25.16": "@sinclair/typebox@^0.25.16":
version "0.25.24" version "0.25.24"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
@ -1767,6 +1772,89 @@
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.47.0.tgz#c41c680d1947e3ab2d60af3febc4132287c60596" resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.47.0.tgz#c41c680d1947e3ab2d60af3febc4132287c60596"
integrity sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA== integrity sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==
"@tiptap/core@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.19.0.tgz#dca483b50e1b8a596f695aecde387a79fe7da717"
integrity sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==
"@tiptap/extension-bold@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.19.0.tgz#ef0ddfd9b242ef9c25e3348aef9bf2dc681cdc19"
integrity sha512-UZgb1d0XK4J/JRIZ7jW+s4S6KjuEDT2z1PPM6ugcgofgJkWQvRZelCPbmtSFd3kwsD+zr9UPVgTh9YIuGQ8t+Q==
"@tiptap/extension-document@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.19.0.tgz#dfa6889cff748d489e0bc1028918bf4571372ba5"
integrity sha512-AOf0kHKSFO0ymjVgYSYDncRXTITdTcrj1tqxVazrmO60KNl1Rc2dAggDvIVTEBy5NvceF0scc7q3sE/5ZtVV7A==
"@tiptap/extension-hard-break@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8"
integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==
"@tiptap/extension-italic@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8"
integrity sha512-6GffxOnS/tWyCbDkirWNZITiXRta9wrCmrfa4rh+v32wfaOL1RRQNyqo9qN6Wjyl1R42Js+yXTzTTzZsOaLMYA==
"@tiptap/extension-link@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.19.0.tgz#e8e656735bda6ca1d4b6577821e06274ab0ff6c8"
integrity sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw==
dependencies:
linkifyjs "^4.3.2"
"@tiptap/extension-paragraph@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.19.0.tgz#91adde189aabf13a2bfbb2d961833d3bc2bc055f"
integrity sha512-xWa6gj82l5+AzdYyrSk9P4ynySaDzg/SlR1FarXE5yPXibYzpS95IWaVR0m2Qaz7Rrk+IiYOTGxGRxcHLOelNg==
"@tiptap/extension-text@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.19.0.tgz#353278c97bd8f5bdc29f06942fbd1e856bdb5b18"
integrity sha512-K95+SnbZy0h6hNFtfy23n8t/nOcTFEf69In9TSFVVmwn/Nwlke+IfiESAkqbt1/7sKJeegRXYO7WzFEmFl9Q/g==
"@tiptap/extension-underline@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.19.0.tgz#bbc81d085725981d256127ab416f91d0802ec2a4"
integrity sha512-800MGEWfG49j10wQzAFiW/ele1HT04MamcL8iyuPNu7ZbjbGN2yknvdrJlRy7hZlzIrVkZMr/1tz62KN33VHIw==
"@tiptap/extensions@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.19.0.tgz#5747c0ebf460b9669e8b4362561872448f66abfe"
integrity sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==
"@tiptap/markdown@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/markdown/-/markdown-3.19.0.tgz#dd05451b40f2a553cab0fdbb4a8714a2b2430b5c"
integrity sha512-Pnfacq2FHky1rqwmGwEmUJxuZu8VZ8XjaJIqsQC34S3CQWiOU+PukC9In2odzcooiVncLWT9s97jKuYpbmF1tQ==
dependencies:
marked "^17.0.1"
"@tiptap/pm@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.19.0.tgz#5cb499c7b2603ec6550d0c7a70b924f27fdb7692"
integrity sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
prosemirror-commands "^1.6.2"
prosemirror-dropcursor "^1.8.1"
prosemirror-gapcursor "^1.3.2"
prosemirror-history "^1.4.1"
prosemirror-inputrules "^1.4.0"
prosemirror-keymap "^1.2.2"
prosemirror-markdown "^1.13.1"
prosemirror-menu "^1.2.4"
prosemirror-model "^1.24.1"
prosemirror-schema-basic "^1.2.3"
prosemirror-schema-list "^1.5.0"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.6.4"
prosemirror-trailing-node "^3.0.0"
prosemirror-transform "^1.10.2"
prosemirror-view "^1.38.1"
"@trysound/sax@0.2.0": "@trysound/sax@0.2.0":
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
@ -1909,6 +1997,24 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/linkify-it@^5":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
"@types/markdown-it@^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/mdurl@^2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/mime@*": "@types/mime@*":
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
@ -2986,7 +3092,7 @@ cosmiconfig@^8.1.3:
parse-json "^5.0.0" parse-json "^5.0.0"
path-type "^4.0.0" path-type "^4.0.0"
crelt@^1.0.5, crelt@^1.0.6: crelt@^1.0.0, crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
@ -4780,6 +4886,18 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
linkify-it@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies:
uc.micro "^2.0.0"
linkifyjs@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
loader-runner@^4.3.1: loader-runner@^4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3"
@ -4867,6 +4985,23 @@ make-dir@^3.0.2:
dependencies: dependencies:
semver "^6.0.0" semver "^6.0.0"
markdown-it@^14.0.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
dependencies:
argparse "^2.0.1"
entities "^4.4.0"
linkify-it "^5.0.0"
mdurl "^2.0.0"
punycode.js "^2.3.1"
uc.micro "^2.1.0"
marked@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-17.0.1.tgz#9db34197ac145e5929572ee49ef701e37ee9b2e6"
integrity sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==
math-intrinsics@^1.1.0: math-intrinsics@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@ -4897,6 +5032,11 @@ mdn-data@2.0.30:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -5218,6 +5358,11 @@ optionator@^0.9.1:
type-check "^0.4.0" type-check "^0.4.0"
word-wrap "^1.2.3" word-wrap "^1.2.3"
orderedmap@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
p-limit@^2.2.0: p-limit@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@ -5739,6 +5884,160 @@ proper-lockfile@^4.1.2:
retry "^0.12.0" retry "^0.12.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
prosemirror-changeset@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz#eee3299cfabc7a027694e9abdc4e85505e9dd5e7"
integrity sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2:
version "1.7.1"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz#d101fef85618b1be53d5b99ea17bee5600781b38"
integrity sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.10.2"
prosemirror-dropcursor@^1.8.1:
version "1.8.2"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz#2ed30c4796109ddeb1cf7282372b3850528b7228"
integrity sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.3.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz#e1144a83b79db7ed0ec32cd0e915a0364220af43"
integrity sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.5.0.tgz#ee21fc5de85a1473e3e3752015ffd6d649a06859"
integrity sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==
dependencies:
prosemirror-state "^1.2.2"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.4.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz#d2e935f6086e3801486b09222638f61dae89a570"
integrity sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2, prosemirror-keymap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz#c0f6ab95f75c0b82c97e44eb6aaf29cbfc150472"
integrity sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.13.1:
version "1.13.4"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz#4620e6a0580cd52b5fc8e352c7e04830cd4b3048"
integrity sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==
dependencies:
"@types/markdown-it" "^14.0.0"
markdown-it "^14.0.0"
prosemirror-model "^1.25.0"
prosemirror-menu@^1.2.4:
version "1.2.5"
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz#dea00e7b623cea89f4d76963bee22d2ac2343250"
integrity sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==
dependencies:
crelt "^1.0.0"
prosemirror-commands "^1.0.0"
prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0, prosemirror-model@^1.25.4:
version "1.25.4"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.4.tgz#8ebfbe29ecbee9e5e2e4048c4fe8e363fcd56e7c"
integrity sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695"
integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==
dependencies:
prosemirror-model "^1.25.0"
prosemirror-schema-list@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5"
integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.7.3"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3, prosemirror-state@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.4.tgz#72b5e926f9e92dcee12b62a05fcc8a2de3bf5b39"
integrity sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.27.0"
prosemirror-tables@^1.6.4:
version "1.8.5"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz#104427012e5a5da1d2a38c122efee8d66bdd5104"
integrity sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==
dependencies:
prosemirror-keymap "^1.2.3"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-transform "^1.10.5"
prosemirror-view "^1.41.4"
prosemirror-trailing-node@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04"
integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==
dependencies:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
version "1.11.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz#f5c5050354423dc83c6b083f6f1959ec86a3f9ba"
integrity sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==
dependencies:
prosemirror-model "^1.21.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.41.4:
version "1.41.5"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.5.tgz#3e152d14af633f2f5a73aba24e6130c63f643b2b"
integrity sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==
dependencies:
prosemirror-model "^1.20.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
proxy-addr@~2.0.7: proxy-addr@~2.0.7:
version "2.0.7" version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -5752,6 +6051,11 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@^2.1.0: punycode@^2.1.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
@ -6010,6 +6314,11 @@ rollbar@^2.26.4:
optionalDependencies: optionalDependencies:
decache "^3.0.5" decache "^3.0.5"
rope-sequence@^1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
run-applescript@^7.0.0: run-applescript@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911"
@ -6792,6 +7101,11 @@ typed-function@^4.1.1:
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.1.1.tgz#38ce3cae31f4f513bcb263563fdad27b2afa73e8" resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.1.1.tgz#38ce3cae31f4f513bcb263563fdad27b2afa73e8"
integrity sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ== integrity sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
unbox-primitive@^1.0.2: unbox-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@ -6934,7 +7248,7 @@ vue@^3.3.2:
"@vue/server-renderer" "3.3.4" "@vue/server-renderer" "3.3.4"
"@vue/shared" "3.3.4" "@vue/shared" "3.3.4"
w3c-keyname@^2.2.4: w3c-keyname@^2.2.0, w3c-keyname@^2.2.4:
version "2.2.8" version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==

Loading…
Cancel
Save