Compare commits

...
46 Commits
Author SHA1 Message Date
Pete Matsyburka 1b90f77e82 skip errors in pdf processor 2024-02-05 13:54:55 +02:00
Pete Matsyburka c37785ecbc update on-prem links 2024-02-03 22:52:41 +02:00
Pete Matsyburka c67188edf8 ability to share template with testing account 2024-02-03 22:37:24 +02:00
Pete Matsyburka 8630c68631 add account_id to submissions 2024-02-03 18:56:45 +02:00
Pete Matsyburka ddcb5c5d40 adjust params validator 2024-02-03 11:37:05 +02:00
Pete Matsyburka 07f0fbb50f add submitter metadata 2024-02-03 10:29:45 +02:00
Pete Matsyburka 3781b0aa72 application_key -> external_id 2024-02-03 10:29:45 +02:00
Pete Matsyburka 42a5cea521 remove verify link from api source 2024-02-02 19:29:01 +02:00
Pete Matsyburka fb6000ca2c add builder title container id 2024-02-01 20:59:03 +02:00
Pete Matsyburka 59f98db754 fix NL i18n 2024-02-01 15:14:52 +02:00
Pete Matsyburka 6add842a27 api match lowercase field names 2024-02-01 13:34:30 +02:00
Pete Matsyburka 43ca4a6365 update readme contact link 2024-02-01 12:48:20 +02:00
Pete Matsyburka 68196d6af0 better on prem console sign up 2024-01-31 02:52:43 +02:00
Pete Matsyburka a582d39bb9 add preview image url to templates 2024-01-30 17:53:25 +02:00
Pete Matsyburka 7a3e89c4d8 allow to disable resubmit button 2024-01-30 17:03:58 +02:00
Alex Turchyn 87f036ad88 optimize template page for mobile devices 2024-01-30 11:45:52 +02:00
Pete Matsyburka 5738b8dd58 fix submissions params validator 2024-01-30 01:46:30 +02:00
Pete Matsyburka 8d73371e0f do not add email signing link automatically with API 2024-01-29 23:37:22 +02:00
Pete Matsyburka 3d870b2305 remove i18n duplicate keys 2024-01-29 21:08:57 +02:00
Pete Matsyburka 27d8cd57bd add dutch form translations 2024-01-29 19:53:27 +02:00
Pete Matsyburka 353b2e7a94 fix typed font 2024-01-29 17:44:55 +02:00
Pete Matsyburka d77bd87acb add account removal button 2024-01-29 01:30:06 +02:00
Pete Matsyburka bae7b924ae adjust testing audit log 2024-01-29 00:07:52 +02:00
Pete Matsyburka 89867fdde3 simplify submissions api 2024-01-28 14:43:13 +02:00
Pete Matsyburka c9a1f067ac make API fields match case insensitive 2024-01-28 13:47:17 +02:00
Pete Matsyburka eb26835d57 remove resubmit button from API submissions 2024-01-28 13:27:50 +02:00
Pete Matsyburka 7301661468 fix api params validator 2024-01-27 14:24:24 +02:00
Pete Matsyburka e4fe984a70 fix validator path 2024-01-26 23:03:39 +02:00
Pete Matsyburka 12ccf622c1 enable API errors 2024-01-26 22:26:55 +02:00
Pete Matsyburka 6c35ff6d1c make confetti configurable 2024-01-26 17:43:52 +02:00
Pete Matsyburka 1d7c84302e fix form area style 2024-01-26 17:24:01 +02:00
Pete Matsyburka f3e8889710 clear canvas on signature toggle 2024-01-26 17:12:08 +02:00
Pete Matsyburka 4b2f9ff306 improve form i18n 2024-01-26 16:45:32 +02:00
Pete Matsyburka b4ba642f8e add builder currencies prop 2024-01-26 10:43:37 +02:00
Pete Matsyburka 5c69b965c0 fix submission default values with 2+ submitters 2024-01-25 23:41:44 +02:00
Pete Matsyburka cba196385c do now show textarea toggle with validation pattern 2024-01-25 22:58:17 +02:00
Alex Turchyn 00abad56cb add the 'more_then' validator to the params validator 2024-01-25 01:36:50 +02:00
Pete Matsyburka d1070c4955 fix focus field border 2024-01-24 23:53:47 +02:00
Pete Matsyburka e84be523c6 add show field names prop 2024-01-24 23:20:53 +02:00
Pete Matsyburka 5d598e8268 improve radio checkboxes form 2024-01-24 22:28:45 +02:00
Pete Matsyburka 0e9d922204 fix normalize values when nil 2024-01-24 10:05:14 +02:00
Pete Matsyburka 80c1b04e83 fix mobile builder style 2024-01-24 01:11:04 +02:00
Pete Matsyburka b2fca3edf3 do not allow add new if parties predefined 2024-01-23 23:58:25 +02:00
Pete Matsyburka 2a292c8e80 add phone validation alert 2024-01-23 21:58:19 +02:00
Pete Matsyburka e6052ad0d8 validate email/phone submission format 2024-01-23 21:47:01 +02:00
Pete Matsyburka 8646e17d5a fix typo 2024-01-22 22:45:24 +02:00
94 changed files with 1392 additions and 556 deletions

No files matched your search

