Compare commits

...
27 Commits
Author SHA1 Message Date
Alex Turchyn 26749a4dbc Merge from docusealco/wip 2025-07-28 12:44:35 +03:00
Pete Matsyburka 14915d37b4 fix build 2025-07-28 09:02:17 +03:00
Pete Matsyburka 16d6d58efb fix typo 2025-07-27 10:42:56 +03:00
Pete Matsyburka b2b97a313a prefillable fields 2025-07-27 10:27:12 +03:00
Pete Matsyburka 2974e6a64f remove unused posthog 2025-07-25 09:41:24 +03:00
Pete Matsyburka dae34d0aa5 remove multiple signatures toggle 2025-07-24 15:13:39 +03:00
Alex Turchyn e9d0671184 remove duplicate translations 2025-07-24 14:39:28 +03:00
Alex Turchyn 897e33bf6a simplify shared links modal UI 2025-07-24 10:13:34 +03:00
Alex Turchyn f37e93a409 remove duplicate translations 2025-07-24 09:43:17 +03:00
Alex Turchyn 561ac63c93 email editor 2025-07-24 09:41:57 +03:00
Pete Matsyburka 149e4be07a do not send invitation if completed 2025-07-23 10:20:21 +03:00
Pete Matsyburka eb98dc9e06 adjust folder form 2025-07-22 15:18:48 +03:00
Pete Matsyburka ce7d965358 update 2025-07-22 10:24:40 +03:00
Pete Matsyburka bb73024859 folder name to string 2025-07-22 09:49:58 +03:00
Pete Matsyburka 631a442e2a fix builder file drop modal close 2025-07-21 16:43:03 +03:00
Pete Matsyburka 20a082ae8e adjust image field validation 2025-07-21 16:26:08 +03:00
Alex Turchyn 154d706be1 Merge from docusealco/wip 2025-07-21 13:38:05 +03:00
Alex Turchyn aaae257c75 group initial and signature images in audit logs 2025-07-20 19:56:10 +03:00
Pete Matsyburka ec944a2283 add subfolders 2025-07-20 08:28:19 +03:00
Pete Matsyburka 69d63ecffe with signature id reason 2025-07-18 11:08:22 +03:00
Pete Matsyburka 06a1e2992d fix redirect url 2025-07-17 18:33:24 +03:00
Pete Matsyburka ec9293bac0 with signature id 2025-07-17 10:54:44 +03:00
Pete Matsyburka 8bf727c43c adjust font auto 2025-07-15 09:19:49 +03:00
Pete Matsyburka 708b8afdf5 optimize 2025-07-14 17:13:04 +03:00
Pete Matsyburka 6f7b9b1ebf code highligh 2025-07-14 13:43:26 +03:00
Pete Matsyburka 4f48c4fd9f fix gh font 2025-07-14 13:36:34 +03:00
Pete Matsyburka 9dac90e6cc cleanup 2025-07-14 13:29:39 +03:00
93 changed files with 1376 additions and 358 deletions

No files matched your search

+18 -10
View File
@@ -1,9 +1,17 @@
name: Build Docker Images name: Build Docker Images
on: on:
push: workflow_dispatch:
tags: inputs:
- "*.*.*" version:
description: Version
type: string
required: true
image:
description: QEMU image
type: string
required: false
default: tonistiigi/binfmt:latest
jobs: jobs:
build: build:
@@ -16,23 +24,23 @@ jobs:
with: with:
submodules: recursive submodules: recursive
- - name: Docker meta
name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: | images: docuseal/docuseal
docuseal/docuseal tags: type=semver,pattern={{version}}
tags: |
type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with:
image: ${{ inputs.image }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Create .version file - name: Create .version file
run: echo ${{ github.ref_name }} > .version run: echo ${{ inputs.version }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
+8 -8
View File
@@ -319,7 +319,7 @@ GEM
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.9)
minitest (5.25.4) minitest (5.25.4)
msgpack (1.7.5) msgpack (1.7.5)
multi_json (1.15.0) multi_json (1.15.0)
@@ -338,18 +338,18 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.8) nokogiri (1.18.9)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-gnu) nokogiri (1.18.9-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-musl) nokogiri (1.18.9-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin) nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu) nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-musl) nokogiri (1.18.9-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.8) oj (3.16.8)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@@ -541,7 +541,7 @@ GEM
stringio (3.1.2) stringio (3.1.2)
strip_attributes (1.14.1) strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
thor (1.3.2) thor (1.4.0)
timeout (0.4.3) timeout (0.4.3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.11) turbo-rails (2.0.11)
@@ -11,8 +11,9 @@ module Api
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submitters = paginate( submitters = paginate(
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob, submitters.preload(template: { folder: :parent_folder },
combined_document_attachment: :blob }], submission: [:submitters, { audit_trail_attachment: :blob,
combined_document_attachment: :blob }],
documents_attachments: :blob, attachments_attachments: :blob), documents_attachments: :blob, attachments_attachments: :blob),
field: :completed_at field: :completed_at
) )
@@ -14,7 +14,7 @@ module Api
submissions = filter_submissions(submissions, params) submissions = filter_submissions(submissions, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: { folder: :parent_folder },
combined_document_attachment: :blob, combined_document_attachment: :blob,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
@@ -104,9 +104,10 @@ module Api
submissions = submissions.where(slug: params[:slug]) if params[:slug].present? submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present? if params[:template_folder].present?
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id) folders =
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids }) submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
end end
if params.key?(:archived) if params.key?(:archived)
+4 -4
View File
@@ -7,7 +7,7 @@ module Api
def index def index
templates = filter_templates(@templates, params) templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, :folder)) templates = paginate(templates.preload(:author, folder: :parent_folder))
schema_documents = schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id), ActiveStorage::Attachment.where(record_id: templates.map(&:id),
@@ -92,9 +92,9 @@ module Api
templates = templates.where(slug: params[:slug]) if params[:slug].present? templates = templates.where(slug: params[:slug]) if params[:slug].present?
if params[:folder].present? if params[:folder].present?
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id) folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
templates = templates.where(folder_id: folder_ids) templates = templates.where(folder_id: folders.pluck(:id))
end end
templates templates
@@ -109,7 +109,7 @@ module Api
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description, :prefillable,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
+10 -9
View File
@@ -32,10 +32,7 @@ class SubmissionsController < ApplicationController
def create def create
save_template_message(@template, params) if params[:save_message] == '1' save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1' [params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submissions = submissions =
if params[:emails].present? if params[:emails].present?
@@ -46,11 +43,16 @@ class SubmissionsController < ApplicationController
emails: params[:emails], emails: params[:emails],
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
else else
submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
Submissions.create_from_submitters(template: @template, Submissions.create_from_submitters(template: @template,
user: current_user, user: current_user,
source: :invite, source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
submissions_attrs: submissions_params[:submission].to_h.values, submissions_attrs:,
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end
@@ -62,9 +64,8 @@ class SubmissionsController < ApplicationController
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added') redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error, render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
partial: 'submissions/error', locals: { error: e.message }),
locals: { error: e.message }),
status: :unprocessable_entity status: :unprocessable_entity
end end
@@ -95,7 +96,7 @@ class SubmissionsController < ApplicationController
end end
def submissions_params def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end end
def load_template def load_template
@@ -3,14 +3,31 @@
class TemplateFoldersAutocompleteController < ApplicationController class TemplateFoldersAutocompleteController < ApplicationController
load_and_authorize_resource :template_folder, parent: false load_and_authorize_resource :template_folder, parent: false
LIMIT = 100 LIMIT = 30
def index def index
templates_query = Template.accessible_by(current_ability).where(archived_at: nil) parent_name, name =
if params[:parent_name].present?
[params[:parent_name], params[:q]]
else
params[:q].to_s.split(' /', 2).map(&:squish)
end
template_folders = @template_folders.where(id: templates_query.select(:folder_id)) if name
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
else
name = parent_name
end
render json: template_folders.as_json(only: %i[name archived_at]) template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
Template.accessible_by(current_ability))
name = name.to_s.downcase
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
render json: template_folders.preload(:parent_folder)
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
.as_json(only: %i[name archived_at], methods: %i[full_name])
end end
end end
+49 -6
View File
@@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController
helper_method :selected_order helper_method :selected_order
def show TEMPLATES_PER_PAGE = 12
@templates = @template_folder.templates.active.accessible_by(current_ability) FOLDERS_PER_PAGE = 18
.preload(:author, :template_accesses)
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, selected_order)
@pagy, @templates = pagy_auto(@templates, limit: 12) def show
@templates = Template.active.accessible_by(current_ability)
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
.preload(:author, :template_accesses)
@template_folders =
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
if @templates.exists?
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, selected_order)
limit =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy_auto(@templates, limit:)
load_related_submissions if params[:q].present? && @templates.blank?
else
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
@templates = @templates.none
end
end end
def edit; end def edit; end
@@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController
def template_folder_params def template_folder_params
params.require(:template_folder).permit(:name) params.require(:template_folder).permit(:name)
end end
def load_related_submissions
@related_submissions =
Submission.accessible_by(current_ability)
.where(archived_at: nil)
.where(template_id: current_account.templates.active
.where(folder: [@template_folder, *@template_folder.subfolders])
.select(:id))
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end end
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
def index def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) @templates = @templates.where.not(archived_at: nil)
.preload(:author, :template_accesses, folder: :parent_folder)
.order(id: :desc)
@templates = Templates.search(current_user, @templates, params[:q]) @templates = Templates.search(current_user, @templates, params[:q])
@pagy, @templates = pagy_auto(@templates, limit: 12) @pagy, @templates = pagy_auto(@templates, limit: 12)
return unless params[:q].present? && @templates.blank?
@related_submissions =
Submission.accessible_by(current_ability)
.joins(:template)
.where.not(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end end
end end
+1 -1
View File
@@ -120,7 +120,7 @@ class TemplatesController < ApplicationController
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]], submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description, :prefillable,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
@@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController
helper_method :selected_order helper_method :selected_order
def index def index
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)) @template_folders =
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
@template_folders = TemplateFolders.search(@template_folders, params[:q]) @template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = sort_template_folders(@template_folders, current_user, selected_order) @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
@@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController
Templates.search(current_user, rel, params[:q]) Templates.search(current_user, rel, params[:q])
end end
def sort_template_folders(template_folders, current_user, order)
case order
when 'used_at'
subquery =
Template.left_joins(:submissions)
.group(:folder_id)
.where(account_id: current_user.account_id)
.select(
:folder_id,
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
)
template_folders = template_folders.joins(
Template.arel_table
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.join_sources
)
template_folders.order(
Arel::Nodes::Case.new
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
.then(Template.arel_table[:submission_created_at_max])
.else(Template.arel_table[:updated_at_max])
.desc
)
when 'name'
template_folders.order(name: :asc)
else
template_folders.order(id: :desc)
end
end
def selected_order def selected_order
@selected_order ||= @selected_order ||=
if cookies.permanent[:dashboard_templates_order].blank? || if cookies.permanent[:dashboard_templates_order].blank? ||
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
def edit; end def edit; end
def update def update
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name]) name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
if @template.save if @template.save
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved')) redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
@@ -27,7 +27,7 @@ class TemplatesPreferencesController < ApplicationController
completed_redirect_url validate_unique_submitters completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration shared_link_2fa default_expire_at_duration shared_link_2fa
default_expire_at default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body], [completed_message: %i[title body],
@@ -0,0 +1,26 @@
# frozen_string_literal: true
class TemplatesPrefillableFieldsController < ApplicationController
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
load_and_authorize_resource :template
def create
authorize!(:update, @template)
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
if params[:prefillable] == 'false'
field.delete('prefillable')
field.delete('readonly')
elsif params[:prefillable] == 'true'
field['prefillable'] = true
field['readonly'] = true
end
@template.save!
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
locals: { template: @template })
end
end
+2
View File
@@ -37,6 +37,7 @@ import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone' 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 * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -109,6 +110,7 @@ safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick) 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('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
+28 -2
View File
@@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement {
static [target.static] = [ static [target.static] = [
'form', 'form',
'fileDropzone', 'fileDropzone',
'folderDropzone',
'fileDropzoneLoading' 'fileDropzoneLoading'
] ]
@@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement {
window.addEventListener('dragleave', this.onWindowDragleave) window.addEventListener('dragleave', this.onWindowDragleave)
this.fileDropzone?.addEventListener('drop', this.onDropFile) this.fileDropzone?.addEventListener('drop', this.onDropFile)
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el))) this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate)) this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart)) this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
el?.addEventListener('dragover', this.onDragover) el?.addEventListener('dragover', this.onDragover)
el?.addEventListener('dragleave', this.onDragleave) el?.addEventListener('dragleave', this.onDragleave)
}) })
@@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement {
onTemplateDragStart = (e) => { onTemplateDragStart = (e) => {
const id = e.target.href.split('/').pop() const id = e.target.href.split('/').pop()
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
this.folderDropzone?.classList?.remove('hidden')
window.flash?.remove()
e.dataTransfer.effectAllowed = 'move' e.dataTransfer.effectAllowed = 'move'
if (id) { if (id) {
@@ -104,7 +110,7 @@ export default targetable(class extends HTMLElement {
} else { } else {
const formData = new FormData() const formData = new FormData()
formData.append('name', el.innerText.trim()) formData.append('name', el.dataset.fullName)
fetch(`/templates/${templateId}/folder`, { fetch(`/templates/${templateId}/folder`, {
method: 'PUT', method: 'PUT',
@@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement {
} }
} }
onDropNewFolder (e) {
e.preventDefault()
const templateId = e.dataTransfer.getData('template_id')
const a = document.createElement('a')
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
a.dataset.turboFrame = 'modal'
a.classList.add('hidden')
document.body.append(a)
a.click()
a.remove()
}
onDragleave () { onDragleave () {
this.style.backgroundColor = null this.style.backgroundColor = null
@@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement {
this.isDrag = true this.isDrag = true
window.flash?.remove()
this.fileDropzone?.classList?.remove('hidden') this.fileDropzone?.classList?.remove('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' }) this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
@@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement {
this.isDrag = false this.isDrag = false
this.fileDropzone?.classList?.add('hidden') this.fileDropzone?.classList?.add('hidden')
this.folderDropzone?.classList?.add('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = null }) this.hiddenOnDrag.forEach((el) => { el.style.display = null })
+131
View File
@@ -0,0 +1,131 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
let loaderPromise = null
function loadCodeMirror () {
if (!loaderPromise) {
loaderPromise = Promise.all([
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
]).then(([view, commands, language, html, htmlflow]) => {
return {
minimalSetup: [
commands.history(),
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
],
EditorView: view.EditorView,
html: html.html,
htmlflow: htmlflow.default || htmlflow
}
})
}
return loaderPromise
}
export default targetable(class extends HTMLElement {
static [target.static] = [
'codeViewTab',
'previewViewTab',
'editorContainer',
'previewIframe'
]
connectedCallback () {
this.mount()
if (this.input.value) {
this.showPreviewView()
} else {
this.showCodeView()
}
this.previewViewTab.addEventListener('click', this.showPreviewView)
this.codeViewTab.addEventListener('click', this.showCodeView)
}
showCodeView = () => {
this.editorView.dispatch({
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
})
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
this.previewViewTab.classList.add('pb-[3px]')
this.codeViewTab.classList.remove('pb-[3px]')
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.remove('hidden')
this.previewIframe.classList.add('hidden')
}
showPreviewView = () => {
this.previewIframe.srcdoc = this.input.value
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
this.codeViewTab.classList.add('pb-[3px]')
this.previewViewTab.classList.remove('pb-[3px]')
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.add('hidden')
this.previewIframe.classList.remove('hidden')
}
async mount () {
this.input = this.querySelector('input[type="hidden"]')
this.input.style.display = 'none'
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
this.editorView = new EditorView({
doc: this.input.value,
parent: this.editorContainer,
extensions: [
html(),
minimalSetup,
EditorView.lineWrapping,
EditorView.updateListener.of(update => {
if (update.docChanged) this.input.value = update.state.doc.toString()
}),
EditorView.theme({
'&': {
backgroundColor: 'white',
color: 'black',
fontSize: '14px',
fontFamily: 'monospace'
},
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
borderRadius: '0.375rem',
border: 'none'
},
'.cm-gutters': {
display: 'none'
}
})
]
})
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
this.previewIframe.onload = () => {
const previewIframeDoc = this.previewIframe.contentDocument
if (previewIframeDoc.body) {
previewIframeDoc.body.contentEditable = true
}
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
contentDocument.body.addEventListener('input', async () => {
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
const prettifiedHtml = await htmlflow(html)
this.input.value = prettifiedHtml
})
}
}
})
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
if (this.dataset.enabled === 'false') return
autocomplete({ autocomplete({
input: this.input, input: this.input,
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1, preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
} }
onSelect = (item) => { onSelect = (item) => {
this.input.value = item.name this.input.value = this.dataset.parentName ? item.name : item.full_name
} }
fetch = (text, resolve) => { fetch = (text, resolve) => {
const queryParams = new URLSearchParams({ q: text }) const queryParams = new URLSearchParams({ q: text })
if (this.dataset.parentName) {
queryParams.append('parent_name', this.dataset.parentName)
}
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => { fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json() const items = await resp.json()
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
div.setAttribute('dir', 'auto') div.setAttribute('dir', 'auto')
div.textContent = item.name div.textContent = this.dataset.parentName ? item.name : item.full_name
return div return div
} }
+5 -7
View File
@@ -1,14 +1,12 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.image.addEventListener('load', (e) => { const image = this.querySelector('img')
this.image.setAttribute('width', e.target.naturalWidth)
this.image.setAttribute('height', e.target.naturalHeight) image.addEventListener('load', (e) => {
image.setAttribute('width', e.target.naturalWidth)
image.setAttribute('height', e.target.naturalHeight)
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}` this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
}) })
} }
get image () {
return this.querySelector('img')
}
} }
@@ -7,5 +7,9 @@ export default actionable(class extends HTMLElement {
elementIds.forEach((elementId) => { elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId) document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
}) })
if (this.dataset.focusId) {
document.getElementById(this.dataset.focusId)?.focus()
}
} }
}) })
+10 -6
View File
@@ -56,11 +56,11 @@
<div <div
v-else-if="field.type === 'signature' && signature" v-else-if="field.type === 'signature' && signature"
class="flex justify-between h-full gap-1 overflow-hidden w-full" class="flex justify-between h-full gap-1 overflow-hidden w-full"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'" :class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
> >
<div <div
class="flex overflow-hidden" class="flex overflow-hidden"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'" :class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
style="min-height: 50%" style="min-height: 50%"
> >
<img <img
@@ -69,7 +69,7 @@
> >
</div> </div>
<div <div
v-if="withSignatureId || field.preferences?.reason_field_uuid" v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]" class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
:class="isNarrow ? 'w-1/2' : 'w-full'" :class="isNarrow ? 'w-1/2' : 'w-full'"
> >
@@ -333,6 +333,13 @@ export default {
verification: this.t('verify_id') verification: this.t('verify_id')
} }
}, },
isShowSignatureId () {
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
return this.field.preferences.with_signature_id
} else {
return this.withSignatureId
}
},
alignClasses () { alignClasses () {
if (!this.field.preferences) { if (!this.field.preferences) {
return { 'items-center': true } return { 'items-center': true }
@@ -460,9 +467,6 @@ export default {
fontScale () { fontScale () {
return 1000 / 612.0 return 1000 / 612.0
}, },
ladscapeScale () {
return 8.5 / 11.0
},
computedStyle () { computedStyle () {
const { x, y, w, h } = this.area const { x, y, w, h } = this.area
+15 -3
View File
@@ -112,11 +112,23 @@ export default {
onSelectFiles (e) { onSelectFiles (e) {
e.preventDefault() e.preventDefault()
this.uploadFiles(this.$refs.input.files).then(() => { const files = Array.from(this.$refs.input.files).filter((f) => {
if (this.$refs.input) { if (this.accept === 'image/*') {
this.$refs.input.value = '' return f.type.startsWith('image')
} else {
return true
} }
}) })
if (this.accept === 'image/*' && !files.length) {
alert(this.t('please_upload_an_image_file'))
} else {
this.uploadFiles(files).then(() => {
if (this.$refs.input) {
this.$refs.input.value = ''
}
})
}
}, },
async uploadFiles (files) { async uploadFiles (files) {
this.isLoading = true this.isLoading = true
@@ -149,7 +149,8 @@ export default {
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, { return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug submitter_slug: this.submitterSlug,
redirect_url: document.location.href
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {
+12 -3
View File
@@ -140,6 +140,8 @@
:background-color="'white'" :background-color="'white'"
:with-required="false" :with-required="false"
:with-areas="false" :with-areas="false"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true" @click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@@ -347,6 +349,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -414,9 +426,6 @@ export default {
fontScale () { fontScale () {
return 1040 / 612.0 return 1040 / 612.0
}, },
ladscapeScale () {
return 8.5 / 11.0
},
isDefaultValuePresent () { isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) { if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
return false return false
@@ -330,6 +330,8 @@
:input-mode="inputMode" :input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields || drawField" :allow-draw="!onlyDefinedFields || drawField"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:data-document-uuid="document.uuid" :data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:drag-field-placeholder="fieldsDragFieldRef.value || dragField" :drag-field-placeholder="fieldsDragFieldRef.value || dragField"
@@ -436,6 +438,8 @@
:default-required-fields="defaultRequiredFields" :default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:only-defined-fields="onlyDefinedFields" :only-defined-fields="onlyDefinedFields"
:editable="editable" :editable="editable"
:show-tour-start-form="showTourStartForm" :show-tour-start-form="showTourStartForm"
@@ -563,6 +567,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
backgroundColor: { backgroundColor: {
type: String, type: String,
required: false, required: false,
@@ -794,6 +803,13 @@ export default {
language () { language () {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
}, },
withPrefillable () {
if (this.template.fields) {
return this.template.fields.some((f) => f.prefillable)
} else {
return false
}
},
isInlineSize () { isInlineSize () {
return CSS.supports('container-type: size') return CSS.supports('container-type: size')
}, },
@@ -1068,6 +1084,11 @@ export default {
} }
} }
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
field.preferences ||= {}
field.preferences.with_signature_id = this.withSignatureId
}
this.template.fields.push(field) this.template.fields.push(field)
this.save() this.save()
@@ -1474,6 +1495,11 @@ export default {
field.preferences ||= {} field.preferences ||= {}
field.preferences.format ||= this.defaultDateFormat field.preferences.format ||= this.defaultDateFormat
} }
if (field.type === 'signature' && [true, false].includes(this.withSignatureId)) {
field.preferences ||= {}
field.preferences.with_signature_id = this.withSignatureId
}
} }
const fieldArea = { const fieldArea = {
@@ -10,6 +10,8 @@
:data-page="index" :data-page="index"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw" :allow-draw="allowDraw"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:is-drag="isDrag" :is-drag="isDrag"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:default-fields="defaultFields" :default-fields="defaultFields"
@@ -66,6 +68,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
drawFieldType: { drawFieldType: {
type: String, type: String,
required: false, required: false,
+12
View File
@@ -125,6 +125,8 @@
:field="field" :field="field"
:default-field="defaultField" :default-field="defaultField"
:editable="editable" :editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor" :background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true" @click-font="isShowFontModal = true"
@@ -302,6 +304,16 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withOptions: { withOptions: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -320,6 +320,21 @@
{{ t('format') }} {{ t('format') }}
</label> </label>
</div> </div>
<li
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
:checked="field.preferences?.with_signature_id"
type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]"
>
<span class="label-text">{{ t('signature_id') }}</span>
</label>
</li>
<li <li
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'" v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop @click.stop
@@ -391,6 +406,21 @@
<span class="label-text">{{ t('read_only') }}</span> <span class="label-text">{{ t('read_only') }}</span>
</label> </label>
</li> </li>
<li
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.prefillable"
type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('prefillable') }}</span>
</label>
</li>
<hr <hr
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5" class="pb-0.5 mt-0.5"
@@ -525,6 +555,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
backgroundColor: { backgroundColor: {
type: String, type: String,
required: false, required: false,
@@ -535,6 +570,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withRequired: { withRequired: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -26,6 +26,8 @@
:field="field" :field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)" :type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable" :editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[field.name]" :default-field="defaultFieldsIndex[field.name]"
:draggable="editable" :draggable="editable"
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]" @dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@@ -253,6 +255,16 @@ export default {
required: false, required: false,
default: null default: null
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
template: { template: {
type: Object, type: Object,
required: true required: true
@@ -163,7 +163,7 @@
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview" class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
:style="{ :style="{
color: preferences.color || 'black', color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt', fontSize: (preferences.font_size || 11) + 'pt',
}" }"
:class="textClasses" :class="textClasses"
> >
@@ -2,6 +2,8 @@
<div <div
v-if="isDragging || isLoading" v-if="isDragging || isLoading"
class="modal modal-open" class="modal modal-open"
@dragover.prevent
@drop.prevent="$emit('error')"
> >
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full"> <div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
<Dropzone <Dropzone
+12
View File
@@ -1,4 +1,6 @@
const en = { const en = {
prefillable: 'Prefillable',
signature_id: 'Signature ID',
error_message: 'Error message', error_message: 'Error message',
length: 'Length', length: 'Length',
min: 'Min', min: 'Min',
@@ -172,6 +174,8 @@ const en = {
} }
const es = { const es = {
prefillable: 'Rellenable',
signature_id: 'ID de Firma',
error_message: 'Mensaje de error', error_message: 'Mensaje de error',
length: 'Longitud', length: 'Longitud',
min: 'Mín', min: 'Mín',
@@ -345,6 +349,8 @@ const es = {
} }
const it = { const it = {
prefillable: 'Precompilabile',
signature_id: 'ID firma',
error_message: 'Messaggio di errore', error_message: 'Messaggio di errore',
length: 'Lunghezza', length: 'Lunghezza',
min: 'Min', min: 'Min',
@@ -518,6 +524,8 @@ const it = {
} }
const pt = { const pt = {
prefillable: 'Pré-preenchível',
signature_id: 'ID da Assinatura',
error_message: 'Mensagem de erro', error_message: 'Mensagem de erro',
length: 'Comprimento', length: 'Comprimento',
min: 'Mín', min: 'Mín',
@@ -691,6 +699,8 @@ const pt = {
} }
const fr = { const fr = {
prefillable: 'Pré-remplissable',
signature_id: 'ID de signature',
error_message: 'Message d\'erreur', error_message: 'Message d\'erreur',
length: 'Longueur', length: 'Longueur',
min: 'Min', min: 'Min',
@@ -864,6 +874,8 @@ const fr = {
} }
const de = { const de = {
prefillable: 'Vorausfüllbar',
signature_id: 'Signatur-ID',
error_message: 'Fehlermeldung', error_message: 'Fehlermeldung',
length: 'Länge', length: 'Länge',
min: 'Min', min: 'Min',
+12
View File
@@ -27,6 +27,8 @@
:field="item.field" :field="item.field"
:editable="editable" :editable="editable"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[item.field.name]" :default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:max-page="totalPages - 1" :max-page="totalPages - 1"
@@ -78,6 +80,16 @@ export default {
required: false, required: false,
default: null default: null
}, },
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
areas: { areas: {
type: Array, type: Array,
required: false, required: false,
@@ -6,6 +6,7 @@ class SendSubmitterInvitationEmailJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
return if submitter.completed_at?
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true) return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account) unless Accounts.can_send_invitation_emails?(submitter.account)
+7 -2
View File
@@ -30,6 +30,7 @@ class SubmitterMailer < ApplicationMailer
@submitter.template&.preferences&.dig('request_email_subject').presence @submitter.template&.preferences&.dig('request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_invitation', @submitter) assign_message_metadata('submitter_invitation', @submitter)
@@ -71,7 +72,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config @subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['completed_notification_email_body'].presence @body = template_preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config @body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_completed', @submitter) assign_message_metadata('submitter_completed', @submitter)
@@ -127,7 +128,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config @subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['documents_copy_email_body'].presence @body = template_preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config @body ||= fetch_config_email_body(@email_config, @submitter)
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)
@@ -246,4 +247,8 @@ class SubmitterMailer < ApplicationMailer
user.friendly_name user.friendly_name
end end
end end
def fetch_config_email_body(email_config, _submitter = nil)
email_config ? email_config.value['body'].presence : nil
end
end end
+1
View File
@@ -41,6 +41,7 @@ class AccountConfig < ApplicationRecord
FORCE_SSO_AUTH_KEY = 'force_sso_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
+4 -2
View File
@@ -72,12 +72,14 @@ class Template < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
delegate :name, to: :folder, prefix: true
def application_key def application_key
external_id external_id
end end
def folder_name
folder.full_name
end
private private
def maybe_set_default_folder def maybe_set_default_folder
+23 -9
View File
@@ -4,36 +4,50 @@
# #
# Table name: template_folders # Table name: template_folders
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# archived_at :datetime # archived_at :datetime
# name :string not null # name :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null # account_id :bigint not null
# author_id :bigint not null # author_id :bigint not null
# parent_folder_id :bigint
# #
# Indexes # Indexes
# #
# index_template_folders_on_account_id (account_id) # index_template_folders_on_account_id (account_id)
# index_template_folders_on_author_id (author_id) # index_template_folders_on_author_id (author_id)
# index_template_folders_on_parent_folder_id (parent_folder_id)
# #
# Foreign Keys # Foreign Keys
# #
# fk_rails_... (account_id => accounts.id) # fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id) # fk_rails_... (author_id => users.id)
# fk_rails_... (parent_folder_id => template_folders.id)
# #
class TemplateFolder < ApplicationRecord class TemplateFolder < ApplicationRecord
DEFAULT_NAME = 'Default' DEFAULT_NAME = 'Default'
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
belongs_to :account belongs_to :account
belongs_to :parent_folder, class_name: 'TemplateFolder', optional: true
has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
has_many :subfolders, class_name: 'TemplateFolder', foreign_key: :parent_folder_id, inverse_of: :parent_folder,
dependent: :destroy
has_many :active_templates, -> { where(archived_at: nil) }, has_many :active_templates, -> { where(archived_at: nil) },
class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
def full_name
if parent_folder_id?
[parent_folder.name, name].join(' / ')
else
name
end
end
def default? def default?
name == DEFAULT_NAME name == DEFAULT_NAME
end end
+3 -3
View File
@@ -53,7 +53,7 @@
<span class="top-0 right-0 absolute"> <span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> <%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span> </span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre> <pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
</div> </div>
</div> </div>
</div> </div>
@@ -79,7 +79,7 @@
<span class="top-0 right-0 absolute"> <span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> <%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span> </span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre> <pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
</div> </div>
</div> </div>
</div> </div>
@@ -101,7 +101,7 @@
<span class="top-0 right-0 absolute"> <span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> <%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span> </span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre> <pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -141,7 +141,7 @@
<%= t('preferences') %> <%= t('preferences') %>
</h2> </h2>
</div> </div>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %> <% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || account_config.persisted?) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5"> <div class="flex items-center justify-between py-2.5">
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" />
</svg>

After

Width:  |  Height:  |  Size: 351 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18" /><path d="M8 12l4 4" /><path d="M8 12h8" /><path d="M12 8l-4 4" />
</svg>

After

Width:  |  Height:  |  Size: 398 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18" /><path d="M9 12l4 4" /><path d="M13 8l-4 4" />
</svg>

After

Width:  |  Height:  |  Size: 378 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 19h-7a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v3.5" /><path d="M16 19h6" /><path d="M19 16v6" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M17 5l-10 14" />
</svg>

After

Width:  |  Height:  |  Size: 308 B

-1
View File
@@ -18,7 +18,6 @@
<link href="<%= canonical_url %>" rel="canonical"> <link href="<%= canonical_url %>" rel="canonical">
<% end %> <% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %> <%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %> <%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
</head> </head>
<body> <body>
-1
View File
@@ -12,7 +12,6 @@
<%= javascript_pack_tag 'form', defer: true %> <%= javascript_pack_tag 'form', defer: true %>
<% end %> <% end %>
<%= stylesheet_pack_tag 'form', media: 'all' %> <%= stylesheet_pack_tag 'form', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head> </head>
<body> <body>
<%= yield %> <%= yield %>
-1
View File
@@ -12,7 +12,6 @@
<%= javascript_pack_tag 'application', defer: true %> <%= javascript_pack_tag 'application', defer: true %>
<% end %> <% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %> <%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head> </head>
<body> <body>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <% if flash.present? %><%= render 'shared/flash' %><% end %>
@@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %> <% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control"> <div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %> <%= ff.label :reply_to, t('reply_to'), class: 'label' %>
@@ -0,0 +1,11 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>">
<%= 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>
@@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" 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, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
@@ -18,17 +18,7 @@
</div> </div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 mx-1"> <div class="flex items-center justify-between pt-2.5 mx-1">
<span> <span>
<%= t('attach_documents') %> <%= t('attach_documents') %>
+1 -1
View File
@@ -1,5 +1,5 @@
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub"> <a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-extrabold"> <span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %> <%= svg_icon('start', class: 'h-3 w-3') %>
<span>9k</span> <span>9k</span>
</span> </span>
-4
View File
@@ -1,4 +0,0 @@
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<%= ENV.fetch('POSTHOG_TOKEN', nil) %>',{api_host:'https://app.posthog.com',autocapture: false,enable_recording_console_log: true,capture_pageview: false,session_recording:{maskAllInputs: false,maskInputOptions: {password: true}}})
</script>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="block w-full md:w-52 flex-none"> <div class="block w-full md:w-52 flex-none">
<menu-active> <menu-active>
<ul id="account_settings_menu" class="menu px-0"> <ul id="account_settings_menu" class="menu px-0">
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">&larr; <%= t('back') %></a></li> <li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5"><%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %><span><%= t('back') %></span></a></li>
<li class="menu-title py-0 !bg-transparent"> <li class="menu-title py-0 !bg-transparent">
<span class="!bg-transparent"><%= t('settings') %></span> <span class="!bg-transparent"><%= t('settings') %></span>
</li> </li>
+51 -15
View File
@@ -1,3 +1,4 @@
<% 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? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="space-y-4">
@@ -11,6 +12,7 @@
</div> </div>
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4"> <div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
<% submitters.each_with_index do |item, index| %> <% submitters.each_with_index do |item, index| %>
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %>
<submitter-item class="form-control"> <submitter-item class="form-control">
<% if submitters.size > 1 %> <% if submitters.size > 1 %>
<label class="label pt-0 pb-1"> <label class="label pt-0 pb-1">
@@ -18,30 +20,62 @@
</label> </label>
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name"> <% if prefillable_fields.blank? %>
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <submitters-autocomplete data-field="name">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %> <linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
</linked-input> <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
</submitters-autocomplete> </linked-input>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>"> </submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
</linked-input>
</submitters-autocomplete>
<% has_phone_field = true %>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input>
</submitters-autocomplete>
</div>
<% end %>
<% if prefillable_fields.present? %>
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>"> <input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 w-full" placeholder="<%= t('email') %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>" required>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
<submitters-autocomplete data-field="phone"> <% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %>
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <% has_phone_field = true %>
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %> <submitters-autocomplete data-field="phone">
</linked-input> <linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
</submitters-autocomplete> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
</div> </linked-input>
</submitters-autocomplete>
<% end %>
<% prefillable_fields.each do |field| %>
<% if field['type'] == 'checkbox' %>
<label for="detailed_field_<%= field['uuid'] %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid']}]", id: "detailed_field_#{field['uuid']}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
</label>
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
<%= select_tag "submission[1][submitters][][values][#{field['uuid']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{field['uuid']}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
<% elsif field['type'] == 'date' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
<% elsif field['type'] != 'phone' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
<% end %>
<% end %>
<% end %>
</submitter-item> </submitter-item>
<% end %> <% end %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<% if params[:selfsign].blank? %> <% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem"> <a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span> <span><%= t('add_new') %></span>
@@ -51,7 +85,9 @@
<div> <div>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %> <%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
<%= render 'send_email', f:, template: %> <%= render 'send_email', f:, template: %>
<%= render 'send_sms', f: %> <% if has_phone_field %>
<%= render 'send_sms', f: %>
<% end %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
+1 -1
View File
@@ -4,7 +4,7 @@
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %> <%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] %> <%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span> <span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %> <% end %>
<div> <div>
+11 -8
View File
@@ -16,15 +16,18 @@
<div class="truncate uppercase"> <div class="truncate uppercase">
ID: <%= attachment.uuid %> ID: <%= attachment.uuid %>
</div> </div>
<% if local_assigns[:with_signature_id_reason] != false %>
<div>
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %>
<% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %>
<% if submitter.email %>
&lt;<%= submitter.email %>&gt;
<% end %>
</div>
<% end %>
<div> <div>
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %> <% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
<% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %> <%= l(attachment.created_at.in_time_zone(timezone), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
<% if submitter.email %>
&lt;<%= submitter.email %>&gt;
<% end %>
</div>
<div>
<%= l(attachment.created_at.in_time_zone(local_assigns[:timezone]), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(local_assigns[:timezone], attachment.created_at) %>
</div> </div>
</div> </div>
<% end %> <% end %>
+7 -5
View File
@@ -1,11 +1,13 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %> <% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% only_detailed = require_phone_2fa || prefillable_fields.present? %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %> <%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %> <% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
<div class="flex justify-center"> <div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<div> <div>
<%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>"> <label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
<%= label %> <%= label %>
</label> </label>
@@ -14,7 +16,7 @@
</div> </div>
</toggle-visible> </toggle-visible>
<div class="px-5 mb-5 mt-4"> <div class="px-5 mb-5 mt-4">
<% unless require_phone_2fa %> <% unless only_detailed %>
<div id="email"> <div id="email">
<%= render 'email_form', template: @template %> <%= render 'email_form', template: @template %>
</div> </div>
@@ -22,8 +24,8 @@
<%= render 'phone_form', template: @template %> <%= render 'phone_form', template: @template %>
</div> </div>
<% end %> <% end %>
<div id="detailed" class="<%= 'hidden' unless require_phone_2fa %>"> <div id="detailed" class="<%= 'hidden' unless only_detailed %>">
<%= render 'detailed_form', template: @template, require_phone_2fa: %> <%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
</div> </div>
<div id="list" class="hidden"> <div id="list" class="hidden">
<%= render 'list_form', template: @template %> <%= render 'list_form', template: @template %>
+9 -4
View File
@@ -2,7 +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 %>
<% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %> <% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %>
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true %>
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
<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">
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1"> <a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
@@ -108,17 +112,18 @@
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %> <% next if value.blank? %>
<% submitter = submitters_index[field['submitter_uuid']] %>
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
<span class="group"> <span class="group">
<span class="hidden group-hover:inline"> <span class="hidden group-hover:inline">
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
</span> </span>
<span class="group-hover:hidden"> <span class="group-hover:hidden">
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
</span> </span>
</span> </span>
<% else %> <% else %>
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
@@ -1,14 +1,14 @@
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<div> <div>
<%= link_to root_path do %> <%= link_to root_path, class: 'flex items-center' do %>
&larr; <%= svg_icon('chevron_left', class: 'w-5 h-5') %>
<span><%= t('back_to_active') %></span> <span style="margin-left: 3px"><%= t('back_to_active') %></span>
<% end %> <% end %>
</div> </div>
<div class="flex flex-col md:flex-row md:items-center mb-4 gap-3"> <div class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between items-center">
<div> <div>
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1> <h1 class="text-2xl md:text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
</div> </div>
<div> <div>
<% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %> <% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
@@ -10,6 +10,17 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if params[:folder].present? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('folder') %>">
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
<span class="font-normal truncate"><%= params[:folder] %></span>
<% end %>
<%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
<% end %>
<% if params[:author].present? %> <% if params[:author].present? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>"> <div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>">
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
+1 -1
View File
@@ -84,7 +84,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', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
<% end %> <% end %>
</div> </div>
</page-container> </page-container>
@@ -12,7 +12,6 @@
<%= javascript_pack_tag 'draw', defer: true %> <%= javascript_pack_tag 'draw', defer: true %>
<% end %> <% end %>
<%= stylesheet_pack_tag 'form', media: 'all' %> <%= stylesheet_pack_tag 'form', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head> </head>
<body> <body>
<% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %> <% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>
@@ -1,14 +1,13 @@
<% if @email_config || @body.present? %> <% if @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %> <%= 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')) %></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: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></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 %>
</p> </p>
+1 -1
View File
@@ -1,5 +1,5 @@
<% is_long = folder.name.size > 32 %> <% is_long = folder.name.size > 32 %>
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards"> <a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards" data-full-name="<%= folder.full_name %>">
<% if !is_long %> <% if !is_long %>
<%= svg_icon('folder', class: 'w-6 h-6') %> <%= svg_icon('folder', class: 'w-6 h-6') %>
<% end %> <% end %>
+26 -9
View File
@@ -1,24 +1,27 @@
<div> <div>
<%= link_to root_path do %> <%= link_to @template_folder.parent_folder ? folder_path(@template_folder.parent_folder) : root_path, class: 'flex items-center' do %>
&larr; <%= svg_icon('chevron_left', class: 'w-5 h-5') %>
<span><%= t('home') %></span> <span style="margin-left: 3px"><%= @template_folder.parent_folder&.name || t('home') %></span>
<% end %> <% end %>
</div> </div>
<dashboard-dropzone> <dashboard-dropzone>
<div class="relative flex justify-between items-center w-full mb-4"> <div class="relative flex justify-between items-center w-full mb-4">
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %> <%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
<input name="form_id" value="<%= form_id %>"> <input name="form_id" value="<%= form_id %>">
<input name="folder_name" value="<%= @template_folder.name %>"> <input name="folder_name" value="<%= @template_folder.full_name %>">
<button type="submit"></button> <button type="submit"></button>
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple> <input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
<% end %> <% end %>
<%= render 'templates/dashboard_dropzone', style: 'height: 137px' %> <%= render 'templates/dashboard_dropzone', style: 'height: 137px' %>
<% unless @template_folder.parent_folder %>
<%= render 'templates/dashboard_folder_dropzone', style: 'height: 137px' %>
<% end %>
<h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>"> <h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %> <%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
<span class="peer truncate"> <span class="peer truncate">
<%= @template_folder.name %> <%= @template_folder.name %>
</span> </span>
<% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %> <% if can?(:update, @template_folder) && @template_folder.full_name != TemplateFolder::DEFAULT_NAME %>
<span class="opacity-0 hover:opacity-100 peer-hover:opacity-100"> <span class="opacity-0 hover:opacity-100 peer-hover:opacity-100">
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal"> <a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
<%= svg_icon('pencil', class: 'w-7 h-7') %> <%= svg_icon('pencil', class: 'w-7 h-7') %>
@@ -27,18 +30,23 @@
<% end %> <% end %>
</h1> </h1>
<div class="flex space-x-2"> <div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 %> <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %> <%= render 'shared/search_input' %>
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% if can?(:create, ::Template) %>
<%= render 'templates/upload_button', folder_name: @template_folder.name %> <%= render 'templates/upload_button', folder_name: @template_folder.full_name %>
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %> <%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block"><%= t('create') %></span> <span class="hidden md:block"><%= t('create') %></span>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
</div> </div>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% end %>
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
@@ -48,12 +56,21 @@
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %> <%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
<% end %> <% end %>
<% end %> <% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %> <%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %>
<% elsif params[:q].present? %> <% elsif params[:q].present? %>
<div class="text-center"> <div class="text-center">
<div class="mt-16 text-3xl font-semibold"> <div class="mt-16 text-3xl font-semibold">
<%= t('templates_not_found') %> <%= t('templates_not_found') %>
</div> </div>
</div> </div>
<% if @related_submissions.present? %>
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
<%= t('submissions') %>
</h1>
<div class="space-y-4 mt-4">
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
</div>
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q], folder: @template_folder.full_name) %>
<% end %>
<% end %> <% end %>
</dashboard-dropzone> </dashboard-dropzone>
@@ -0,0 +1,20 @@
<div class="absolute bottom-0 w-full cursor-pointer rounded-xl bg-base-100 border-2 border-base-300 border-dashed hidden z-50" data-target="dashboard-dropzone.folderDropzone" style="<%= local_assigns[:style] %>">
<div class="absolute top-0 right-0 left-0 bottom-0 flex justify-center p-2 items-center pointer-events-none">
<div class="flex flex-col items-center text-center" data-target="dashboard-dropzone.toggleLoading">
<span class="flex flex-col items-center">
<span>
<%= svg_icon('folder_plus', class: 'w-9 h-9') %>
</span>
<div class="font-medium mb-1">
<%= t('create_a_new_folder') %>
</div>
</span>
<span class="flex flex-col items-center hidden" data-target="dashboard-dropzone.fileDropzoneLoading">
<%= svg_icon('loader', class: 'w-9 h-9 animate-spin') %>
<div class="font-medium mb-1">
<%= t('loading') %>...
</div>
</span>
</div>
</div>
</div>
+2 -2
View File
@@ -1,6 +1,6 @@
<div class="h-36 relative group"> <div class="h-36 relative group">
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl" data-targets="dashboard-dropzone.templateCards"> <a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl" data-targets="dashboard-dropzone.templateCards">
<div class="text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <div class="text-xl font-semibold" style="line-height: 1.6rem; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% if template.template_accesses.present? %> <% if template.template_accesses.present? %>
<%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %> <%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %>
<% end %> <% end %>
@@ -22,7 +22,7 @@
<% if template.archived_at? %> <% if template.archived_at? %>
<span class="flex items-center space-x-1 w-1/2"> <span class="flex items-center space-x-1 w-1/2">
<%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %> <%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %>
<span class="truncate"><%= template.folder.name %></span> <span class="truncate"><%= template.folder.full_name %></span>
</span> </span>
<% end %> <% end %>
</p> </p>
+2 -2
View File
@@ -16,12 +16,12 @@
<a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer"> <a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %> <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %>
<span class="text-sm"> <span class="text-sm">
<%= @template.folder.name %> <%= @template.folder.full_name %>
</span> </span>
</a> </a>
<% if can?(:update, template) %> <% if can?(:update, template) %>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('move') %>"> <span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('move') %>">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal"> <a href="<%= edit_template_folder_path(template.id, subfolder: @template.folder.parent_folder.present?) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %> <%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a> </a>
</span> </span>
+1 -1
View File
@@ -16,7 +16,7 @@
<%= svg_icon('folder', class: 'w-6 h-6') %> <%= svg_icon('folder', class: 'w-6 h-6') %>
</a> </a>
<folder-autocomplete class="flex justify-between w-full"> <folder-autocomplete class="flex justify-between w-full">
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off"> <input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5"> <a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
<%= t('change_folder') %> <%= t('change_folder') %>
</a> </a>
+13 -4
View File
@@ -1,12 +1,12 @@
<div> <div>
<%= link_to root_path do %> <%= link_to root_path, class: 'flex items-center' do %>
&larr; <%= svg_icon('chevron_left', class: 'w-5 h-5') %>
<span><%= t('back_to_active') %></span> <span style="margin-left: 3px"><%= t('back_to_active') %></span>
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-4 items-center"> <div class="flex justify-between mb-4 items-center">
<div> <div>
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('document_templates_html') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1> <h1 class="text-2xl md:text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('document_templates_html') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
</div> </div>
<% if params[:q].present? || @pagy.pages > 1 %> <% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %> <%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
@@ -22,5 +22,14 @@
<%= t('templates_not_found') %> <%= t('templates_not_found') %>
</div> </div>
</div> </div>
<% if @related_submissions.present? %>
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
<%= t('submissions') %>
</h1>
<div class="space-y-4 mt-4">
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
</div>
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_archived_index_path(q: params[:q]) %>
<% end %>
<% end %> <% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %> <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %>
@@ -1,15 +1,15 @@
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<% with_filters = @pagy.pages > 1 || params[:q].present? || filter_params.present? %> <% with_filters = @pagy.pages > 1 || params[:q].present? || filter_params.present? %>
<%= render 'templates/title', template: @template %> <%= render 'templates/title', template: @template %>
<div>
<%= link_to template_path(@template) do %>
&larr;
<span><%= t('back_to_active') %></span>
<% end %>
</div>
<div class="flex flex-col md:flex-row md:items-center mb-6 gap-3"> <div class="flex flex-col md:flex-row md:items-center mb-6 gap-3">
<div class="flex w-full justify-between md:items-end items-center"> <div class="flex w-full justify-between md:items-end items-end">
<div> <div>
<div>
<%= link_to template_path(@template), class: 'flex items-center' do %>
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
<span style="margin-left: 3px"><%= t('back_to_active') %></span>
<% end %>
</div>
<h1 class="text-3xl font-bold md:block"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1> <h1 class="text-3xl font-bold md:block"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
</div> </div>
<div class="flex space-x-2 justify-end"> <div class="flex space-x-2 justify-end">
@@ -11,6 +11,7 @@
<% unless show_dropzone %> <% unless show_dropzone %>
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %> <%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
<% end %> <% end %>
<%= render 'templates/dashboard_folder_dropzone', style: 'height: 114px' %>
<div class="flex items-center flex-grow min-w-0"> <div class="flex items-center flex-grow min-w-0">
<% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %> <% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
<div class="mr-2"> <div class="mr-2">
+31 -3
View File
@@ -1,7 +1,35 @@
<% with_subfolder = @template.folder.name != TemplateFolder::DEFAULT_NAME && params[:subfolder] != 'false' %>
<%= render 'shared/turbo_modal', title: t('move_into_folder') do %> <%= render 'shared/turbo_modal', title: t('move_into_folder') do %>
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> <% if with_subfolder %>
<div class="form-control my-6"> <%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'subfolder_form', autocomplete: :off } do |f| %>
<folder-autocomplete class="block" data-submit-on-select="true"> <%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name">
<div class="flex items-center justify-between mb-2.5">
<label for="is_root_folder" class="flex items-center tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
<span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>
<%= svg_icon('folder_share', class: 'w-5 h-5 flex-shrink-0 hidden group-hover:inline mr-1') %>
<span class="text-md">
<%= @template.folder.parent_folder&.name || @template.folder.name %>
</span>
</span>
</label>
</div>
</toggle-visible>
<div class="form-control mb-6">
<folder-autocomplete class="block" data-submit-on-select="true" data-parent-name="<%= @template.folder.parent_folder&.name || @template.folder.name %>" data-enabled="<%= params[:autocomplete] != 'false' %>">
<%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true, id: 'subfolder_name' %>
</folder-autocomplete>
</div>
<div class="form-control">
<%= f.button button_title(title: t('move'), disabled_with: t('moving')), class: 'base-button' %>
</div>
<% end %>
<% end %>
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'folder_form', autocomplete: :off, class: "mt-6 #{'hidden' if with_subfolder}" } do |f| %>
<div class="form-control mb-6">
<folder-autocomplete class="block" data-submit-on-select="true" data-enabled="<%= params[:autocomplete] != 'false' %>">
<%= f.text_field :name, required: true, placeholder: "#{t('new_folder_name')}...", class: 'base-input w-full', autofocus: true %> <%= f.text_field :name, required: true, placeholder: "#{t('new_folder_name')}...", class: 'base-input w-full', autofocus: true %>
</folder-autocomplete> </folder-autocomplete>
</div> </div>
+10 -1
View File
@@ -59,6 +59,7 @@
<%= t('form_preferences') %> <%= t('form_preferences') %>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<%= render 'templates_prefillable_fields/form', template: @template %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit> <toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %> <% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
@@ -68,7 +69,7 @@
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %> <%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
</div> </div>
<%= ff.fields_for :completed_message, ff.object.completed_message do |fff| %> <%= ff.fields_for :completed_message, ff.object.completed_message do |fff| %>
<div class="form-control mb-2 border-t mt-4"> <div class="form-control mb-2 mt-4">
<%= fff.label :body, t('completion_message'), class: 'label' %> <%= fff.label :body, t('completion_message'), class: 'label' %>
<autoresize-textarea> <autoresize-textarea>
<%= fff.text_area :body, required: false, class: 'base-input w-full py-2', dir: 'auto' %> <%= fff.text_area :body, required: false, class: 'base-input w-full py-2', dir: 'auto' %>
@@ -162,6 +163,14 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= 'Send signature request email' %>
</span>
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center"> <div class="flex justify-center">
@@ -0,0 +1,16 @@
<% select_fields = template.fields.filter_map { |f| [f['name'], f['uuid']] if f['name'].present? && f['type'].in?(TemplatesPrefillableFieldsController::PREFILLABLE_FIELD_TYPES) } %>
<% if select_fields.present? %>
<div class="border-b pb-4 mb-4">
<%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
<div class="form-control">
<%= f.hidden_field :prefillable, value: 'true' %>
<%= f.label :field_uuid, t(:invite_form_fields), class: 'label' %>
<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 %>
<%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %>
</div>
</div>
<% end %>
<%= render partial: 'templates_prefillable_fields/list', locals: { template: } %>
</div>
<% end %>
@@ -0,0 +1,6 @@
<div id="prefillable_fields_list" class="flex gap-1 px-0.5 mt-2 flex-wrap">
<% template.fields.each do |f| %>
<% next unless f['prefillable'] %>
<%= button_to button_title(title: f['name'].presence || f['type'].capitalize, disabled_with: f['name'].presence || f['type'].capitalize, icon: svg_icon('x', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), template_prefillable_fields_path(template), params: { field_uuid: f['uuid'], prefillable: 'false' }, class: 'badge badge-lg badge-primary space-x-1 pr-3 pl-2', form: { data: { close_on_submit: false } } %>
<% end %>
</div>
+46 -27
View File
@@ -1,3 +1,5 @@
<% multiple_submitters = @template.submitters.to_a.length > 1 %>
<% enough_defined_submitters = Templates.filter_undefined_submitters(@template.submitters).size < 2 %>
<%= render 'shared/turbo_modal_large', title: t('share_link') do %> <%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5"> <div class="mt-2 mb-4 px-5">
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
@@ -12,28 +14,14 @@
</check-on-click> </check-on-click>
</div> </div>
<% end %> <% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 mb-5' }, data: { close_on_submit: false } do |f| %> <% if multiple_submitters && !enough_defined_submitters %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-12 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size > 1 %>
<div class="alert items-start bg-base-100 border-base-300 mt-4"> <div class="alert items-start bg-base-100 border-base-300 mt-4">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %> <%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div> <div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div>
</div> </div>
<% end %> <div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<% if @template.submitters.to_a.length > 1 %> <input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse collapse-arrow join-item border border-base-300 mt-4 mb-5"> <div class="collapse-title py-4 min-h-0">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= t('default_parties') %> <%= t('default_parties') %>
</div> </div>
<div class="collapse-content !pb-0"> <div class="collapse-content !pb-0">
@@ -41,15 +29,46 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %> <div class="collapse collapse-arrow join-item mt-4">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %> <input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %> <div class="collapse-title min-h-0 after:!right-3 p-1">
<label for="template_preferences_shared_link_2fa" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300"> <%= t('advanced_settings') %>
<span><%= t('request_email_otp_verification_with_shared_link') %></span> </div>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %> <div class="collapse-content !p-0">
</label> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %> <% end %>
<% end %> <% if multiple_submitters && enough_defined_submitters %>
<% end %> <div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
</div>
</div>
<% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div> </div>
<% end %> <% end %>
@@ -1,7 +1,7 @@
<p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p> <p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p>
<p><%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Docuseal.product_name) %></p> <p><%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Docuseal.product_name) %></p>
<p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p> <p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></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 %>
</p> </p>
+1
View File
@@ -8,6 +8,7 @@ module Rouge
module Lexers module Lexers
autoload :JSON, 'rouge/lexers/json' autoload :JSON, 'rouge/lexers/json'
autoload :Shell, 'rouge/lexers/shell'
end end
autoload :Formatter, 'rouge/formatter' autoload :Formatter, 'rouge/formatter'
+131 -55
View File
@@ -23,6 +23,8 @@ en: &en
pro: Pro pro: Pro
thanks: Thanks thanks: Thanks
private: Private private: Private
select: Select
invite_form_fields: Invite 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
@@ -42,7 +44,6 @@ en: &en
send_verification_code: Send verification code send_verification_code: Send verification code
code_has_been_resent: Code has been re-sent code_has_been_resent: Code has been re-sent
invalid_code: Invalid code invalid_code: Invalid code
unarchive: Unarchive
signed: Signed signed: Signed
first_party: 'First Party' first_party: 'First Party'
remove_filter: Remove filter remove_filter: Remove filter
@@ -56,7 +57,6 @@ en: &en
accessiable_by: Accessible by accessiable_by: Accessible by
team_access: Team access team_access: Team access
document_download_filename_format: Document download filename format document_download_filename_format: Document download filename format
document_name: Document Name
docuseal_trusted_signature: DocuSeal Trusted Signature docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name} hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name} you_are_invited_to_product_name: You are invited to %{product_name}
@@ -73,7 +73,7 @@ en: &en
awaiting_completion_by_the_other_party: "Awaiting completion by the other party" awaiting_completion_by_the_other_party: "Awaiting completion by the other party"
review_and_sign: Review and Sign review_and_sign: Review and Sign
review_and_submit: Review and Submit review_and_submit: Review and Submit
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Please contact us by replying to this email if you have any questions." please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Please contact us by replying to this email if you have any questions."
submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}'
verification_code_sms_body: 'Verification code: {code}' verification_code_sms_body: 'Verification code: {code}'
you_are_invited_to_submit_a_form: 'You are invited to submit a form' you_are_invited_to_submit_a_form: 'You are invited to submit a form'
@@ -398,7 +398,6 @@ 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
but_not_activated: but not activated
sms_not_configured: SMS not Configure sms_not_configured: SMS not Configure
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
@@ -460,6 +459,10 @@ en: &en
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.' users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.'
move_into_folder: Move Into Folder move_into_folder: Move Into Folder
new_folder_name: New Folder Name new_folder_name: New Folder Name
new_subfolder_name: New Subfolder Name
change_parent_folder: Change Parent Folder
folder: Folder
create_a_new_folder: Create a New Folder
exit_preview: Exit Preview exit_preview: Exit Preview
general: General general: General
recipients: Recipients recipients: Recipients
@@ -492,7 +495,6 @@ en: &en
edit_user: Edit User edit_user: Edit User
new_user: New User new_user: New User
unarchive: Unarchive unarchive: Unarchive
role: Role
last_session: Last session last_session: Last session
unlock_more_user_roles_with_docuseal_pro: Unlock more user roles with DocuSeal Pro. unlock_more_user_roles_with_docuseal_pro: Unlock more user roles with DocuSeal Pro.
view_active: View Active view_active: View Active
@@ -559,8 +561,6 @@ en: &en
new_webhook: New Webhook new_webhook: New Webhook
delete_webhook: Delete webhook delete_webhook: Delete webhook
count_submissions_have_been_created: '%{count} submissions have been created.' count_submissions_have_been_created: '%{count} submissions have been created.'
gmail_has_been_connected: Gmail has been connected
microsoft_account_has_been_connected: Microsoft Account has been connected
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
connected_successfully: Connected successfully. connected_successfully: Connected successfully.
user_nameid_not_found: 'User %{nameid} not found.' user_nameid_not_found: 'User %{nameid} not found.'
@@ -779,6 +779,19 @@ en: &en
email_verification: Email verification email_verification: Email verification
your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":' your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this. please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this.
advanced_settings: Advanced settings
text: Text
code: Code
custom_html_emails: Custom HTML emails
connect_your_email_to_send_html_emails: Connect your email to send HTML emails
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Connect your Gmail or Outlook account or add SMTP settings to send custom HTML emails.
connect_gmail_or_outlook: Connect Gmail or Outlook
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.
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.
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.'
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@@ -885,6 +898,8 @@ en: &en
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
es: &es es: &es
select: Seleccionar
invite_form_fields: Invitar campos del formulario
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
@@ -918,8 +933,6 @@ es: &es
team_access: Acceso del equipo team_access: Acceso del equipo
remove_filter: Eliminar filtro remove_filter: Eliminar filtro
document_download_filename_format: Formato del nombre del archivo de descarga del documento document_download_filename_format: Formato del nombre del archivo de descarga del documento
document_name: Nombre del documento
unarchive: Desarchivar
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte" awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios' enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
first_party: 'Primera Parte' first_party: 'Primera Parte'
@@ -940,7 +953,7 @@ es: &es
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.'
review_and_sign: Revisar y Firmar review_and_sign: Revisar y Firmar
review_and_submit: Revisar y Enviar review_and_submit: Revisar y Enviar
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta." please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}'
verification_code_sms_body: 'Código de verificación: {code}' verification_code_sms_body: 'Código de verificación: {code}'
you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario' you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario'
@@ -969,7 +982,7 @@ es: &es
submitter_documents_copy_email_body: | submitter_documents_copy_email_body: |
Hola, Hola,
Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos. Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos del correo electrónico.
Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación: Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación:
[{template.name}]({documents.link}) [{template.name}]({documents.link})
@@ -1264,7 +1277,6 @@ es: &es
send_signature_request_emails_without_limits_with_docuseal_pro: Envía solicitudes de firma sin límites con DocuSeal Pro send_signature_request_emails_without_limits_with_docuseal_pro: Envía solicitudes de firma sin límites con DocuSeal Pro
count_emails_used: '%{count} correos electrónicos utilizados' count_emails_used: '%{count} correos electrónicos utilizados'
has_been_connected: ha sido conectado has_been_connected: ha sido conectado
but_not_activated: pero no activado
sms_not_configured: SMS no configurado sms_not_configured: SMS no configurado
configure_sms_settings_in_order_to_send_text_messages_: 'Configura los ajustes de SMS para enviar mensajes de texto:' configure_sms_settings_in_order_to_send_text_messages_: 'Configura los ajustes de SMS para enviar mensajes de texto:'
go_to_sms_settings: Ir a la configuración de SMS go_to_sms_settings: Ir a la configuración de SMS
@@ -1326,6 +1338,10 @@ es: &es
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".' users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".'
move_into_folder: Mover a la carpeta move_into_folder: Mover a la carpeta
new_folder_name: Nuevo nombre de la carpeta new_folder_name: Nuevo nombre de la carpeta
new_subfolder_name: Nuevo nombre de subcarpeta
change_parent_folder: Cambiar carpeta principal
folder: Carpeta
create_a_new_folder: Crear una nueva carpeta
exit_preview: Salir de la vista previa exit_preview: Salir de la vista previa
general: General general: General
recipients: Destinatarios recipients: Destinatarios
@@ -1358,7 +1374,6 @@ es: &es
edit_user: Editar usuario edit_user: Editar usuario
new_user: Nuevo usuario new_user: Nuevo usuario
unarchive: Desarchivar unarchive: Desarchivar
role: Rol
last_session: Última sesión last_session: Última sesión
unlock_more_user_roles_with_docuseal_pro: Desbloquea más roles de usuario con DocuSeal Pro. unlock_more_user_roles_with_docuseal_pro: Desbloquea más roles de usuario con DocuSeal Pro.
view_active: Ver activos view_active: Ver activos
@@ -1425,8 +1440,6 @@ es: &es
new_webhook: Nuevo Webhook new_webhook: Nuevo Webhook
delete_webhook: Eliminar webhook delete_webhook: Eliminar webhook
count_submissions_have_been_created: '%{count} envíos han sido creados.' count_submissions_have_been_created: '%{count} envíos han sido creados.'
gmail_has_been_connected: Gmail ha sido conectado.
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes. sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
connected_successfully: Conectado con éxito. connected_successfully: Conectado con éxito.
user_nameid_not_found: 'Usuario %{nameid} no encontrado.' user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
@@ -1519,7 +1532,6 @@ es: &es
select_field: Campo de Selección select_field: Campo de Selección
multiple_field: Campo Múltiple multiple_field: Campo Múltiple
radio_field: Campo de Grupo Radio radio_field: Campo de Grupo Radio
radio_field: Campo de Radio
cells_field: Campo de Celdas cells_field: Campo de Celdas
stamp_field: Campo de Sello stamp_field: Campo de Sello
payment_field: Campo de Pago payment_field: Campo de Pago
@@ -1645,6 +1657,19 @@ es: &es
email_verification: Verificación por correo electrónico email_verification: Verificación por correo electrónico
your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":' your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto. please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto.
advanced_settings: Configuración avanzada
text: Texto
code: Código
custom_html_emails: Correos electrónicos HTML personalizados
connect_your_email_to_send_html_emails: Conecta tu correo electrónico para enviar correos HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Conecta tu cuenta de Gmail u Outlook o añade ajustes SMTP para enviar correos HTML personalizados.
connect_gmail_or_outlook: Conectar Gmail u Outlook
connect_your_email_to_bulk_send: Conecta tu correo electrónico para el envío masivo
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Conecta tu cuenta de Gmail u Outlook o añade ajustes SMTP para el envío masivo.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: ¿Estás seguro de que quieres añadir destinatarios sin enviar? Para enviar correos electrónicos necesitas conectar Gmail u Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@@ -1751,6 +1776,8 @@ es: &es
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
it: &it it: &it
select: Seleziona
invite_form_fields: Invita campi modulo
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"
@@ -1783,8 +1810,6 @@ it: &it
team_access: Accesso al team team_access: Accesso al team
remove_filter: Rimuovi filtro remove_filter: Rimuovi filtro
document_download_filename_format: Formato del nome file scaricato document_download_filename_format: Formato del nome file scaricato
document_name: Nome del Documento
unarchive: Ripristina
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte" awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
enforce_recipients_order: "Applicare l'ordine dei destinatari" enforce_recipients_order: "Applicare l'ordine dei destinatari"
first_party: 'Prima parte' first_party: 'Prima parte'
@@ -1805,7 +1830,7 @@ it: &it
please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email." please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email."
review_and_sign: Rivedi e Firma review_and_sign: Rivedi e Firma
review_and_submit: Rivedi e Invia review_and_submit: Rivedi e Invia
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Per favore, contattaci rispondendo a questa email se hai domande." please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Per favore, contattaci rispondendo a questa email se hai domande."
submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}'
verification_code_sms_body: 'Codice di verifica: {code}' verification_code_sms_body: 'Codice di verifica: {code}'
you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo' you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo'
@@ -2129,7 +2154,6 @@ it: &it
send_signature_request_emails_without_limits_with_docuseal_pro: Invia email di richiesta di firma senza limiti con DocuSeal Pro send_signature_request_emails_without_limits_with_docuseal_pro: Invia email di richiesta di firma senza limiti con DocuSeal Pro
count_emails_used: '%{count} email utilizzate' count_emails_used: '%{count} email utilizzate'
has_been_connected: è stato connesso has_been_connected: è stato connesso
but_not_activated: ma non attivato
sms_not_configured: SMS non configurato sms_not_configured: SMS non configurato
configure_sms_settings_in_order_to_send_text_messages_: 'Configura le impostazioni SMS per inviare messaggi di testo:' configure_sms_settings_in_order_to_send_text_messages_: 'Configura le impostazioni SMS per inviare messaggi di testo:'
go_to_sms_settings: Vai alle impostazioni SMS go_to_sms_settings: Vai alle impostazioni SMS
@@ -2191,6 +2215,10 @@ it: &it
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".' users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".'
move_into_folder: Sposta nella cartella move_into_folder: Sposta nella cartella
new_folder_name: Nuovo nome della cartella new_folder_name: Nuovo nome della cartella
new_subfolder_name: Nuovo nome della sottocartella
change_parent_folder: Cambia cartella principale
folder: Cartella
create_a_new_folder: Crea una nuova cartella
exit_preview: "Esci dall'anteprima" exit_preview: "Esci dall'anteprima"
general: Generale general: Generale
recipients: Destinatari recipients: Destinatari
@@ -2222,7 +2250,6 @@ it: &it
edit_user: Modifica utente edit_user: Modifica utente
new_user: Nuovo utente new_user: Nuovo utente
unarchive: Disarchivia unarchive: Disarchivia
role: Ruolo
last_session: Ultima sessione last_session: Ultima sessione
unlock_more_user_roles_with_docuseal_pro: Sblocca più ruoli utente con DocuSeal Pro. unlock_more_user_roles_with_docuseal_pro: Sblocca più ruoli utente con DocuSeal Pro.
view_active: Visualizza attivi view_active: Visualizza attivi
@@ -2289,8 +2316,6 @@ it: &it
new_webhook: Nuovo Webhook new_webhook: Nuovo Webhook
delete_webhook: Elimina webhook delete_webhook: Elimina webhook
count_submissions_have_been_created: '%{count} invii sono stati creati.' count_submissions_have_been_created: '%{count} invii sono stati creati.'
gmail_has_been_connected: Gmail è stato connesso.
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte." sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
connected_successfully: Collegamento avvenuto con successo. connected_successfully: Collegamento avvenuto con successo.
user_nameid_not_found: 'Utente %{nameid} non trovato.' user_nameid_not_found: 'Utente %{nameid} non trovato.'
@@ -2510,6 +2535,19 @@ it: &it
email_verification: Verifica email email_verification: Verifica email
your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":' your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo. please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo.
advanced_settings: Impostazioni avanzate
text: Testo
code: Codice
custom_html_emails: Email HTML personalizzate
connect_your_email_to_send_html_emails: Collega la tua email per inviare email HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Collega il tuo account Gmail o Outlook oppure aggiungi impostazioni SMTP per inviare email HTML personalizzate.
connect_gmail_or_outlook: Collega Gmail o Outlook
connect_your_email_to_bulk_send: Collega la tua email per invio multiplo
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Collega il tuo account Gmail o Outlook oppure aggiungi impostazioni SMTP per invio multiplo.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Sei sicuro di voler aggiungere destinatari senza inviare? Per inviare email devi collegare Gmail o Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@@ -2616,6 +2654,8 @@ it: &it
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
fr: &fr fr: &fr
select: Sélectionner
invite_form_fields: Inviter des champs de formulaire
pro: Pro pro: Pro
default_parties: Parties par défaut default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
@@ -2649,8 +2689,6 @@ fr: &fr
team_access: Accès à l'équipe team_access: Accès à l'équipe
remove_filter: Supprimer le filtre remove_filter: Supprimer le filtre
document_download_filename_format: Format du nom de fichier du téléchargement de document document_download_filename_format: Format du nom de fichier du téléchargement de document
document_name: Nom du document
unarchive: Désarchiver
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie" awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
enforce_recipients_order: "Respecter l'ordre des destinataires" enforce_recipients_order: "Respecter l'ordre des destinataires"
first_party: 'Première partie' first_party: 'Première partie'
@@ -2671,7 +2709,7 @@ fr: &fr
please_check_the_copy_of_your_name_in_the_email_attachments: "Veuillez vérifier la copie de votre \"%{name}\" dans les pièces jointes de l'email." please_check_the_copy_of_your_name_in_the_email_attachments: "Veuillez vérifier la copie de votre \"%{name}\" dans les pièces jointes de l'email."
review_and_sign: Consulter et signer review_and_sign: Consulter et signer
review_and_submit: Consulter et soumettre review_and_submit: Consulter et soumettre
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.' please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.'
submitter_invitation_sms_body_sign: '{account.name} vous a invité à signer un document: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} vous a invité à signer un document: {submitter.link}'
verification_code_sms_body: 'Code de vérification: {code}' verification_code_sms_body: 'Code de vérification: {code}'
you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire
@@ -2687,7 +2725,7 @@ fr: &fr
[Consulter et signer]({submitter.link}) [Consulter et signer]({submitter.link})
Veuillez nous contacter si vous avez des questions. Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.
Merci, Merci,
{account.name} {account.name}
@@ -2700,7 +2738,7 @@ fr: &fr
submitter_documents_copy_email_body: | submitter_documents_copy_email_body: |
Bonjour, Bonjour,
Veuillez vérifier la copie de votre "{template.name}" dans les pièces jointes. Veuillez vérifier la copie de votre "{template.name}" dans les pièces jointes de l'email.
Vous pouvez également consulter et télécharger votre copie en utilisant le lien ci-dessous: Vous pouvez également consulter et télécharger votre copie en utilisant le lien ci-dessous:
[{template.name}]({documents.link}) [{template.name}]({documents.link})
@@ -2996,7 +3034,6 @@ fr: &fr
send_signature_request_emails_without_limits_with_docuseal_pro: Envoyez des e-mails de demande de signature sans limites avec DocuSeal Pro send_signature_request_emails_without_limits_with_docuseal_pro: Envoyez des e-mails de demande de signature sans limites avec DocuSeal Pro
count_emails_used: '%{count} e-mails utilisés' count_emails_used: '%{count} e-mails utilisés'
has_been_connected: a été connecté has_been_connected: a été connecté
but_not_activated: mais pas activé
sms_not_configured: SMS non configuré sms_not_configured: SMS non configuré
configure_sms_settings_in_order_to_send_text_messages_: 'Configurez les paramètres SMS pour envoyer des messages texte' configure_sms_settings_in_order_to_send_text_messages_: 'Configurez les paramètres SMS pour envoyer des messages texte'
go_to_sms_settings: Aller aux paramètres SMS go_to_sms_settings: Aller aux paramètres SMS
@@ -3058,6 +3095,10 @@ fr: &fr
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".' users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".'
move_into_folder: Déplacer dans le dossier move_into_folder: Déplacer dans le dossier
new_folder_name: Nouveau nom du dossier new_folder_name: Nouveau nom du dossier
new_subfolder_name: Nouveau nom du sous-dossier
change_parent_folder: Changer le dossier parent
folder: Dossier
create_a_new_folder: Créer un nouveau dossier
exit_preview: "Quitter l'aperçu" exit_preview: "Quitter l'aperçu"
general: Général general: Général
recipients: Destinataires recipients: Destinataires
@@ -3090,7 +3131,6 @@ fr: &fr
edit_user: "Modifier l'utilisateur" edit_user: "Modifier l'utilisateur"
new_user: Nouvel utilisateur new_user: Nouvel utilisateur
unarchive: Désarchiver unarchive: Désarchiver
role: Rôle
last_session: Dernière session last_session: Dernière session
unlock_more_user_roles_with_docuseal_pro: Déverrouillez plus de rôles d'utilisateurs avec DocuSeal Pro. unlock_more_user_roles_with_docuseal_pro: Déverrouillez plus de rôles d'utilisateurs avec DocuSeal Pro.
view_active: Voir les actifs view_active: Voir les actifs
@@ -3157,8 +3197,6 @@ fr: &fr
new_webhook: Nouveau Webhook new_webhook: Nouveau Webhook
delete_webhook: Supprimer le webhook delete_webhook: Supprimer le webhook
count_submissions_have_been_created: '%{count} soumissions ont été créées.' count_submissions_have_been_created: '%{count} soumissions ont été créées.'
gmail_has_been_connected: Gmail a été connecté.
microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets. sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
connected_successfully: Connecté avec succès. connected_successfully: Connecté avec succès.
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.' user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
@@ -3378,6 +3416,19 @@ fr: &fr
email_verification: Vérification de l'email email_verification: Vérification de l'email
your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :' your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :'
please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande. please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande.
advanced_settings: Paramètres avancés
text: Texte
code: Code
custom_html_emails: E-mails HTML personnalisés
connect_your_email_to_send_html_emails: Connectez votre e-mail pour envoyer des e-mails HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Connectez votre compte Gmail ou Outlook ou ajoutez des paramètres SMTP pour envoyer des e-mails HTML personnalisés.
connect_gmail_or_outlook: Connecter Gmail ou Outlook
connect_your_email_to_bulk_send: Connectez votre e-mail pour l'envoi groupé
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connectez votre compte Gmail ou Outlook ou ajoutez des paramètres SMTP pour l'envoi groupé.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Êtes-vous sûr de vouloir ajouter des destinataires sans envoi ? Pour envoyer des e-mails, vous devez connecter Gmail ou Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de l’e-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@@ -3484,6 +3535,8 @@ fr: &fr
range_without_total: "%{from} à %{to} événements" range_without_total: "%{from} à %{to} événements"
pt: &pt pt: &pt
select: Selecionar
invite_form_fields: Convidar campos do formulário
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
@@ -3517,8 +3570,6 @@ pt: &pt
team_access: Acesso à equipe team_access: Acesso à equipe
remove_filter: Remover filtro remove_filter: Remover filtro
document_download_filename_format: Formato do nome do arquivo de download do documento document_download_filename_format: Formato do nome do arquivo de download do documento
document_name: Nome do documento
unarchive: Desarquivar
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte" awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
enforce_recipients_order: 'Forçar a ordem dos recipientes' enforce_recipients_order: 'Forçar a ordem dos recipientes'
first_party: 'Primeira Parte' first_party: 'Primeira Parte'
@@ -3539,7 +3590,7 @@ pt: &pt
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.'
review_and_sign: Revisar e assinar review_and_sign: Revisar e assinar
review_and_submit: Revisar e submeter review_and_submit: Revisar e submeter
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.' please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}'
verification_code_sms_body: 'Código de verificação: {code}' verification_code_sms_body: 'Código de verificação: {code}'
you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário
@@ -3551,11 +3602,11 @@ pt: &pt
submitter_invitation_email_sign_body: | submitter_invitation_email_sign_body: |
Olá, Olá,
Você foi convidado a assinar o documento "{template.name}". Você foi convidado a assinar "{template.name}".
[Revisar e assinar]({submitter.link}) [Revisar e assinar]({submitter.link})
Por favor, entre em contato conosco se tiver alguma dúvida. Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado, Obrigado,
{account.name} {account.name}
@@ -3568,8 +3619,8 @@ pt: &pt
submitter_documents_copy_email_body: | submitter_documents_copy_email_body: |
Olá, Olá,
Por favor, verifique a cópia de seu "{template.name}" nos anexos do e-mail. Por favor, verifique a cópia de "{template.name}" nos anexos do e-mail.
Alternativamente, você pode revisar e baixar sua cópia usando o link abaixo: Você pode revisar e baixar sua cópia usando o link abaixo:
[{template.name}]({documents.link}) [{template.name}]({documents.link})
@@ -3863,7 +3914,6 @@ pt: &pt
send_signature_request_emails_without_limits_with_docuseal_pro: Enviar e-mails de solicitação de assinatura sem limites com o DocuSeal Pro send_signature_request_emails_without_limits_with_docuseal_pro: Enviar e-mails de solicitação de assinatura sem limites com o DocuSeal Pro
count_emails_used: '%{count} e-mails usados' count_emails_used: '%{count} e-mails usados'
has_been_connected: foi conectado has_been_connected: foi conectado
but_not_activated: mas não ativado
sms_not_configured: SMS não configurado sms_not_configured: SMS não configurado
configure_sms_settings_in_order_to_send_text_messages_: 'Configure as configurações de SMS para enviar mensagens de texto:' configure_sms_settings_in_order_to_send_text_messages_: 'Configure as configurações de SMS para enviar mensagens de texto:'
go_to_sms_settings: Ir para configurações de SMS go_to_sms_settings: Ir para configurações de SMS
@@ -3925,6 +3975,10 @@ pt: &pt
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".' users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".'
move_into_folder: Mover para pasta move_into_folder: Mover para pasta
new_folder_name: Novo nome da pasta new_folder_name: Novo nome da pasta
new_subfolder_name: Novo nome da subpasta
change_parent_folder: Alterar pasta pai
folder: Pasta
create_a_new_folder: Criar uma nova pasta
exit_preview: Sair da pré-visualização exit_preview: Sair da pré-visualização
general: Geral general: Geral
recipients: Destinatários recipients: Destinatários
@@ -3957,7 +4011,6 @@ pt: &pt
edit_user: Editar usuário edit_user: Editar usuário
new_user: Novo usuário new_user: Novo usuário
unarchive: Desarquivar unarchive: Desarquivar
role: Função
last_session: Última sessão last_session: Última sessão
unlock_more_user_roles_with_docuseal_pro: Desbloqueie mais funções de usuário com DocuSeal Pro. unlock_more_user_roles_with_docuseal_pro: Desbloqueie mais funções de usuário com DocuSeal Pro.
view_active: Ver ativos view_active: Ver ativos
@@ -4024,8 +4077,6 @@ pt: &pt
new_webhook: Novo Webhook new_webhook: Novo Webhook
delete_webhook: Excluir webhook delete_webhook: Excluir webhook
count_submissions_have_been_created: '%{count} submissões foram criadas.' count_submissions_have_been_created: '%{count} submissões foram criadas.'
gmail_has_been_connected: O Gmail foi conectado
microsoft_account_has_been_connected: A conta da Microsoft foi conectada
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
connected_successfully: Conectado com sucesso. connected_successfully: Conectado com sucesso.
user_nameid_not_found: 'Usuário %{nameid} não encontrado.' user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
@@ -4244,6 +4295,19 @@ pt: &pt
email_verification: Verificação de e-mail email_verification: Verificação de e-mail
your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":' your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso. please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso.
advanced_settings: Configurações avançadas
text: Texto
code: Código
custom_html_emails: E-mails HTML personalizados
connect_your_email_to_send_html_emails: Conecte seu e-mail para enviar e-mails HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Conecte sua conta Gmail ou Outlook ou adicione configurações SMTP para enviar e-mails HTML personalizados.
connect_gmail_or_outlook: Conectar Gmail ou Outlook
connect_your_email_to_bulk_send: Conecte seu e-mail para envio em massa
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Conecte sua conta Gmail ou Outlook ou adicione configurações SMTP para envio em massa.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Tem certeza de que deseja adicionar destinatários sem enviar? Para enviar e-mails é necessário conectar Gmail ou Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@@ -4324,7 +4388,6 @@ pt: &pt
settings_account_description: 'Com configurações completas de conta, você pode personalizar a experiência de assinatura eletrônica e convidar mais usuários.' settings_account_description: 'Com configurações completas de conta, você pode personalizar a experiência de assinatura eletrônica e convidar mais usuários.'
support: 'Suporte' support: 'Suporte'
support_description: 'Você pode usar nosso assistente de IA ou nos escrever para support@docuseal.com se tiver dúvidas.' support_description: 'Você pode usar nosso assistente de IA ou nos escrever para support@docuseal.com se tiver dúvidas.'
settings: 'Configurações'
settings_template_description: 'Use este botão para acessar a página de configurações da sua conta.' settings_template_description: 'Use este botão para acessar a página de configurações da sua conta.'
doorkeeper: doorkeeper:
scopes: scopes:
@@ -4351,6 +4414,8 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
de: &de de: &de
select: Auswählen
invite_form_fields: Formularfelder einladen
pro: Pro pro: Pro
default_parties: Standardparteien default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
@@ -4384,8 +4449,6 @@ de: &de
team_access: Teamzugang team_access: Teamzugang
remove_filter: Filter entfernen remove_filter: Filter entfernen
document_download_filename_format: Format des Dateinamens beim Herunterladen von Dokumenten document_download_filename_format: Format des Dateinamens beim Herunterladen von Dokumenten
document_name: Dokumentname
unarchive: Wiederherstellen
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei" awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen' enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
first_party: 'Erste Partei' first_party: 'Erste Partei'
@@ -4406,7 +4469,7 @@ de: &de
please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte überprüfe die Kopie von "%{name}" im E-Mail-Anhang.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte überprüfe die Kopie von "%{name}" im E-Mail-Anhang.'
review_and_sign: Überprüfen und unterschreiben review_and_sign: Überprüfen und unterschreiben
review_and_submit: Überprüfen und einreichen review_and_submit: Überprüfen und einreichen
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Bitte kontaktiere uns, indem du auf diese E-Mail antwortest, falls du Fragen hast.' please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.'
submitter_invitation_sms_body_sign: '{account.name} hat dich eingeladen, ein Dokument zu unterschreiben: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} hat dich eingeladen, ein Dokument zu unterschreiben: {submitter.link}'
verification_code_sms_body: 'Verifizierungscode: {code}' verification_code_sms_body: 'Verifizierungscode: {code}'
you_are_invited_to_submit_a_form: Du wurdest eingeladen, ein Formular einzureichen you_are_invited_to_submit_a_form: Du wurdest eingeladen, ein Formular einzureichen
@@ -4418,11 +4481,11 @@ de: &de
submitter_invitation_email_sign_body: | submitter_invitation_email_sign_body: |
Hallo, Hallo,
Du wurdest eingeladen, das Dokument "{template.name}" zu unterschreiben. Du wurdest eingeladen, "{template.name}" zu unterschreiben.
[Überprüfen und unterschreiben]({submitter.link}) [Überprüfen und unterschreiben]({submitter.link})
Bitte kontaktiere uns, falls du Fragen hast. Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke, Danke,
{account.name} {account.name}
@@ -4435,8 +4498,8 @@ de: &de
submitter_documents_copy_email_body: | submitter_documents_copy_email_body: |
Hallo, Hallo,
Bitte überprüfe die Kopie deines "{template.name}" im E-Mail-Anhang. Bitte überprüfe die Kopie von "{template.name}" im E-Mail-Anhang.
Alternativ kannst du deine Kopie mit dem untenstehenden Link überprüfen und herunterladen: Du kannst alternativ deine Kopie mit dem untenstehenden Link überprüfen und herunterladen:
[{template.name}]({documents.link}) [{template.name}]({documents.link})
@@ -4730,7 +4793,6 @@ de: &de
send_signature_request_emails_without_limits_with_docuseal_pro: Unterschriftsanfragen ohne Limits mit DocuSeal Pro senden send_signature_request_emails_without_limits_with_docuseal_pro: Unterschriftsanfragen ohne Limits mit DocuSeal Pro senden
count_emails_used: '%{count} E-Mails verwendet' count_emails_used: '%{count} E-Mails verwendet'
has_been_connected: wurde verbunden has_been_connected: wurde verbunden
but_not_activated: aber nicht aktiviert
sms_not_configured: SMS nicht konfiguriert sms_not_configured: SMS nicht konfiguriert
configure_sms_settings_in_order_to_send_text_messages_: 'Konfiguriere die SMS-Einstellungen, um Textnachrichten zu senden:' configure_sms_settings_in_order_to_send_text_messages_: 'Konfiguriere die SMS-Einstellungen, um Textnachrichten zu senden:'
go_to_sms_settings: Zu den SMS-Einstellungen gehen go_to_sms_settings: Zu den SMS-Einstellungen gehen
@@ -4792,6 +4854,10 @@ de: &de
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".' users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".'
move_into_folder: In Ordner verschieben move_into_folder: In Ordner verschieben
new_folder_name: Neuer Ordnername new_folder_name: Neuer Ordnername
new_subfolder_name: Neuer Unterordnername
change_parent_folder: Übergeordneten Ordner ändern
folder: Ordner
create_a_new_folder: Neuen Ordner erstellen
exit_preview: Vorschau beenden exit_preview: Vorschau beenden
general: Allgemein general: Allgemein
recipients: Empfänger recipients: Empfänger
@@ -4824,7 +4890,6 @@ de: &de
edit_user: Benutzer bearbeiten edit_user: Benutzer bearbeiten
new_user: Neuer Benutzer new_user: Neuer Benutzer
unarchive: Entarchivieren unarchive: Entarchivieren
role: Rolle
last_session: Letzte Sitzung last_session: Letzte Sitzung
unlock_more_user_roles_with_docuseal_pro: Weitere Benutzerrollen mit DocuSeal Pro freischalten. unlock_more_user_roles_with_docuseal_pro: Weitere Benutzerrollen mit DocuSeal Pro freischalten.
view_active: Aktive anzeigen view_active: Aktive anzeigen
@@ -4891,8 +4956,6 @@ de: &de
new_webhook: Neuer Webhook new_webhook: Neuer Webhook
delete_webhook: Webhook löschen delete_webhook: Webhook löschen
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.' count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
gmail_has_been_connected: Gmail wurde verbunden.
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten. sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
connected_successfully: Erfolgreich verbunden. connected_successfully: Erfolgreich verbunden.
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.' user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
@@ -5111,6 +5174,19 @@ de: &de
email_verification: E-Mail-Verifizierung email_verification: E-Mail-Verifizierung
your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":' your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben. please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben.
advanced_settings: Erweiterte Einstellungen
text: Text
code: Code
custom_html_emails: Benutzerdefinierte HTML-E-Mails
connect_your_email_to_send_html_emails: Verbinden Sie Ihr E-Mail-Konto, um HTML-E-Mails zu senden
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Verbinden Sie Ihr Gmail- oder Outlook-Konto oder fügen Sie SMTP-Einstellungen hinzu, um benutzerdefinierte HTML-E-Mails zu senden.
connect_gmail_or_outlook: Gmail oder Outlook verbinden
connect_your_email_to_bulk_send: Verbinden Sie Ihr E-Mail-Konto für Massenversand
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Verbinden Sie Ihr Gmail- oder Outlook-Konto oder fügen Sie SMTP-Einstellungen für den Massenversand hinzu.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Möchten Sie wirklich Empfänger hinzufügen, ohne zu senden? Zum Senden von E-Mails ist eine Verbindung zu Gmail oder Outlook erforderlich.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
+1
View File
@@ -108,6 +108,7 @@ Rails.application.routes.draw do
resource :preferences, only: %i[show create], controller: 'templates_preferences' resource :preferences, only: %i[show create], controller: 'templates_preferences'
resource :share_link, only: %i[show create], controller: 'templates_share_link' resource :share_link, only: %i[show create], controller: 'templates_share_link'
resources :recipients, only: %i[create], controller: 'templates_recipients' resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
resources :submissions_export, only: %i[index new] resources :submissions_export, only: %i[index new]
end end
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid' resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddParentFolderIdToTemplateFolders < ActiveRecord::Migration[8.0]
def change
add_reference :template_folders, :parent_folder, foreign_key: { to_table: :template_folders }, index: true
end
end
+4 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "btree_gin" enable_extension "btree_gin"
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -356,8 +356,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do
t.datetime "archived_at" t.datetime "archived_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.bigint "parent_folder_id"
t.index ["account_id"], name: "index_template_folders_on_account_id" t.index ["account_id"], name: "index_template_folders_on_account_id"
t.index ["author_id"], name: "index_template_folders_on_author_id" t.index ["author_id"], name: "index_template_folders_on_author_id"
t.index ["parent_folder_id"], name: "index_template_folders_on_parent_folder_id"
end end
create_table "template_sharings", force: :cascade do |t| create_table "template_sharings", force: :cascade do |t|
@@ -498,6 +500,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do
add_foreign_key "submitters", "submissions" add_foreign_key "submitters", "submissions"
add_foreign_key "template_accesses", "templates" add_foreign_key "template_accesses", "templates"
add_foreign_key "template_folders", "accounts" add_foreign_key "template_folders", "accounts"
add_foreign_key "template_folders", "template_folders", column: "parent_folder_id"
add_foreign_key "template_folders", "users", column: "author_id" add_foreign_key "template_folders", "users", column: "author_id"
add_foreign_key "template_sharings", "templates" add_foreign_key "template_sharings", "templates"
add_foreign_key "templates", "accounts" add_foreign_key "templates", "accounts"
+14
View File
@@ -5,6 +5,7 @@ module Submissions
ALLOWED_PARAMS = %w[ ALLOWED_PARAMS = %w[
author author
status status
folder
completed_at_from completed_at_from
completed_at_to completed_at_to
created_at_from created_at_from
@@ -24,6 +25,7 @@ module Submissions
filters = normalize_filter_params(params, current_user) filters = normalize_filter_params(params, current_user)
submissions = filter_by_author(submissions, filters, current_user) submissions = filter_by_author(submissions, filters, current_user)
submissions = filter_by_folder(submissions, filters, current_user)
submissions = filter_by_status(submissions, filters) submissions = filter_by_status(submissions, filters)
submissions = filter_by_created_at(submissions, filters) submissions = filter_by_created_at(submissions, filters)
@@ -34,6 +36,7 @@ module Submissions
return submissions if filters[:author].blank? return submissions if filters[:author].blank?
user = current_user.account.users.find_by(email: filters[:author]) user = current_user.account.users.find_by(email: filters[:author])
submissions.where(created_by_user_id: user&.id || -1) submissions.where(created_by_user_id: user&.id || -1)
end end
@@ -87,6 +90,17 @@ module Submissions
submissions submissions
end end
def filter_by_folder(submissions, filters, current_user)
return submissions if filters[:folder].blank?
folders =
TemplateFolders.filter_by_full_name(current_user.account.template_folders, filters[:folder])
folders += folders.preload(:subfolders).flat_map(&:subfolders)
submissions.joins(:template).where(templates: { folder_id: folders.map(&:id) })
end
def filter_by_completed_at(submissions, filters) def filter_by_completed_at(submissions, filters)
return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present? return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present?
+32 -6
View File
@@ -261,8 +261,6 @@ module Submissions
e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present? e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end end
submitter_field_counters = Hash.new { 0 }
info_rows = [ info_rows = [
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
@@ -298,25 +296,53 @@ module Submissions
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } }) composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
submitter_field_counters = Hash.new { 0 }
grouped_value_field_names = {}
skip_grouped_field_uuids = {}
submission.template_fields.each do |field|
next unless field['type'].in?(%w[signature initials])
submitter_field_counters[field['type']] += 1
next if field['submitter_uuid'] != submitter.uuid
value = submitter.values[field['uuid']]
field_name = field['title'].presence || field['name'].presence ||
"#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}"
if grouped_value_field_names[value]
skip_grouped_field_uuids[field['uuid']] = true
grouped_value_field_names[value] += ", #{field_name}"
else
grouped_value_field_names[value] = field_name
end
end
submitter_field_counters = Hash.new { 0 }
submission.template_fields.filter_map do |field| submission.template_fields.filter_map do |field|
submitter_field_counters[field['type']] += 1
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
next if field['type'] == 'heading' next if field['type'] == 'heading'
next if !with_audit_values && !field['type'].in?(%w[signature initials]) next if !with_audit_values && !field['type'].in?(%w[signature initials])
next if skip_grouped_field_uuids[field['uuid']]
submitter_field_counters[field['type']] += 1
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
field_name = field['title'].presence || field['name'].to_s field_name = grouped_value_field_names[value].presence || field['title'].presence || field['name'].to_s
[ [
composer.formatted_text_box( composer.formatted_text_box(
[ [
{ {
text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence || text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence ||
"#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}\n".upcase, "#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}".upcase,
font_size: 6 font_size: 6
} }
].compact_blank, ].compact_blank,
@@ -14,11 +14,14 @@ 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_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_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_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten) pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
@@ -31,7 +34,8 @@ module Submissions
submitters.preload(attachments_attachments: :blob).each_with_index do |s, index| submitters.preload(attachments_attachments: :blob).each_with_index do |s, index|
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_submitter_timezone:,
with_signature_id_reason:)
end end
template = submission.template template = submission.template
+19 -6
View File
@@ -139,11 +139,14 @@ module Submissions
def generate_pdfs(submitter) def generate_pdfs(submitter)
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_SUBMITTER_TIMEZONE_KEY]) AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true 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_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_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten) pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten)
@@ -188,11 +191,12 @@ module Submissions
end end
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_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_submitter_timezone: false, with_signature_id_reason: true)
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center) cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
attachments_data_cache = {} attachments_data_cache = {}
@@ -274,6 +278,10 @@ module Submissions
field_type = 'file' if field_type == 'image' && field_type = 'file' if field_type == 'image' &&
!submitter.attachments.find { |a| a.uuid == value }.image? !submitter.attachments.find { |a| a.uuid == value }.image?
if field_type == 'signature' && field.dig('preferences', 'with_signature_id').in?([true, false])
with_signature_id = field['preferences']['with_signature_id']
end
case field_type case field_type
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) } when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
@@ -295,10 +303,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
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \ if with_signature_id_reason
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ "#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
else
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
end
end end
reason_text = HexaPDF::Layout::TextFragment.create(reason_string, reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
+9 -12
View File
@@ -13,6 +13,8 @@ module Submitters
AccountConfig::REUSE_SIGNATURE_KEY, AccountConfig::REUSE_SIGNATURE_KEY,
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_SIGNATURE_ID_REASON_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
module_function module_function
@@ -31,20 +33,15 @@ module Submitters
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true 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_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY) policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
attrs = { completed_button:, attrs = { completed_button:, with_typed_signature:, with_confetti:,
with_typed_signature:, reuse_signature:, with_decline:, with_partial_download:,
with_confetti:, policy_links:, enforce_signing_order:, completed_message:,
reuse_signature:, require_signing_reason:, prefill_signature:, with_submitter_timezone:,
with_decline:, with_signature_id_reason:, with_signature_id: }
with_partial_download:,
policy_links:,
enforce_signing_order:,
completed_message:,
require_signing_reason:,
prefill_signature:,
with_signature_id: }
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
+75 -1
View File
@@ -3,15 +3,89 @@
module TemplateFolders module TemplateFolders
module_function module_function
def filter_by_full_name(template_folders, name)
return template_folders.none if name.blank?
parent_name, name = name.to_s.split(' / ', 2).map(&:squish)
if name.present?
parent_folder = template_folders.where(parent_folder_id: nil).find_by(name: parent_name)
else
name = parent_name
end
template_folders.where(name:, parent_folder:)
end
def search(folders, keyword) def search(folders, keyword)
return folders if keyword.blank? return folders if keyword.blank?
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%")) folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
end end
def filter_active_folders(template_folders, templates)
folder_exists =
templates.active.where(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.select(1).limit(1).arel.exists
subfolders_arel = TemplateFolder.arel_table.alias('subfolders')
subfolder_exists =
TemplateFolder.from(subfolders_arel)
.where(subfolders_arel[:parent_folder_id].eq(TemplateFolder.arel_table[:id]))
.where(
templates.active.where(Template.arel_table[:folder_id].eq(subfolders_arel[:id])).arel.exists
).select(1).limit(1).arel.exists
template_folders.where(folder_exists).or(template_folders.where(subfolder_exists))
end
def sort(template_folders, current_user, order)
case order
when 'used_at'
subquery =
Template.left_joins(:submissions)
.group(:folder_id)
.where(account_id: current_user.account_id)
.select(
:folder_id,
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
)
template_folders = template_folders.joins(
Template.arel_table
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.join_sources
)
template_folders.order(
Arel::Nodes::Case.new
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
.then(Template.arel_table[:submission_created_at_max])
.else(Template.arel_table[:updated_at_max])
.desc
)
when 'name'
template_folders.order(name: :asc)
else
template_folders.order(id: :desc)
end
end
def find_or_create_by_name(author, name) def find_or_create_by_name(author, name)
return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME
author.account.template_folders.create_with(author:, account: author.account).find_or_create_by(name:) parent_name, name = name.to_s.split(' / ', 2).map(&:squish)
if name.present?
parent_folder = author.account.template_folders.create_with(author:)
.find_or_create_by(name: parent_name, parent_folder_id: nil)
else
name = parent_name
end
author.account.template_folders.create_with(author:).find_or_create_by(name:, parent_folder:)
end end
end end
+3
View File
@@ -6,10 +6,12 @@
"@babel/plugin-transform-runtime": "7.21.4", "@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5", "@babel/preset-env": "7.21.5",
"@babel/runtime": "7.21.5", "@babel/runtime": "7.21.5",
"@codemirror/lang-html": "^6.4.9",
"@eid-easy/eideasy-widget": "^2.163.4", "@eid-easy/eideasy-widget": "^2.163.4",
"@github/catalyst": "^2.0.0-beta", "@github/catalyst": "^2.0.0-beta",
"@hotwired/turbo": "https://github.com/docusealco/turbo#main", "@hotwired/turbo": "https://github.com/docusealco/turbo#main",
"@hotwired/turbo-rails": "^7.3.0", "@hotwired/turbo-rails": "^7.3.0",
"@specious/htmlflow": "^1.1.0",
"@tabler/icons-vue": "^2.47.0", "@tabler/icons-vue": "^2.47.0",
"autocompleter": "^9.1.0", "autocompleter": "^9.1.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
@@ -17,6 +19,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"canvas-confetti": "^1.6.0", "canvas-confetti": "^1.6.0",
"codemirror": "^6.0.2",
"compression-webpack-plugin": "10.0.0", "compression-webpack-plugin": "10.0.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0", "css-minimizer-webpack-plugin": "^5.0.0",
+203 -1
View File
@@ -1030,6 +1030,112 @@
"@babel/helper-string-parser" "^7.25.9" "@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9"
"@codemirror/autocomplete@^6.0.0":
version "6.18.6"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb"
integrity sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/commands@^6.0.0":
version "6.8.1"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.8.1.tgz#639f5559d2f33f2582a2429c58cb0c1b925c7a30"
integrity sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.4.0"
"@codemirror/view" "^6.27.0"
"@lezer/common" "^1.1.0"
"@codemirror/lang-css@^6.0.0":
version "6.3.1"
resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz#763ca41aee81bb2431be55e3cfcc7cc8e91421a3"
integrity sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@lezer/common" "^1.0.2"
"@lezer/css" "^1.1.7"
"@codemirror/lang-html@^6.4.9":
version "6.4.9"
resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.9.tgz#d586f2cc9c341391ae07d1d7c545990dfa069727"
integrity sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/lang-css" "^6.0.0"
"@codemirror/lang-javascript" "^6.0.0"
"@codemirror/language" "^6.4.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/css" "^1.1.0"
"@lezer/html" "^1.3.0"
"@codemirror/lang-javascript@^6.0.0":
version "6.2.4"
resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz#eef2227d1892aae762f3a0f212f72bec868a02c5"
integrity sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.6.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/javascript" "^1.0.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0":
version "6.11.2"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.2.tgz#90d2d094cfbd14263bc5354ebd2445ee4e81bdc3"
integrity sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/lint@^6.0.0":
version "6.8.5"
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.5.tgz#9edaa808e764e28e07665b015951934c8ec3a418"
integrity sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.35.0"
crelt "^1.0.5"
"@codemirror/search@^6.0.0":
version "6.5.11"
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.11.tgz#a324ffee36e032b7f67aa31c4fb9f3e6f9f3ed63"
integrity sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0":
version "6.38.0"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.38.0.tgz#4486062b791a4247793e0953e05ae71a9e172217"
integrity sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==
dependencies:
"@codemirror/state" "^6.5.0"
crelt "^1.0.6"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0": "@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0":
version "0.5.7" version "0.5.7"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@@ -1237,6 +1343,57 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd"
integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==
"@lezer/css@^1.1.0", "@lezer/css@^1.1.7":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.3.0.tgz#296f298814782c2fad42a936f3510042cdcd2034"
integrity sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.3.0"
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b"
integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/html@^1.3.0":
version "1.3.10"
resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.10.tgz#1be9a029a6fe835c823b20a98a449a630416b2af"
integrity sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/javascript@^1.0.0":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.5.1.tgz#2a424a6ec29f1d4ef3c34cbccc5447e373618ad8"
integrity sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727"
integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==
dependencies:
"@lezer/common" "^1.0.0"
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1" version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@@ -1280,6 +1437,13 @@
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"
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
"@specious/htmlflow@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@specious/htmlflow/-/htmlflow-1.1.0.tgz#cc8319c2cc6b5e2e309fa213d5a1f54cc093d9eb"
integrity sha512-g41FgN5kfuGkOrWhhmVvFes2BzQ3msU5oR/8KPdXbkBt8UIHtwL+by9maItDIhu+BR7pxshABtaB/FqpESa9Mg==
dependencies:
htmlparser2 "^8.0.1"
"@tabler/icons-vue@^2.47.0": "@tabler/icons-vue@^2.47.0":
version "2.47.0" version "2.47.0"
resolved "https://registry.yarnpkg.com/@tabler/icons-vue/-/icons-vue-2.47.0.tgz#604608a6df673d035c0e8e33f0565eeffa3adf36" resolved "https://registry.yarnpkg.com/@tabler/icons-vue/-/icons-vue-2.47.0.tgz#604608a6df673d035c0e8e33f0565eeffa3adf36"
@@ -2172,6 +2336,19 @@ clone-deep@^4.0.1:
kind-of "^6.0.2" kind-of "^6.0.2"
shallow-clone "^3.0.0" shallow-clone "^3.0.0"
codemirror@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.2.tgz#4d3fea1ad60b6753f97ca835f2f48c6936a8946e"
integrity sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/commands" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/search" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
color-convert@^1.9.0: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -2351,6 +2528,11 @@ 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:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -2711,7 +2893,7 @@ enhanced-resolve@^5.17.1:
graceful-fs "^4.2.4" graceful-fs "^4.2.4"
tapable "^2.2.0" tapable "^2.2.0"
entities@^4.2.0: entities@^4.2.0, entities@^4.4.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
@@ -3511,6 +3693,16 @@ html-entities@^2.3.2:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
htmlparser2@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.3"
domutils "^3.0.1"
entities "^4.4.0"
http-deceiver@^1.2.7: http-deceiver@^1.2.7:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -5438,6 +5630,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
stylehacks@^6.0.0: stylehacks@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.0.0.tgz#9fdd7c217660dae0f62e14d51c89f6c01b3cb738" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.0.0.tgz#9fdd7c217660dae0f62e14d51c89f6c01b3cb738"
@@ -5845,6 +6042,11 @@ 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:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
watchpack@^2.4.0, watchpack@^2.4.1: watchpack@^2.4.0, watchpack@^2.4.1:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da"