+1 -1
View File
@@ -44,7 +44,7 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#']
Metrics/AbcSize:
Max: 35
Max: 40
Metrics/ModuleLength:
Max: 500
+1 -1
View File
@@ -81,7 +81,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://calendly.com/kriti-docuseal/30min)
[Book a Meeting](https://www.docuseal.co/contact)
## License
@@ -7,6 +7,7 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY
].freeze
+12
View File
@@ -37,6 +37,18 @@ class AccountsController < ApplicationController
render :show, status: :unprocessable_entity
end
def destroy
authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current)
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
)
end
private
def load_account
+25 -17
View File
@@ -63,7 +63,7 @@ module Api
end
def create
Params::SubmissionCreateValidator.call(params, dry_run: true)
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
@@ -96,7 +96,8 @@ module Api
def create_submissions(template, params)
is_send_email = !params[:send_email].in?(['false', false])
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
if (emails = (params[:emails] || params[:email]).presence) &&
(params[:submission].blank? && params[:submitters].blank?)
Submissions.create_from_emails(template:,
user: current_user,
source: :api,
@@ -131,8 +132,8 @@ module Api
include: {
submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at
created_at updated_at],
methods: %i[status] },
created_at updated_at external_id metadata],
methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] },
created_by_user: { only: %i[id email first_name last_name] }
}
@@ -140,20 +141,27 @@ module Api
end
def submissions_params
key = params.key?(:submission) ? :submission : :submissions
permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}
]
params.permit(
key => [
[:send_email, :send_sms, :bcc_completed, :completed_redirect_url, {
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}]
]
).fetch(key, [])
if params.key?(:submitters)
params.permit(*permitted_attrs)
else
key = params.key?(:submission) ? :submission : :submissions
params.permit(
key => [permitted_attrs]
).fetch(key, [])
end
end
end
end
+9 -5
View File
@@ -7,7 +7,8 @@ module Api
def index
submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = paginate(
@@ -44,7 +45,7 @@ module Api
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
[normalized_params],
submitter_uuid: @submitter.uuid)
default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params)
@@ -69,8 +70,9 @@ module Api
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role, :completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body],
:send_email, :send_sms, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
)
end
@@ -82,7 +84,9 @@ module Api
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.application_key = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
assign_submission_fields(submitter.submission)
assign_preferences(submitter, attrs)
@@ -7,11 +7,13 @@ module Api
def create
authorize!(:manage, @template)
cloned_template = Templates::Clone.call(@template,
author: current_user,
name: params[:name],
application_key: params[:application_key],
folder_name: params[:folder_name])
cloned_template = Templates::Clone.call(
@template,
author: current_user,
name: params[:name],
external_id: params[:external_id].presence || params[:application_key],
folder_name: params[:folder_name]
)
cloned_template.source = :api
cloned_template.save!
+13 -6
View File
@@ -5,11 +5,7 @@ module Api
load_and_authorize_resource :template
def index
templates = Templates.search(@templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, documents_attachments: :blob))
@@ -49,10 +45,21 @@ module Api
private
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates
end
def serialize_params
{
methods: %i[application_key],
include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } }
documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
}
end
+7 -1
View File
@@ -52,7 +52,13 @@ class DashboardController < ApplicationController
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
if params[:q].blank?
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
Templates.search(rel, params[:q])
end
+5 -2
View File
@@ -10,7 +10,8 @@ class StartFormController < ApplicationController
before_action :load_template
def show
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid'])
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: @template.submitters.first['uuid'])
end
def update
@@ -57,7 +58,8 @@ class StartFormController < ApplicationController
ip: request.remote_ip,
ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true }
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {}
)
if submitter.values.present?
@@ -67,6 +69,7 @@ class StartFormController < ApplicationController
end
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
source: :link)
+1 -1
View File
@@ -75,6 +75,6 @@ class SubmissionsController < ApplicationController
end
def load_template
@template = current_account.templates.find(params[:template_id])
@template = Template.accessible_by(current_ability).find(params[:template_id])
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class TemplateSharingsTestingController < ApplicationController
load_and_authorize_resource :template, parent: true
before_action do
authorize!(:manage, TemplateSharing.new(template: @template))
end
def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
else
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
end
head :ok
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class TemplatesCodeModalController < ApplicationController
load_and_authorize_resource :template
def show; end
end
+4 -3
View File
@@ -6,7 +6,7 @@ class TemplatesController < ApplicationController
before_action :load_base_template, only: %i[new create]
def show
submissions = @template.submissions
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q])
@@ -47,11 +47,12 @@ class TemplatesController < ApplicationController
name: params.dig(:template, :name),
folder_name: params[:folder_name])
else
@template.account = current_account
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
@template.account = current_account
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -87,6 +88,6 @@ class TemplatesController < ApplicationController
def load_base_template
return if params[:base_template_id].blank?
@base_template = current_account.templates.find_by(id: params[:base_template_id])
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template
def show
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: [:metadata],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :show, layout: 'plain'
end
end
@@ -7,7 +7,7 @@ class TestingAccountsController < ApplicationController
authorize!(:manage, current_account)
authorize!(:manage, current_user)
impersonate_user(Accounts.find_or_create_testing_user(current_account))
impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
redirect_back(fallback_location: root_path)
end
+2
View File
@@ -88,7 +88,9 @@ window.customElements.define('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true'
})
+1 -1
View File
@@ -15,7 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false',
withConfetti: true,
withConfetti: this.dataset.withConfetti !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false',
values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton),
+24 -21
View File
@@ -1,9 +1,9 @@
<template>
<div
class="flex absolute lg:text-base"
class="flex absolute lg:text-base -outline-offset-1"
dir="auto"
:style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed border-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
>
<div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -81,7 +81,8 @@
</div>
<div
v-else-if="field.type === 'checkbox'"
class="w-full p-[0.2vw] flex items-center justify-center"
class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
>
<input
v-if="submittable"
@@ -100,14 +101,15 @@
</div>
<div
v-else-if="field.type === 'radio' && area.option_uuid"
class="w-full p-[0.2vw] flex items-center justify-center"
class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
>
<input
v-if="submittable"
type="radio"
:value="false"
class="aspect-square base-radio"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
class="aspect-square checked:checkbox checked:checkbox-xs"
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', optionValue(option))"
>
@@ -119,7 +121,8 @@
</div>
<div
v-else-if="field.type === 'multiple' && area.option_uuid"
class="w-full p-[0.2vw] flex items-center justify-center"
class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
>
<input
v-if="submittable"
@@ -233,20 +236,20 @@ export default {
computed: {
fieldNames () {
return {
text: 'Text',
cells: 'Text',
signature: 'Signature',
date: 'Date',
image: 'Image',
stamp: 'Stamp',
initials: 'Initials',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
radio: 'Radio',
multiple: 'Multiple Select',
phone: 'Phone',
payment: 'Payment'
text: this.t('text'),
signature: this.t('signature'),
initials: this.t('initials'),
date: this.t('date'),
image: this.t('image'),
file: this.t('file'),
select: this.t('select'),
checkbox: this.t('checkbox'),
multiple: this.t('multiple'),
radio: this.t('radio'),
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
}
},
option () {
@@ -42,7 +42,7 @@
>
</template>
<FileDropzone
:message="`Upload ${field.name || 'Attachments'}${field.required ? '' : ' (optional)'}`"
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:is-direct-upload="isDirectUpload"
:multiple="true"
@@ -62,6 +62,7 @@ export default {
IconPaperclip,
IconTrashX
},
inject: ['t'],
props: {
field: {
type: Object,
+6 -1
View File
@@ -4,7 +4,7 @@
<label
:for="field.uuid"
class="label text-2xl"
>{{ field.name || t('date') }}
>{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
<button
@@ -46,6 +46,11 @@ export default {
type: Object,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: String,
required: false,
+45 -8
View File
@@ -4,7 +4,7 @@
:steps="stepFields"
:values="values"
:attachments-index="attachmentsIndex"
:with-label="!isAnonymousChecboxes"
:with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/>
@@ -64,6 +64,7 @@
<TextStep
:key="currentField.uuid"
v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField"
@focus="scrollIntoField(currentField)"
/>
@@ -72,12 +73,13 @@
v-else-if="currentField.type === 'date'"
:key="currentField.uuid"
v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField"
@focus="scrollIntoField(currentField)"
/>
<div v-else-if="currentField.type === 'select'">
<label
v-if="currentField.name"
v-if="showFieldNames && currentField.name"
:for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2"
@@ -116,15 +118,28 @@
</div>
<div v-else-if="currentField.type === 'radio'">
<label
v-if="currentField.name"
v-if="showFieldNames && currentField.name"
:for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</label>
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div class="flex w-full max-h-44 overflow-y-auto">
<div
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
class="text-xl px-1"
>
<span
@click="scrollIntoField(currentField)"
>
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
>
<div
v-for="(option, index) in currentField.options"
:key="option.uuid"
@@ -154,6 +169,8 @@
v-else-if="currentField.type === 'multiple'"
:key="currentField.uuid"
v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:is-last-step="stepFields.length === currentStep + 1"
:field="currentField"
/>
<div
@@ -168,7 +185,7 @@
<div
class="space-y-3.5 mx-auto"
>
<template v-if="isAnonymousChecboxes">
<template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span>
@@ -228,6 +245,7 @@
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
:show-field-names="showFieldNames"
@attached="[attachments.push($event), scrollIntoField(currentField)]"
/>
<SignatureStep
@@ -241,6 +259,7 @@
:with-typed-signature="withTypedSignature"
:attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug"
:show-field-names="showFieldNames"
@attached="attachments.push($event)"
@start="scrollIntoField(currentField)"
@minimize="isFormVisible = false"
@@ -254,6 +273,7 @@
:previous-value="previousInitialsValue"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex"
:show-field-names="showFieldNames"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
@start="scrollIntoField(currentField)"
@@ -276,6 +296,7 @@
:key="currentField.uuid"
v-model="values[currentField.uuid]"
:field="currentField"
:show-field-names="showFieldNames"
:default-value="submitter.phone"
:submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)"
@@ -376,7 +397,7 @@ import DateStep from './date_step'
import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import { t } from './i18n'
import i18n from './i18n'
export default {
name: 'SubmissionForm',
@@ -401,6 +422,7 @@ export default {
return {
baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea,
scrollIntoField: this.scrollIntoField,
t: this.t
}
},
@@ -431,6 +453,11 @@ export default {
required: false,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
withTypedSignature: {
type: Boolean,
required: false,
@@ -481,6 +508,11 @@ export default {
required: false,
default: true
},
language: {
type: String,
required: false,
default: ''
},
values: {
type: Object,
required: false,
@@ -522,6 +554,9 @@ export default {
currentStepFields () {
return this.stepFields[this.currentStep]
},
browserLanguage () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
},
queryParams () {
return new URLSearchParams(window.location.search)
},
@@ -639,7 +674,9 @@ export default {
})
},
methods: {
t,
t (key) {
return i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
},
maybeTrackEmailClick () {
const { queryParams } = this
+253 -45
View File
@@ -1,4 +1,19 @@
const en = {
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
take_photo: 'Take photo',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
submit_form: 'Submit Form',
type_here_: 'Type here...',
optional: 'optional',
@@ -13,8 +28,6 @@ const en = {
or_drag_and_drop_files: 'or drag and drop files',
send_copy_via_email: 'Send copy via email',
download: 'Download',
signature: 'Signature',
initials: 'Initials',
clear: 'Clear',
redraw: 'Redraw',
draw_initials: 'Draw initials',
@@ -40,13 +53,30 @@ const en = {
draw: 'Draw',
type: 'Type',
type_text: 'Type text',
date: 'Date',
email_has_been_sent: 'Email has been sent',
processing: 'Processing',
pay_with_strip: 'Pay with Stripe'
pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files'
}
const es = {
text: 'Texto',
signature: 'Firma',
initials: 'Iniciales',
date: 'Fecha',
image: 'Imagen',
file: 'Archivo',
select: 'Seleccionar',
checkbox: 'Casilla',
multiple: 'Múltiple',
radio: 'Radio',
cells: 'Celdas',
stamp: 'Sello',
payment: 'Pago',
phone: 'Teléfono',
take_photo: 'Tomar foto',
submit_form: 'Enviar Formulario',
type_here_: 'Escribe aquí...',
optional: 'opcional',
@@ -60,8 +90,6 @@ const es = {
or_drag_and_drop_files: 'o arrastra y suelta archivos',
send_copy_via_email: 'Enviar copia por correo electrónico',
download: 'Descargar',
signature: 'Firma',
initials: 'Iniciales',
clear: 'Borrar',
redraw: 'Redibujar',
draw_initials: 'Dibujar iniciales',
@@ -81,7 +109,6 @@ const es = {
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
set_today: 'Establecer Hoy',
date: 'Fecha',
toggle_multiline_text: 'Alternar Texto Multilínea',
draw_signature: 'Dibujar firma',
type_initial: 'Escribir iniciales',
@@ -90,15 +117,33 @@ const es = {
type_text: 'Escribir texto',
email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando',
pay_with_strip: 'Pagar con Stripe'
pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos'
}
const it = {
text: 'Testo',
signature: 'Firma',
initials: 'Iniziali',
date: 'Data',
image: 'Immagine',
file: 'File',
select: 'Seleziona',
checkbox: 'Casella di controllo',
multiple: 'Multiplo',
radio: 'Radio',
cells: 'Celle',
stamp: 'Timbro',
payment: 'Pagamento',
phone: 'Telefono',
submit_form: 'Invia Modulo',
type_here_: 'Digita qui...',
optional: 'opzionale',
appears_on: 'Compare su',
page: 'Pagina',
take_photo: 'Scattare una foto',
select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia',
@@ -107,8 +152,6 @@ const it = {
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
send_copy_via_email: 'Invia copia via email',
download: 'Scarica',
signature: 'Firma',
initials: 'Iniziali',
clear: 'Cancella',
redraw: 'Ridisegna',
draw_initials: 'Disegna iniziali',
@@ -128,7 +171,6 @@ const it = {
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
set_today: 'Imposta Oggi',
date: 'Data',
draw_signature: 'Disegna firma',
type_initial: 'Inserisci iniziali',
draw: 'Disegna',
@@ -137,15 +179,33 @@ const it = {
toggle_multiline_text: 'Attiva Testo Multilinea',
email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione',
pay_with_strip: 'Paga con Stripe'
pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File'
}
const de = {
text: 'Text',
signature: 'Unterschrift',
initials: 'Initialen',
date: 'Datum',
image: 'Bild',
file: 'Datei',
select: 'Auswählen',
checkbox: 'Checkbox',
multiple: 'Mehrere',
radio: 'Radio',
cells: 'Zellen',
stamp: 'Stempel',
payment: 'Zahlung',
phone: 'Telefon',
submit_form: 'Formular absenden',
type_here_: 'Hier eingeben...',
optional: 'optional',
appears_on: 'Erscheint auf',
page: 'Seite',
take_photo: 'Foto aufnehmen',
select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden',
@@ -154,8 +214,6 @@ const de = {
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
send_copy_via_email: 'Kopie per E-Mail senden',
download: 'Herunterladen',
signature: 'Unterschrift',
initials: 'Initialen',
clear: 'Löschen',
redraw: 'Neu zeichnen',
draw_initials: 'Initialen zeichnen',
@@ -175,7 +233,6 @@ const de = {
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
set_today: 'Heute einstellen',
date: 'Datum',
draw_signature: 'Unterschrift zeichnen',
type_initial: 'Initialen eingeben',
draw: 'Zeichnen',
@@ -184,15 +241,33 @@ const de = {
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
email_has_been_sent: 'Die E-Mail wurde gesendet',
processing: 'Verarbeitung',
pay_with_strip: 'Mit Stripe bezahlen'
pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien'
}
const fr = {
text: 'Texte',
signature: 'Signature',
initials: 'Initiales',
date: 'Date',
image: 'Image',
file: 'Fichier',
select: 'Choisir',
checkbox: 'Coche',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cellules',
stamp: 'Tampon',
payment: 'Paiement',
phone: 'Téléphone',
submit_form: 'Envoyer le Formulaire',
type_here_: 'Tapez ici...',
optional: 'facultatif',
appears_on: 'Apparaît sur',
page: 'Page',
take_photo: 'Prendre une photo',
select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer',
@@ -201,8 +276,6 @@ const fr = {
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
send_copy_via_email: 'Envoyer une copie par e-mail',
download: 'Télécharger',
signature: 'Signature',
initials: 'Initiales',
clear: 'Effacer',
redraw: 'Redessiner',
draw_initials: 'Dessiner les initiales',
@@ -222,7 +295,6 @@ const fr = {
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
set_today: "Définir Aujourd'hui",
date: 'Date',
draw_signature: 'Dessiner une signature',
type_initial: 'Saisir les initiales',
draw: 'Dessiner',
@@ -231,10 +303,27 @@ const fr = {
toggle_multiline_text: 'Basculer le Texte Multiligne',
email_has_been_sent: "L'email a été envoyé",
processing: 'Traitement',
pay_with_strip: 'Paiement avec Stripe'
pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers'
}
const pl = {
text: 'Tekst',
signature: 'Podpis',
initials: 'Inicjały',
date: 'Data',
image: 'Obraz',
file: 'Plik',
select: 'Wybierz',
checkbox: 'Pole wyboru',
multiple: 'Wielokrotne',
radio: 'Radio',
cells: 'Komórki',
stamp: 'Pieczątka',
payment: 'Płatność',
phone: 'Telefon',
submit_form: 'Wyślij Formularz',
type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny',
@@ -244,12 +333,11 @@ const pl = {
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij',
next: 'dalej',
take_photo: 'Zrób zdjęcie',
click_to_upload: 'Kliknij, aby przesłać',
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
send_copy_via_email: 'Wyślij kopię drogą mailową',
download: 'Pobierz',
signature: 'Podpis',
initials: 'Inicjały',
clear: 'Wyczyść',
redraw: 'Przerysuj',
draw_initials: 'Narysuj inicjały',
@@ -269,7 +357,6 @@ const pl = {
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
set_today: 'Ustaw Dziś',
date: 'Data',
draw_signature: 'Rysuj podpis',
type_initial: 'Wprowadź inicjały',
draw: 'Rysuj',
@@ -278,15 +365,33 @@ const pl = {
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie',
pay_with_strip: 'Płatność za pomocą Stripe'
pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki'
}
const uk = {
text: 'Текст',
signature: 'Підпис',
initials: 'Ініціали',
date: 'Дата',
image: 'Зображення',
file: 'Файл',
select: 'Вибрати',
checkbox: 'Прапорець',
multiple: 'Множинний',
radio: 'Радіо',
cells: 'Клітинки',
stamp: 'Штамп',
payment: 'Платіж',
phone: 'Телефон',
submit_form: 'Надіслати Форму',
type_here_: 'Введіть тут',
optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
take_photo: 'Зробити фото',
select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати',
@@ -295,8 +400,6 @@ const uk = {
or_drag_and_drop_files: 'або перетягніть файли сюди',
send_copy_via_email: 'Надіслати копію електронною поштою',
download: 'Завантажити',
signature: 'Підпис',
initials: 'Ініціали',
clear: 'Очистити',
redraw: 'Перемалювати',
draw_initials: 'Намалювати ініціали',
@@ -316,7 +419,6 @@ const uk = {
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
set_today: 'Задати Сьогодні',
date: 'Дата',
draw_signature: 'Намалюйте підпис',
type_initial: 'Введіть ініціали',
draw: 'Підпис',
@@ -325,16 +427,34 @@ const uk = {
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка',
pay_with_strip: 'Сплатити за допомогою Stripe'
pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
}
const cs = {
text: 'Text',
signature: 'Podpis',
initials: 'Iniciály',
date: 'Datum',
image: 'Obrázek',
file: 'Soubor',
select: 'Vybrat',
checkbox: 'Zaškrtávací políčko',
multiple: 'Více možností',
radio: 'Rádio',
cells: 'Buňky',
stamp: 'Razítko',
payment: 'Platba',
phone: 'Telefon',
submit_form: 'Odeslat formulář',
type_here_: 'Zadejte zde',
optional: 'volitelné',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu',
take_photo: 'Fotit',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat',
next: 'další',
@@ -342,8 +462,6 @@ const cs = {
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
send_copy_via_email: 'Odeslat kopii e-mailem',
download: 'Stáhnout',
signature: 'Podpis',
initials: 'Iniciály',
clear: 'Smazat',
redraw: 'Překreslit',
draw_initials: 'Nakreslit iniciály',
@@ -363,7 +481,6 @@ const cs = {
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
set_today: 'Nastavit Dnes',
date: 'Datum',
draw_signature: 'Nakreslit podpis',
type_initial: 'Zadat iniciály',
draw: 'Kreslit',
@@ -372,15 +489,33 @@ const cs = {
toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování',
pay_with_strip: 'Zaplacení přes Stripe'
pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory'
}
const pt = {
text: 'Texto',
signature: 'Assinatura',
initials: 'Iniciais',
date: 'Data',
image: 'Imagem',
file: 'Arquivo',
select: 'Selecionar',
checkbox: 'Caixa',
multiple: 'Múltiplo',
radio: 'Rádio',
cells: 'Células',
stamp: 'Carimbo',
payment: 'Pagamento',
phone: 'Telefone',
submit_form: 'Enviar Formulário',
type_here_: 'Digite aqui',
optional: 'opcional',
appears_on: 'Aparece em',
page: 'Página',
take_photo: 'Tirar foto',
select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar',
@@ -389,8 +524,6 @@ const pt = {
or_drag_and_drop_files: 'ou arraste e solte arquivos',
send_copy_via_email: 'Enviar cópia por e-mail',
download: 'Baixar',
signature: 'Assinatura',
initials: 'Iniciais',
clear: 'Limpar',
redraw: 'Redesenhar',
draw_initials: 'Desenhar iniciais',
@@ -410,7 +543,6 @@ const pt = {
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
set_today: 'Definir Hoje',
date: 'Data',
draw_signature: 'Desenhar assinatura',
type_initial: 'Inserir iniciais',
draw: 'Desenhar',
@@ -419,16 +551,34 @@ const pt = {
toggle_multiline_text: 'Alternar Texto Multilinha',
email_has_been_sent: 'Email enviado',
processing: 'Processamento',
pay_with_strip: 'Pagar com Stripe'
pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos'
}
const he = {
text: 'טקסט',
signature: 'חתימה',
initials: 'ראשי תיקיות',
date: 'תאריך',
image: 'תמונה',
file: 'קובץ',
select: 'בחר',
checkbox: 'תיק בחירה',
multiple: 'רב ערכים',
radio: 'רדיו',
cells: 'תאים',
stamp: 'חותמת',
payment: 'תשלום',
phone: 'טלפון',
submit_form: 'שלח טופס',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
appears_on: 'מופיע ב',
page: 'עמוד',
take_photo: 'צלם תמונה',
select_your_option: 'בחר את האפשרות שלך',
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
submit: 'שליחה',
@@ -437,8 +587,6 @@ const he = {
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
send_copy_via_email: 'שלח עותק באמצעות אימייל',
download: 'הורדה',
signature: 'חתימה',
initials: 'ציוני ראשי תיבות',
clear: 'נקה',
redraw: 'צייר מחדש',
draw_initials: 'צייר ציוני ראשי תיבות',
@@ -464,17 +612,77 @@ const he = {
draw: 'צייר',
type: 'הקלד',
type_text: 'הקלד טקסט',
date: 'תאריך',
email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד',
pay_with_strip: 'שלם עם סטרייפ'
pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he }
const nl = {
text: 'Tekst',
signature: 'Handtekening',
initials: 'Initialen',
date: 'Datum',
image: 'Afbeelding',
take_photo: 'Maak een foto',
file: 'Bestand',
select: 'Selecteer',
checkbox: 'Aankruisvakje',
multiple: 'Meerdere',
radio: 'Radio',
cells: 'Cellen',
stamp: 'Stempel',
payment: 'Betaling',
phone: 'Telefoon',
submit_form: 'Formulier verzenden',
type_here_: 'Typ hier...',
optional: 'Optioneel',
option: 'Optie',
appears_on: 'Verschijnt op',
page: 'Pagina',
select_your_option: 'Selecteer uw optie',
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
submit: 'Voltooien',
next: 'Volgende',
click_to_upload: 'Klik om te uploaden',
or_drag_and_drop_files: 'of sleep en plaats bestanden',
send_copy_via_email: 'Stuur kopie per e-mail',
download: 'Downloaden',
clear: 'Wissen',
redraw: 'Opnieuw tekenen',
draw_initials: 'Initialen tekenen',
type_signature_here: 'Typ hier uw handtekening',
type_initial_here: 'Typ hier uw initialen',
form_has_been_completed: 'Formulier is voltooid!',
create_a_free_account: 'Maak een gratis account aan',
signed_with: 'Ondertekend met',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer',
sending: 'Voltooien...',
resend_code: 'Code opnieuw verzenden',
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
please_fill_all_required_fields: 'Vul alle verplichte velden in',
set_today: 'Vandaag',
toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
draw_signature: 'Handtekening tekenen',
type_initial: 'Typ initialen',
draw: 'Tekenen',
type: 'Typen',
type_text: 'Typ tekst',
email_has_been_sent: 'E-mail is verzonden',
processing: 'Verwerken',
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden'
}
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
export default i18n
export { t }
@@ -3,13 +3,13 @@
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ field.name || 'Image' }}</label>
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
<button
class="btn btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
Reupload
{{ t('reupload') }}
</button>
</div>
<div>
@@ -27,7 +27,7 @@
<div>
<FileDropzone
v-if="!modelValue"
:message="`Upload ${field.name || 'Image'}${field.required ? '' : ' (optional)'}`"
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:accept="'image/*'"
:is-direct-upload="isDirectUpload"
@@ -46,6 +46,7 @@ export default {
FileDropzone,
IconReload
},
inject: ['t'],
props: {
field: {
type: Object,
@@ -56,6 +57,11 @@ export default {
required: true,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
submitterSlug: {
type: String,
required: true
@@ -3,7 +3,7 @@
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ field.name || t('initials') }}</label>
>{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
<div class="space-x-2 flex">
<span
v-if="isDrawInitials"
@@ -125,6 +125,11 @@ export default {
type: String,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: {
type: Boolean,
required: true,
@@ -1,11 +1,11 @@
<template>
<label
v-if="field.name"
v-if="showFieldNames && field.name"
:for="field.uuid"
dir="auto"
class="label text-2xl mb-2"
>{{ field.name }}</label>
<div class="flex w-full">
<div class="flex w-full max-h-44 overflow-y-auto">
<input
v-if="modelValue.length === 0"
type="text"
@@ -13,7 +13,18 @@
:value="''"
class="hidden"
>
<div class="space-y-3.5 mx-auto">
<div
v-if="!showOptions"
class="text-xl px-1"
>
<span @click="scrollIntoField(field)">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ isLastStep ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showOptions }"
>
<div
v-for="(option, index) in field.options"
:key="option.uuid"
@@ -44,12 +55,22 @@
<script>
export default {
name: 'MultiSelectStep',
inject: ['t'],
inject: ['t', 'scrollIntoField'],
props: {
field: {
type: Object,
required: true
},
isLastStep: {
type: Boolean,
required: true,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: Array,
required: false,
@@ -62,6 +83,11 @@ export default {
inputRefs: []
}
},
computed: {
showOptions () {
return this.showFieldNames && (this.field.options.some((e) => e.value) || this.field.options.length < 5)
}
},
beforeUpdate () {
this.inputRefs = []
},
+11 -2
View File
@@ -3,7 +3,7 @@
<label
:for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-2xl mb-2"
>{{ field.name || t('verified_phone_number') }}
>{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
<div>
@@ -93,6 +93,11 @@ export default {
type: String,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: String,
required: false,
@@ -140,7 +145,11 @@ export default {
})
},
async submit () {
if (!this.isCodeSent) {
if (!this.$refs.phone.value.toString().startsWith('+')) {
alert(this.t('use_international_format'))
return Promise.reject(new Error('phone invalid'))
} else if (!this.isCodeSent) {
this.sendVerificationCode()
this.$emit('update:model-value', this.$refs.phone.value)
@@ -3,7 +3,7 @@
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ field.name || t('signature') }}</label>
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
<div class="space-x-2 flex">
<span
v-if="isTextSignature"
@@ -41,7 +41,7 @@
</span>
<span
class="tooltip"
data-tip="Take photo"
:data-tip="t('take_photo')"
>
<label
class="btn btn-outline btn-sm font-medium"
@@ -147,6 +147,11 @@ export default {
type: String,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: {
type: Boolean,
required: true,
@@ -221,7 +226,7 @@ export default {
},
loadFont () {
if (!isFontLoaded) {
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
font.load().then((loadedFont) => {
document.fonts.add(loadedFont)
@@ -260,6 +265,7 @@ export default {
},
toggleTextInput () {
this.remove()
this.clear()
this.isTextSignature = !this.isTextSignature
if (this.isTextSignature) {
+9 -3
View File
@@ -1,6 +1,6 @@
<template>
<label
v-if="field.name"
v-if="showFieldNames && field.name"
:for="field.uuid"
dir="auto"
class="label text-2xl"
@@ -26,7 +26,8 @@
v-model="text"
:maxlength="cellsMaxLegth"
dir="auto"
class="base-input !text-2xl w-full !pr-11 -mr-10"
class="base-input !text-2xl w-full"
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
:required="field.required"
:pattern="field.validation?.pattern"
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
@@ -50,7 +51,7 @@
@focus="$emit('focus')"
/>
<div
v-if="!isTextArea && field.type !== 'cells'"
v-if="!isTextArea && field.type !== 'cells' && !field.validation?.pattern"
class="tooltip"
:data-tip="t('toggle_multiline_text')"
>
@@ -81,6 +82,11 @@ export default {
type: Object,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: String,
required: false,
+7 -1
View File
@@ -43,7 +43,8 @@
class="border-r"
:compact="true"
:editable="editable && !defaultField"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
:allow-add-new="!defaultSubmitters.length"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
:submitters="template.submitters"
@update:model-value="save"
@click="selectedAreaRef.value = area"
@@ -166,6 +167,11 @@ export default {
required: false,
default: null
},
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
editable: {
type: Boolean,
required: false,
+39 -19
View File
@@ -5,6 +5,7 @@
>
<div
v-if="$slots.buttons || withTitle"
id="title_container"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
:class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }"
@@ -61,6 +62,7 @@
</span>
</a>
<button
v-if="editable"
class="base-button"
:class="{ disabled: isSaving }"
v-bind="isSaving ? { disabled: true } : {}"
@@ -79,6 +81,15 @@
{{ t('save') }}
</span>
</button>
<a
v-else
:href="`/templates/${template.id}`"
class="base-button"
>
<span class="hidden md:inline">
{{ t('back') }}
</span>
</a>
</template>
</div>
</div>
@@ -148,6 +159,7 @@
:is-drag="!!dragField"
:default-fields="defaultFields"
:allow-draw="!onlyDefinedFields"
:default-submitters="defaultSubmitters"
:draw-field="drawField"
:editable="editable"
:base-url="baseUrl"
@@ -185,25 +197,6 @@
</div>
</template>
</div>
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<MobileFields
v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields"
:default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
</div>
<div
v-if="withFieldsList"
@@ -259,6 +252,27 @@
</div>
</div>
</div>
<div class="sticky bottom-0">
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<MobileFields
v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields"
:default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
</div>
</div>
</template>
@@ -301,6 +315,7 @@ export default {
template: this.template,
save: this.save,
t: this.t,
currencies: this.currencies,
baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor,
@@ -349,6 +364,11 @@ export default {
required: false,
default: () => []
},
currencies: {
type: Array,
required: false,
default: () => []
},
fieldTypes: {
type: Array,
required: false,
@@ -10,6 +10,7 @@
:allow-draw="allowDraw"
:is-drag="isDrag"
:default-fields="defaultFields"
:default-submitters="defaultSubmitters"
:draw-field="drawField"
:selected-submitter="selectedSubmitter"
:image="image"
@@ -42,6 +43,11 @@ export default {
required: false,
default: () => []
},
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
allowDraw: {
type: Boolean,
required: false,
@@ -125,7 +125,8 @@
/>
</div>
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
<IconPlus
<component
:is="editable ? 'IconPlus' : 'IconChevronDown'"
width="18"
height="18"
/>
@@ -188,7 +189,7 @@
</div>
</a>
</li>
<li v-if="submitters.length < 10 && editable">
<li v-if="submitters.length < 10 && editable && allowAddNew">
<a
href="#"
class="flex px-2"
@@ -208,7 +209,7 @@
</template>
<script>
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } from '@tabler/icons-vue'
import Contenteditable from './contenteditable'
import { v4 } from 'uuid'
@@ -216,6 +217,7 @@ export default {
name: 'FieldSubmitter',
components: {
IconUserPlus,
IconChevronDown,
Contenteditable,
IconPlus,
IconTrashX,
@@ -242,6 +244,11 @@ export default {
required: false,
default: false
},
allowAddNew: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: String,
required: true
@@ -1,5 +1,5 @@
<template>
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
<div class="absolute text-center w-full bottom-0 pr-3 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
<div class="flex items-center space-x-2">
<component
@@ -1,6 +1,6 @@
<template>
<span
class="dropdown dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 md:hidden"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
+6
View File
@@ -24,6 +24,7 @@
:field="item.field"
:editable="editable"
:default-field="defaultFields.find((f) => f.name === item.field.name)"
:default-submitters="defaultSubmitters"
@start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null"
@start-drag="isMove = true"
@@ -85,6 +86,11 @@ export default {
type: Object,
required: true
},
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
drawField: {
type: Object,
required: false,
@@ -31,7 +31,7 @@
@change="save"
>
<option
v-for="currency in currencies"
v-for="currency in currenciesList"
:key="currency"
:value="currency"
>
@@ -167,7 +167,7 @@ export default {
IconInnerShadowTop,
IconBrandStripe
},
inject: ['backgroundColor', 'save'],
inject: ['backgroundColor', 'save', 'currencies'],
props: {
field: {
type: Object,
@@ -187,9 +187,12 @@ export default {
redirectUri () {
return document.location.origin + '/auth/stripe_connect/callback'
},
currencies () {
defaultCurrencies () {
return ['USD', 'EUR', 'GBP']
},
currenciesList () {
return this.currencies.length ? this.currencies : this.defaultCurrencies
},
authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content
},
+2
View File
@@ -27,8 +27,10 @@ class AccountConfig < ApplicationRecord
BCC_EMAILS = 'bcc_emails'
FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
SUBMITTER_REMAILERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DEFAULT_VALUES = {
+3 -1
View File
@@ -15,11 +15,13 @@
# template_submitters :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# created_by_user_id :bigint
# template_id :bigint not null
#
# Indexes
#
# index_submissions_on_account_id (account_id)
# index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id)
@@ -31,7 +33,7 @@
#
class Submission < ApplicationRecord
belongs_to :template
has_one :account, through: :template
belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true
has_many :submitters, dependent: :destroy
+25 -18
View File
@@ -4,23 +4,24 @@
#
# Table name: submitters
#
# id :bigint not null, primary key
# application_key :string
# completed_at :datetime
# email :string
# ip :string
# name :string
# opened_at :datetime
# phone :string
# preferences :text not null
# sent_at :datetime
# slug :string not null
# ua :string
# uuid :string not null
# values :text not null
# created_at :datetime not null
# updated_at :datetime not null
# submission_id :bigint not null
# id :bigint not null, primary key
# completed_at :datetime
# email :string
# ip :string
# metadata :text not null
# name :string
# opened_at :datetime
# phone :string
# preferences :text not null
# sent_at :datetime
# slug :string not null
# ua :string
# uuid :string not null
# values :text not null
# created_at :datetime not null
# updated_at :datetime not null
# external_id :string
# submission_id :bigint not null
#
# Indexes
#
@@ -35,14 +36,16 @@
class Submitter < ApplicationRecord
belongs_to :submission
has_one :template, through: :submission
has_one :account, through: :template
has_one :account, through: :submission
attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} }
attribute :metadata, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, coder: JSON
serialize :preferences, coder: JSON
serialize :metadata, coder: JSON
has_many_attached :documents
has_many_attached :attachments
@@ -62,6 +65,10 @@ class Submitter < ApplicationRecord
end
end
def application_key
external_id
end
def friendly_name
if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>)
+19 -14
View File
@@ -4,20 +4,20 @@
#
# Table name: templates
#
# id :bigint not null, primary key
# application_key :string
# archived_at :datetime
# fields :text not null
# name :string not null
# schema :text not null
# slug :string not null
# source :text not null
# submitters :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# folder_id :bigint not null
# id :bigint not null, primary key
# archived_at :datetime
# fields :text not null
# name :string not null
# schema :text not null
# slug :string not null
# source :text not null
# submitters :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# external_id :string
# folder_id :bigint not null
#
# Indexes
#
@@ -57,10 +57,15 @@ class Template < ApplicationRecord
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
has_many :submissions, dependent: :destroy
has_many :template_sharings, dependent: :destroy
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
def application_key
external_id
end
private
def maybe_set_default_folder
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: template_sharings
#
# id :bigint not null, primary key
# ability :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# template_id :bigint not null
#
# Indexes
#
# index_template_sharings_on_account_id_and_template_id (account_id,template_id) UNIQUE
# index_template_sharings_on_template_id (template_id)
#
# Foreign Keys
#
# fk_rails_... (template_id => templates.id)
#
class TemplateSharing < ApplicationRecord
ALL_ID = -1
belongs_to :template
belongs_to :account, optional: true
end
+18
View File
@@ -63,6 +63,24 @@
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_RESUBMIT) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Allow to resubmit completed forms
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8">
<h2 class="text-2xl font-bold mb-2">Danger Zone</h2>
<%= button_to button_title(title: 'Delete my account'), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: 'Schedule account for deletion?' }, method: :delete, id: :account_delete_button %>
</div>
<% end %>
</div>
+9 -13
View File
@@ -35,20 +35,16 @@
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>,
"submission": [
"submitters": [
{
"submitters": [
{
"name": "John Doe",
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>",
"email": "<%= current_user.email.sub('@', '+test@') %>",
"values": {
"Form Text Field Name": "Default Value"
}
},
{ "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
]
}
"name": "John Doe",
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>",
"email": "<%= current_user.email.sub('@', '+test@') %>",
"values": {
"Form Text Field Name": "Default Value"
}
},
{ "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
]
}'<% end.to_str %>
<span class="top-0 right-0 absolute">
+10 -2
View File
@@ -1,6 +1,14 @@
<div class="max-w-lg mx-auto px-2">
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1>
<% if params[:redir].to_s.ends_with?('on_premise') %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">DocuSeal Console</h1>
<div class="alert my-4 text-sm">
Sign up in DocuSeal Console to upgrade.
On-premises app is completely standalone, Console is used only to manage your license.
</div>
<% else %>
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1>
<% end %>
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
+6
View File
@@ -0,0 +1,6 @@
<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="M7 8l-4 4l4 4" />
<path d="M17 8l4 4l-4 4" />
<path d="M14 4l-4 16" />
</svg>

After

Width:  |  Height:  |  Size: 369 B

@@ -5,7 +5,7 @@
<p>
Send automatic email reminders to your recipients.
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
@@ -5,7 +5,7 @@
<p>
Display your company name and logo when signing documents.
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
Upgrade
<% end %>
<% if signed_in? && current_user != true_user %>
+1 -1
View File
@@ -69,7 +69,7 @@
<%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans
<span class="badge badge-warning">New</span>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<p class="text-gray-700">
Unlock with DocuSeal Pro
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
<p class="text-gray-700">
Unlock with DocuSeal Pro
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
+1 -1
View File
@@ -24,7 +24,7 @@
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div>
<% end %>
<% if @template.submitters.to_a.size == 1 %>
<% if @template.submitters.to_a.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<div>
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div>
@@ -25,7 +25,7 @@
<%= fff.label :endpoint, 'Endpoint (optional)', class: 'label' %>
<%= fff.text_field :endpoint, value: configs['endpoint'], type: :url, class: 'base-input' %>
<label class="label">
<span class="label-text-alt">For AWS S3 compantiable APIs like Minio.</span>
<span class="label-text-alt">For AWS S3 compatible APIs like Minio.</span>
</label>
</div>
<% end %>
+1 -175
View File
@@ -6,181 +6,7 @@
</div>
</div>
<div class="divider uppercase normal-case mt-5 mb-6 text-neutral-700">Or embed on your website</div>
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst="">
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
<%= render 'templates/embedding', template: %>
<div class="mt-4">
<a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a>
</div>
@@ -1,5 +1,5 @@
<div class="mt-2 mb-1">
<div class="tooltip w-full" data-tip="Unlock with Enterpise">
<%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
<%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
</div>
</div>
@@ -1,4 +1,4 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
+1 -1
View File
@@ -36,7 +36,7 @@
</span>
</download-button>
</div>
<% if @submitter.submission.template.submitters.size == 1 %>
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<div class="divider uppercase"><%= t('or') %></div>
<div>
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
@@ -1,7 +1,7 @@
<% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %>
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %>
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) %>
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(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>
<% end %>
<% else %>
+175
View File
@@ -0,0 +1,175 @@
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst="">
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
+53 -45
View File
@@ -1,12 +1,12 @@
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-5 md:px-6 py-5 md:items-center">
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
<% if submitters.size == 1 %>
<div>
<% submitter = submitters.first %>
<div class="flex items-center space-x-4">
<span class="flex items-center space-x-3">
<span class="flex flex-col md:flex-row md:items-center gap-3">
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
<%= submitter.status %>
@@ -20,14 +20,14 @@
</div>
<div class="flex space-x-2 items-center">
<% if submitter.completed_at? %>
<form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<form onsubmit="event.preventDefault()" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline">Download</span>
</span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<span class="inline">Downloa...</span>
</span>
@@ -36,26 +36,30 @@
</form>
<% else %>
<% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white md:w-36 flex">
<span class="flex items-center justify-center space-x-2">
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
<span class="flex items-center justify-center space-x-1 md:space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span>
<span class="inline shrink-0">Sign now</span>
</span>
</button>
</form>
<% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
<div class="flex-1 md:flex-none">
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
</div>
<% end %>
<% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
<div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div>
<% if !submission.archived_at? && can?(:destroy, submission) %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %>
</div>
<% else %>
<div class="space-y-1 w-full md:mr-2">
<div class="flex items-center space-x-3">
<div class="flex flex-col md:flex-row md:items-center gap-3">
<% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
@@ -64,10 +68,10 @@
</span>
</div>
<% end %>
<div class="w-full">
<div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0">
<% submitters.each_with_index do |submitter, index| %>
<div class="flex justify-between items-center space-x-3">
<span class="flex items-center space-x-3">
<div class="relative flex justify-between items-start md:items-center space-x-3">
<span class="flex flex-col md:flex-row md:items-center gap-2">
<% unless is_submission_completed %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
@@ -82,31 +86,31 @@
<% if submitter.completed_at? && !is_submission_completed %>
<form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Download</span>
<span class="inline">Download</span>
</span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
<span class="hidden md:inline">Downloa...</span>
<span class="inline">Downloa...</span>
</span>
</download-button>
</button>
</form>
<% elsif !is_submission_completed %>
<div class="flex items-center space-x-3">
<div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
<button onclick="event.stopPropagation()" class="btn btn-xs btn-outline btn-neutral bg-white md:w-36">
<span class="flex items-center justify-center space-x-2">
<button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36">
<span class="flex items-center justify-center space-x-1 md:space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span>
<span class="inline shrink-0">Sign now</span>
</span>
</button>
</form>
<% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy Link', copied_title_md: 'Copied' %>
<% end %>
</div>
<% end %>
@@ -115,27 +119,31 @@
</div>
</div>
</div>
<div class="flex space-x-2 items-center">
<div class="flex space-x-1 md:space-x-2 items-center">
<% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline">Download</span>
</span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<span class="inline">Downloa...</span>
</span>
</download-button>
</button>
</form>
<div class="flex-1 md:flex-none">
<form onsubmit="event.preventDefault()" class="w-full md:w-fit">
<button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline">Download</span>
</span>
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<span class="inline">Downloa...</span>
</span>
</download-button>
</button>
</form>
</div>
<% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
<div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div>
<% unless submission.archived_at? %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %>
</div>
<% end %>
+4 -1
View File
@@ -7,6 +7,9 @@
<p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('user', class: 'w-4 h-4') %>
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
<% if template.account_id != current_account.id %>
<span class="badge badge-neutral badge-outline badge-sm text-[10px] uppercase">shared</span>
<% end %>
</p>
<p class="flex text-xs text-base-content/60">
<span class="flex items-center space-x-1 w-1/2">
@@ -24,7 +27,7 @@
</a>
<div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px">
<div class="space-y-1">
<% if can?(:update, template) && !template.archived_at? %>
<% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %>
<span class="tooltip tooltip-left" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('folder_share', class: 'w-4 h-4') %>
+71 -35
View File
@@ -1,50 +1,86 @@
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:justify-between md:items-start mb-3">
<div>
<h1 class="text-4xl font-semibold mr-4" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<%= template.name %>
<% if template.archived_at? %>
<span class="badge badge-outline badge-lg align-middle">Archived</span>
<% end %>
</h1>
<div class="flex items-center">
<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') %>
<span class="text-sm">
<%= @template.folder.name %>
</span>
</a>
<% if can?(:update, template) %>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a>
</span>
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3">
<div class="relative flex items-start justify-between w-full space-x-0">
<div>
<h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<%= template.name %>
<% if template.archived_at? %>
<span class="badge badge-outline badge-lg align-middle">Archived</span>
<% end %>
</h1>
<% if @template.account_id == current_account.id %>
<div class="flex items-center justify-between">
<div class="flex items-center">
<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') %>
<span class="text-sm">
<%= @template.folder.name %>
</span>
</a>
<% if can?(:update, template) %>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a>
</span>
<% end %>
</div>
</div>
<% else %>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex items-center space-x-1 mt-1 peer">
<span class="badge badge-neutral badge-outline badge-md text-xs text-white uppercase">shared</span>
</div>
</div>
</div>
<% end %>
</div>
</div>
<div class="flex md:justify-between space-x-2 flex-none pt-1">
<% if !template.archived_at? %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
<div class="flex items-center space-x-2">
<% if can?(:update, template) && (Docuseal.multitenant? || current_account.testing? || current_account.id == 1) %>
<div class="tooltip" data-tip="API and Embedding">
<%= link_to template_code_modal_path(template), class: 'btn btn-ghost btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('code', class: 'w-6 h-6') %>
</span>
<% end %>
</div>
<% end %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: 'Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
</div>
<% end %>
</div>
<div class="flex space-x-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
<%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
<% end %>
<% if can?(:create, template) %>
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %>
<%= svg_icon('copy', class: 'w-6 h-6') %>
<span class="hidden md:inline">Clone</span>
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6') %>
<span class="inline">Clone</span>
</span>
<% end %>
<% end %>
<% if !template.archived_at? && can?(:update, template) %>
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('pencil', class: 'w-6 h-6') %>
<span>Edit</span>
</span>
<% if !template.archived_at? %>
<% if can?(:update, template) %>
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('pencil', class: 'w-6 h-6') %>
<span>Edit</span>
</span>
<% end %>
<% elsif can?(:read, template) %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %>
<span>Preview</span>
</span>
<% end %>
<% end %>
<% end %>
<% if template.archived_at? && can?(:create, template) %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm' %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %>
</div>
</div>
+17 -15
View File
@@ -1,22 +1,24 @@
<%= render 'title', template: @template %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
<% if !@pagy.count.zero? || params[:q].present? %>
<div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<p class="text-3xl font-bold">Submissions</p>
<div class="flex justify-end space-x-2 mt-3 md:mt-0">
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %>
<% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span>
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<div class="flex justify-between items-center md:items-end">
<p class="text-3xl font-bold">Submissions</p>
<div class="flex justify-end space-x-2">
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %>
<% end %>
<% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
@@ -0,0 +1,11 @@
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">API and Embedding</p>
<p class="text-gray-700">
Unlock with DocuSeal Pro
<br>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
Whitespace-only changes.
@@ -0,0 +1,36 @@
<%= render 'shared/turbo_modal', title: 'API and Embedding', close_after_submit: false do %>
<div>
<label class="text-sm font-semibold" for="template_id">
Template ID
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="template_id" type="text" value="<%= @template.id %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<div>
<label class="text-sm font-semibold" for="embedding_url">
Embedding URL
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<%= render 'templates/embedding', template: @template %>
<% if can?(:manage, TemplateSharing.new(template: @template)) %>
<%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %>
<%= f.hidden_field :template_id, value: @template.id %>
<div class="flex items-center justify-between">
<span>
Share template with Test Environment
</span>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<div class="mb-4">
</div>
<% end %>
<%= render 'templates_code_modal/preferences' %>
<%= render 'templates_code_modal/placeholder' %>
<% end %>
@@ -0,0 +1 @@
<template-builder class="grid" data-editable="false" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
+1 -1
View File
@@ -5,7 +5,7 @@
<option value="editor" disabled>Editor</option>
<option value="viewer" disabled>Viewer</option>
<% end %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
Unlock more user roles with DocuSeal Pro.
<span class="link font-medium">Learn More</a>
+1 -1
View File
@@ -22,7 +22,7 @@ module DocuSeal
config.active_storage.routes_prefix = ''
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he]
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
config.i18n.fallbacks = [:en]
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
+4
View File
@@ -4,6 +4,10 @@ ActiveSupport.on_load(:active_storage_attachment) do
attribute :uuid, :string, default: -> { SecureRandom.uuid }
has_many_attached :preview_images
def preview_image_url
preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })&.url
end
end
ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production?
+22 -5
View File
@@ -20,7 +20,6 @@ en: &en
completed_successfully: Completed Successfully
es: &es
digitally_signed_by: Firmado digitalmente por
role: Rol
email: Correo electrónico
digitally_signed_by: Firmado digitalmente por
@@ -42,7 +41,6 @@ es: &es
completed_successfully: Completado exitosamente
it:
digitally_signed_by: Signé numériquement par
role: Rôle
email: Email
digitally_signed_by: Firmato digitalmente da
@@ -84,7 +82,6 @@ fr: &fr
completed_successfully: Terminé avec succès
pt: &pt
digitally_signed_by: Assinado digitalmente por
role: Função
email: Email
digitally_signed_by: Assinado digitalmente por
@@ -106,10 +103,9 @@ pt: &pt
completed_successfully: Concluído com sucesso
de: &de
digitally_signed_by: Digital signiert von
role: Rolle
email: E-Mail
digitally_signed_by: Digital unterzeichnet von
digitally_signed_by: Digital signiert von
role: Rolle
provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten
start: Starten
@@ -207,6 +203,27 @@ he:
downloading: מוריד
completed_successfully: הושלם בהצלחה
nl:
email: E-mail
digitally_signed_by: Digitaal ondertekend door
role: Rol
provide_your_email_to_start: Geef uw e-mailadres om te beginnen
start: Start
starting: Starten
form_has_been_deleted_by_html: 'Formulier is verwijderd door <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Uitgenodigd door <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: U bent uitgenodigd om een formulier in te dienen
signed_on_time: 'Ondertekend op %{time}'
form_has_been_submitted_already: Formulier is al ingediend
send_copy_to_email: Stuur kopie naar E-mail
sending: Voltooien
resubmit: Opnieuw indienen
form_has_been_deleted_by_html: 'Formulier is verwijderd door <span class="font-semibold">%{name}</span>.'
or: of
download_documents: Documenten downloaden
downloading: Downloaden
completed_successfully: Succesvol voltooid
en-US:
<<: *en
date:
+4 -1
View File
@@ -70,11 +70,14 @@ Rails.application.routes.draw do
end
resources :templates_archived, only: %i[index], path: 'archived'
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[new create edit show destroy] do
resources :restore, only: %i[create], controller: 'templates_restore'
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
resources :submissions, only: %i[new create]
resource :folder, only: %i[edit update], controller: 'templates_folders'
resource :preview, only: %i[show], controller: 'templates_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:attachment_uuid'
@@ -116,7 +119,7 @@ Rails.application.routes.draw do
resource :personalization, only: %i[show create], controller: 'personalization_settings'
resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
resource :account, only: %i[show update]
resource :account, only: %i[show update destroy]
resources :profile, only: %i[index] do
collection do
patch :update_contact
@@ -0,0 +1,8 @@
# frozen_string_literal: true
class RenameApplicationKeyToExternalId < ActiveRecord::Migration[7.1]
def change
rename_column :templates, :application_key, :external_id
rename_column :submitters, :application_key, :external_id
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddSubmitterMetadata < ActiveRecord::Migration[7.1]
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
end
def change
add_column :submitters, :metadata, :text
MigrationSubmitter.where(metadata: nil).update_all(metadata: '{}')
change_column_null :submitters, :metadata, false
end
end
@@ -0,0 +1,26 @@
# frozen_string_literal: true
class AddAccountIdToSubmissions < ActiveRecord::Migration[7.1]
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
class MigrationAccount < ApplicationRecord
self.table_name = 'accounts'
end
def change
add_reference :submissions, :account, index: true, null: true
MigrationAccount.all.each do |account|
MigrationSubmission.where(template_id: MigrationTemplate.where(account_id: account.id).select(:id))
.update_all(account_id: account.id)
end
change_column_null :submissions, :account_id, false
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateTemplateSharings < ActiveRecord::Migration[7.1]
def change
create_table :template_sharings do |t|
t.references :template, null: false, foreign_key: true, index: true
t.references :account, null: false, foreign_key: false, index: false
t.string :ability, null: false
t.index %i[account_id template_id], unique: true
t.timestamps
end
end
end
+17 -3
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -151,6 +151,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
t.string "submitters_order", null: false
t.string "slug", null: false
t.text "preferences", null: false
t.bigint "account_id", null: false
t.index ["account_id"], name: "index_submissions_on_account_id"
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id"
@@ -171,8 +173,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
t.datetime "updated_at", null: false
t.string "name"
t.string "phone"
t.string "application_key"
t.string "external_id"
t.text "preferences", null: false
t.text "metadata", null: false
t.index ["email"], name: "index_submitters_on_email"
t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["submission_id"], name: "index_submitters_on_submission_id"
@@ -189,6 +192,16 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
t.index ["author_id"], name: "index_template_folders_on_author_id"
end
create_table "template_sharings", force: :cascade do |t|
t.bigint "template_id", null: false
t.bigint "account_id", null: false
t.string "ability", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "template_id"], name: "index_template_sharings_on_account_id_and_template_id", unique: true
t.index ["template_id"], name: "index_template_sharings_on_template_id"
end
create_table "templates", force: :cascade do |t|
t.string "slug", null: false
t.string "name", null: false
@@ -202,7 +215,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
t.datetime "updated_at", null: false
t.text "source", null: false
t.bigint "folder_id", null: false
t.string "application_key"
t.string "external_id"
t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["folder_id"], name: "index_templates_on_folder_id"
@@ -269,6 +282,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
add_foreign_key "submitters", "submissions"
add_foreign_key "template_folders", "accounts"
add_foreign_key "template_folders", "users", column: "author_id"
add_foreign_key "template_sharings", "templates"
add_foreign_key "templates", "accounts"
add_foreign_key "templates", "template_folders", column: "folder_id"
add_foreign_key "templates", "users", column: "author_id"
+27
View File
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Abilities
module TemplateConditions
module_function
def collection(user, ability: nil)
shared_ids =
Template.joins(:template_sharings)
.where(template_sharings: { ability:,
account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
.select(:id)
Template.where(account_id: user.account_id).or(Template.where(id: shared_ids))
end
def entity(template, user:, ability: nil)
return true if template.account_id == user.account_id
account_ids = [user.account_id, TemplateSharing::ALL_ID]
template.template_sharings.any? do |e|
e.account_id.in?(account_ids) && (ability.nil? || e.ability == ability)
end
end
end
end
+9 -2
View File
@@ -5,9 +5,16 @@ class Ability
def initialize(user)
can :manage, Template, account_id: user.account_id
can %i[read update create], Template,
Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
end
can :manage, TemplateFolder, account_id: user.account_id
can :manage, Submission, template: { account_id: user.account_id }
can :manage, Submitter, template: { account_id: user.account_id }
can :manage, TemplateSharing, template: { account_id: user.account_id }
can :manage, Submission, account_id: user.account_id
can :manage, Submitter, submission: { account_id: user.account_id }
can :manage, User, account_id: user.account_id
can :manage, EncryptedConfig, account_id: user.account_id
can :manage, EncryptedUserConfig, user_id: user.id
+5
View File
@@ -19,6 +19,11 @@ module Docuseal
else
'https://console.docuseal.co'
end
CLOUD_URL = if Rails.env.development?
'http://localhost:3000'
else
'https://docuseal.co'
end
CDN_URL = if Rails.env.development?
'http://localhost:3000'
elsif ENV['MULTITENANT'] == 'true'
+17 -3
View File
@@ -36,8 +36,6 @@ module Params
message += " in `#{@current_path}`." if @current_path.present?
raise InvalidParameterError, message unless dry_run?
Rollbar.error(message) if defined?(Rollbar)
end
def required(params, keys, message: nil)
@@ -59,6 +57,14 @@ module Params
raise_error(message || "#{key} must be a #{type}")
end
def format(params, key, regexp, message: nil)
return if params.blank?
return if params[key].blank?
return if regexp.match?(params[key].to_s)
raise_error(message || "#{key} must follow the #{regexp.source} format")
end
def in_path(params, path = [])
old_path = @current_path
@@ -75,7 +81,7 @@ module Params
old_path = @current_path
params.dig(*path)&.each_with_index do |item, index|
@current_path = old_path + [*path].map(&:to_s).join('.') + "[#{index}]"
@current_path = [old_path, [*path].map(&:to_s).join('.') + "[#{index}]"].compact_blank.join('.')
yield item if item
end
@@ -102,5 +108,13 @@ module Params
raise_error(message || "#{key} must be one of #{values.join(', ')}")
end
def more_than(params, key, value, message: nil)
return if params.blank?
return if params[key].blank?
return if params[key].to_f > value
raise_error(message || "#{key} must be more than #{value}")
end
end
end
+66 -26
View File
@@ -2,13 +2,19 @@
module Params
class SubmissionCreateValidator < BaseValidator
# rubocop:disable Metrics
def call
if params[:submission].blank? && (params[:emails].present? || params[:email].present?)
validate_creation_from_emails(params)
else
elsif params.key?(:submitters)
validate_creation_from_submitters(params)
elsif params.key?(:submission) || params.key?(:submissions)
validate_creation_from_submission(params)
else
required(params, :submitters)
type(params, :submitters, Array)
end
true
end
def validate_creation_from_emails(params)
@@ -25,8 +31,64 @@ module Params
required(message_params, :body)
end
end
true
def validate_creation_from_submitters(params)
required(params, :template_id)
required(params, :submitters)
boolean(params, :send_email)
boolean(params, :send_sms)
type(params, :order, String)
type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String)
type(params, :message, Hash)
type(params, :submitters, Array)
in_path(params, :message) do |message_params|
type(message_params, :subject, String)
type(message_params, :body, String)
required(message_params, :body)
end
value_in(params, :order, %w[preserved random], allow_nil: true)
if params[:submitters].present?
in_path(params, :submitters) do |submitters_params|
type(submitters_params, 0, Hash)
end
end
in_path_each(params, :submitters) do |submitter_params|
validate_submitter(submitter_params)
end
end
def validate_submitter(submitter_params)
required(submitter_params, %i[email phone name])
type(submitter_params, :name, String)
type(submitter_params, :email, String)
format(submitter_params, :email, /@/, message: 'email is invalid')
type(submitter_params, :phone, String)
format(submitter_params, :phone, /\A\+\d+\z/,
message: 'phone should start with +<country code> and contain only digits')
type(submitter_params, :values, Hash)
type(submitter_params, :metadata, Hash)
boolean(submitter_params, :send_email)
boolean(submitter_params, :send_sms)
type(submitter_params, :completed_redirect_url, String)
type(submitter_params, :fields, Array)
in_path_each(submitter_params, :fields) do |field_params|
required(field_params, :name)
type(field_params, :name, String)
type(field_params, :validation_pattern, String)
type(field_params, :invalid_message, String)
boolean(field_params, :readonly)
end
end
def validate_creation_from_submission(params)
@@ -56,30 +118,8 @@ module Params
end
in_path_each(params, %i[submission submitters]) do |submitter_params|
required(submitter_params, %i[email phone name])
type(submitter_params, :name, String)
type(submitter_params, :email, String)
type(submitter_params, :phone, String)
type(submitter_params, :values, Hash)
boolean(submitter_params, :send_email)
boolean(submitter_params, :send_sms)
type(submitter_params, :completed_redirect_url, String)
type(submitter_params, :fields, Array)
in_path_each(submitter_params, %i[fields]) do |field_params|
required(field_params, :name)
type(field_params, :name, String)
type(field_params, :default_value, String)
type(field_params, :validation_pattern, String)
type(field_params, :invalid_message, String)
boolean(field_params, :readonly)
end
validate_submitter(submitter_params)
end
true
end
# rubocop:enable Metrics
end
end
+5
View File
@@ -25,6 +25,11 @@ class PdfProcessor < HexaPDF::Content::Processor
@pos += 1
end
@pos = 0
@num += 1
rescue HexaPDF::Error => e
Rails.logger.error(e.message)
@pos = 0
@num += 1
end
+1
View File
@@ -32,6 +32,7 @@ module Submissions
parse_emails(emails).uniq.map do |email|
submission = template.submissions.new(created_by_user: user,
account_id: user.account_id,
source:,
template_submitters: template.submitters)
+7 -4
View File
@@ -15,6 +15,7 @@ module Submissions
set_submission_preferences['send_email'] = true if params['send_completed_email']
submission = template.submissions.new(created_by_user: user, source:,
account_id: user.account_id,
preferences: set_submission_preferences,
template_submitters: [], submitters_order:)
@@ -39,11 +40,11 @@ module Submissions
end
end
def maybe_set_template_fields(submission, submitters_attrs, submitter_uuid: nil)
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup
submitters_attrs.each_with_index do |submitter_attrs, index|
submitter_uuid ||= find_submitter_uuid(submission.template, submitter_attrs, index)
submitter_uuid = default_submitter_uuid || find_submitter_uuid(submission.template, submitter_attrs, index)
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
process_field_values_param(submitter_attrs[:values], template_fields, submitter_uuid)
@@ -65,6 +66,7 @@ module Submissions
template_fields.each do |f|
next if f['submitter_uuid'] != submitter_uuid ||
(!f['name'].in?(readonly_fields) &&
!f['name'].to_s.downcase.in?(readonly_fields) &&
!f['name'].to_s.parameterize.underscore.in?(readonly_fields))
f['readonly'] = true
@@ -93,7 +95,7 @@ module Submissions
next if f['submitter_uuid'] != submitter_uuid
field_configs = fields.find do |e|
e['name'] == f['name'] || e['name'] == f['name'].to_s.parameterize.underscore
e['name'].to_s.casecmp(f['name'].to_s).zero? || e['name'] == f['name'].to_s.parameterize.underscore
end
next if field_configs.blank?
@@ -134,10 +136,11 @@ module Submissions
email:,
phone: attrs[:phone].to_s.gsub(/[^0-9+]/, ''),
name: attrs[:name],
application_key: attrs[:application_key],
external_id: attrs[:external_id].presence || attrs[:application_key],
completed_at: attrs[:completed] ? Time.current : nil,
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {},
metadata: attrs[:metadata] || {},
preferences: preferences.merge(submitter_preferences)
.merge({ default_values: attrs[:values] }.compact_blank)
.except('bcc_completed'),
+2 -2
View File
@@ -69,7 +69,7 @@ module Submissions
composer.column(columns: 1) do |column|
add_logo(column, submission)
column.text('Audit Log',
column.text(account.linked_account_account&.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log',
font_size: 16,
padding: [15, 0, 0, 0],
position: :float, position_hint: :right)
@@ -78,7 +78,7 @@ module Submissions
composer.column(columns: 1) do |column|
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [20, 0, 10, 0], position: :float)
unless submission.source_embed?
unless submission.source.in?(%w[embed api])
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
font_size: 9, padding: [22, 0, 10, 0], position: :float, align: :right)
end
@@ -30,7 +30,6 @@ module Submitters
)
end
# rubocop:disable Metrics
def generate_stamp_image(submitter)
logo = Vips::Image.new_from_buffer(load_logo(submitter).read, '')
@@ -82,7 +81,6 @@ module Submitters
Vips::Image.text(text, width: WIDTH, height: HEIGHT)
end
# rubocop:enable Metrics
def load_logo(_submitter)
PdfIcons.logo_io
+3 -1
View File
@@ -7,13 +7,15 @@ module Submitters
def call(submitter)
configs = submitter.submission.template.account.account_configs
.where(key: [AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE])
completed_button = configs.find { |e| e.key == AccountConfig::FORM_COMPLETED_BUTTON_KEY }&.value || {}
with_typed_signature = configs.find { |e| e.key == AccountConfig::ALLOW_TYPED_SIGNATURE }&.value != false
with_confetti = configs.find { |e| e.key == AccountConfig::FORM_WITH_CONFETTI_KEY }&.value != false
{ completed_button:, with_typed_signature: }
{ completed_button:, with_typed_signature:, with_confetti: }
end
end
end
+8 -4
View File
@@ -19,14 +19,16 @@ module Submitters
normalized_values = values.to_h.filter_map do |key, value|
if fields_uuid_index[key].blank?
key = fields_name_index[key]&.dig('uuid')
original_key = key
raise(UnknownFieldName, "Unknown field: #{key}") if key.blank? && throw_errors
key = fields_name_index[key]&.dig('uuid') || fields_name_index[key.to_s.downcase]&.dig('uuid')
raise(UnknownFieldName, "Unknown field: #{original_key}") if key.blank? && throw_errors
end
next if key.blank?
if fields_uuid_index[key]['type'].in?(%w[initials signature image file])
if fields_uuid_index[key]['type'].in?(%w[initials signature image file]) && value.present?
new_value, new_attachments = normalize_attachment_value(value, template.account, for_submitter)
attachments.push(*new_attachments)
@@ -59,7 +61,9 @@ module Submitters
end
def build_fields_index(fields)
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
fields.index_by { |e| e['name'] }
.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
.merge(fields.index_by { |e| e['name'].to_s.downcase })
end
def normalize_attachment_value(value, account, for_submitter = nil)
+3 -3
View File
@@ -18,9 +18,9 @@ module Submitters
serialize_params = {
include: {},
only: %i[id slug uuid name email phone completed_at application_key
opened_at sent_at created_at updated_at],
methods: %i[status]
only: %i[id slug uuid name email phone completed_at external_id
metadata opened_at sent_at created_at updated_at],
methods: %i[status application_key]
}
serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template
+2 -1
View File
@@ -16,7 +16,8 @@ module Submitters
submitter_name = (submitter.submission.template_submitters ||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
submitter.as_json(include: [template: { only: %i[id name created_at updated_at] }])
submitter.as_json(methods: %i[application_key],
include: [template: { only: %i[id name external_id created_at updated_at] }])
.except('uuid', 'values', 'slug')
.merge('values' => values,
'documents' => documents,
+2 -2
View File
@@ -4,12 +4,12 @@ module Templates
module Clone
module_function
def call(original_template, author:, application_key: nil, name: nil, folder_name: nil)
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
original_template_account = original_template.account
template = original_template_account.templates.new
template.application_key = application_key
template.external_id = external_id
template.author = author
template.name = name || "#{original_template.name} (Clone)"
+1
View File
@@ -6,6 +6,7 @@ FactoryBot.define do
created_by_user factory: %i[user]
before(:create) do |submission, _|
submission.account_id = submission.template.account_id
submission.template_fields = submission.template.fields
submission.template_schema = submission.template.schema
submission.template_submitters = submission.template.submitters