Compare commits

..
57 Commits
Author SHA1 Message Date
Pete Matsyburka ce1323e849 sign in-person button 2024-01-15 17:43:27 +02:00
Pete Matsyburka fc3c1e65e3 reduce image size 2024-01-15 02:30:10 +02:00
Pete Matsyburka f1ebd8f912 text utils for RTL 2024-01-15 02:22:53 +02:00
Pete Matsyburka 98f5bba832 update pdf processor 2024-01-15 02:22:53 +02:00
Alex Turchyn e864e8a47f process password protected pdf files 2024-01-15 02:22:40 +02:00
Alex Turchyn a573545ead fix right-to-left text direction for legacy Outlook clients 2024-01-15 00:02:44 +02:00
Pete Matsyburka 15ff4386e5 remove via linkk tab if 2+ parties 2024-01-14 14:09:23 +02:00
Pete Matsyburka 7a4aa3bf87 fix success page 2024-01-14 12:49:54 +02:00
Pete Matsyburka c1ba6288c8 audit log toggle in documents email 2024-01-14 12:38:08 +02:00
Pete Matsyburka d0ba180729 add submitter completed redirect url 2024-01-14 12:11:13 +02:00
Pete Matsyburka 2edc76414b do not show archived submission 2024-01-14 11:11:02 +02:00
Pete Matsyburka 452db5dc39 allow to disable audit log PDF attachment 2024-01-14 01:59:13 +02:00
Pete Matsyburka 21c4c53feb update readme docker compose command 2024-01-14 01:11:16 +02:00
Pete Matsyburka 6292789cac return error if template is blank 2024-01-14 00:49:07 +02:00
Pete Matsyburka d445bbc02d fix accepted builder field types 2024-01-14 00:43:14 +02:00
Pete Matsyburka 8627f58cbc add via link recipients tab 2024-01-14 00:43:13 +02:00
Pete Matsyburka 66ab0059bb accept field title and description 2024-01-13 13:52:58 +02:00
Pete Matsyburka 3f06932277 send bcc_completed as multiple to 2024-01-12 23:53:42 +02:00
Pete Matsyburka 59cf1b435e enable_starttls_auto true when using ENV 2024-01-12 22:24:03 +02:00
Pete Matsyburka c9a5b0e66e add submitter slug variable 2024-01-12 18:22:09 +02:00
Pete Matsyburka bcd5e1da98 fix auth token rotate 2024-01-12 09:45:40 +02:00
Pete Matsyburka f74ad050b4 add field types prop 2024-01-10 23:18:30 +02:00
Pete Matsyburka b6f43c9b23 add onSave builder event prop 2024-01-10 17:54:35 +02:00
Pete Matsyburka 298c64a119 fix RTL reverse 2024-01-10 10:28:06 +02:00
Pete Matsyburka 8b80679b3a add bcc completed submission param 2024-01-10 02:06:31 +02:00
Pete Matsyburka 82a2bc561a remove talk to sales checkbox 2024-01-09 11:31:13 +02:00
Pete Matsyburka e019542d3f fix template 2024-01-09 10:49:51 +02:00
Pete Matsyburka 9356247ef0 add signer user forms translations 2024-01-09 01:32:39 +02:00
Pete Matsyburka 4db77c18e3 add Hebrew form translations 2024-01-08 20:18:57 +02:00
Pete Matsyburka 0d0b248988 use arabic letter connector 2024-01-08 19:32:20 +02:00
Pete Matsyburka 44f3910266 fix build 2024-01-07 18:18:24 +02:00
Pete Matsyburka 35c6798a80 fix RTL form area 2024-01-07 15:09:36 +02:00
Pete Matsyburka 0995c7ee63 adjust RTL regexp 2024-01-07 14:52:10 +02:00
Pete Matsyburka 0a25f78329 touch .version 2024-01-07 14:31:10 +02:00
Pete Matsyburka bb8fffc491 link to releases page 2024-01-07 14:25:38 +02:00
Alex Turchyn 6f6c51fb32 add tooltips to personalize emails 2024-01-07 14:22:43 +02:00
Alex Turchyn 39d14c15d6 add display of app version in UI 2024-01-07 14:20:34 +02:00
Pete Matsyburka 17de28965b add LTR script support 2024-01-07 14:19:21 +02:00
Pete Matsyburka ae7865e508 add reorder parties button 2024-01-06 22:25:13 +02:00
Pete Matsyburka dc8e8cc5c9 draw field on field type click 2024-01-06 21:16:27 +02:00
Pete Matsyburka 5208470c38 make current field area more visible 2024-01-06 19:55:20 +02:00
Pete Matsyburka e7ef7b552f show field area pages list inthe form 2024-01-06 17:21:15 +02:00
Pete Matsyburka a478112b07 fix multiple area fields draw 2024-01-06 16:19:40 +02:00
Pete Matsyburka 7d0f32e1f9 handle multiple checkboxes required 2024-01-06 15:38:03 +02:00
Pete Matsyburka d35d8bead3 support checkbox option without name 2024-01-06 15:05:15 +02:00
Pete Matsyburka ab9cc52bcc add rotate api token button 2024-01-06 12:34:20 +02:00
Pete Matsyburka 7dc342a6f9 make it possible to remove default signature 2024-01-05 22:48:55 +02:00
Pete Matsyburka 80a3ef9193 add submission URL to webhooks serializer 2024-01-05 20:27:38 +02:00
Pete Matsyburka bb723d20cb add builder props 2024-01-05 18:40:45 +02:00
Pete Matsyburka 0c24a2016c use Pro instead of Enterprise 2024-01-04 15:28:46 +02:00
Pete Matsyburka 2124965996 add builder i18n 2024-01-04 01:44:16 +02:00
Pete Matsyburka 6f1e104067 add builder autosave prop 2024-01-04 01:44:11 +02:00
Pete Matsyburka b20f40b5a3 fix webhook data fields order 2024-01-03 20:43:47 +02:00
Pete Matsyburka 1093ca5fc4 add md breakpoint prop to form 2024-01-03 18:19:44 +02:00
Pete Matsyburka 0d99c55d3f disable pdf validate on write 2024-01-02 22:59:34 +02:00
Pete Matsyburka 86dc1a88ed Revert "do not use puma pidfile"
This reverts commit 8961294a29.
2024-01-02 22:39:23 +02:00
Pete Matsyburka 968c20d8ca completed email bcc sending via to address 2024-01-02 11:41:28 +02:00
103 changed files with 1598 additions and 354 deletions

No files matched your search

+2 -1
View File
@@ -8,7 +8,8 @@
}, },
"rules": { "rules": {
"vue/no-deprecated-html-element-is": 0, "vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0 "vue/no-mutating-props": 0,
"vue/no-v-html": 0
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2022, "ecmaVersion": 2022,
+3
View File
@@ -30,6 +30,9 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
View File
Whitespace-only changes.
+12 -3
View File
@@ -1,3 +1,11 @@
FROM ruby:3.2.2-alpine3.18 as fonts
WORKDIR /fonts
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
FROM ruby:3.2.2-alpine3.18 as webpack FROM ruby:3.2.2-alpine3.18 as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
@@ -31,11 +39,11 @@ ENV BUNDLE_WITHOUT="development:test"
WORKDIR /app WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && find /usr/local/bundle -type d \( -name "rdoc" -o -name "spec" \) -exec rm -rf {} +
COPY ./bin ./bin COPY ./bin ./bin
COPY ./app ./app COPY ./app ./app
@@ -45,8 +53,9 @@ COPY ./log ./log
COPY ./lib ./lib COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts RUN ln -s /fonts /app/public/fonts
+2
View File
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
ruby '3.2.2' ruby '3.2.2'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false gem 'bootsnap', require: false
@@ -40,6 +41,7 @@ gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false gem 'sqlite3', require: false
gem 'strip_attributes' gem 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
group :development, :test do group :development, :test do
+9
View File
@@ -82,6 +82,7 @@ GEM
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.874.0) aws-partitions (1.874.0)
@@ -124,6 +125,7 @@ GEM
bullet (7.1.4) bullet (7.1.4)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
cancancan (3.5.0) cancancan (3.5.0)
capybara (3.39.2) capybara (3.39.2)
addressable addressable
@@ -135,6 +137,7 @@ GEM
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.2)
@@ -526,6 +529,10 @@ GEM
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.0)
camertron-eprun
cldr-plurals-runtime-rb (~> 1.1)
tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4) tzinfo-data (1.2023.4)
@@ -558,6 +565,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
annotate annotate
arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
azure-storage-blob azure-storage-blob
better_html better_html
@@ -610,6 +618,7 @@ DEPENDENCIES
sqlite3 sqlite3
strip_attributes strip_attributes
turbo-rails turbo-rails
twitter_cldr
tzinfo-data tzinfo-data
web-console web-console
webmock webmock
+1 -1
View File
@@ -73,7 +73,7 @@ curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy): Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
```sh ```sh
HOST=your-domain-name.com docker-compose up sudo HOST=your-domain-name.com docker compose up
``` ```
## For Businesses ## For Businesses
+10 -3
View File
@@ -65,6 +65,12 @@ module Api
def create def create
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank?
Rollbar.error("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
end
params[:send_email] = true unless params.key?(:send_email) params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms) params[:send_sms] = false unless params.key?(:send_sms)
@@ -136,12 +142,13 @@ module Api
params.permit( params.permit(
key => [ key => [
[:send_email, :send_sms, { [:send_email, :send_sms, :bcc_completed, :completed_redirect_url, {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body], { values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}] }]
] ]
).fetch(key, []) ).fetch(key, [])
@@ -22,6 +22,8 @@ module Api
} }
) )
} }
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end end
end end
end end
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
def index def index
authorize!(:read, current_user.access_token) authorize!(:read, current_user.access_token)
end end
def create
authorize!(:manage, current_user.access_token)
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
end
end end
+17
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
include ActiveStorage::SetCurrent include ActiveStorage::SetCurrent
include Pagy::Backend include Pagy::Backend
@@ -32,6 +34,21 @@ class ApplicationController < ActionController::Base
private private
def with_browser_locale(&)
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
locale =
if locale.starts_with?('en-') && locale != 'en-US'
'en-GB'
else
locale.split('-').first.presence || 'en-GB'
end
locale = 'en-GB' unless I18n.locale_available?(locale)
I18n.with_locale(locale, &)
end
def sign_in_for_demo def sign_in_for_demo
sign_in(User.active.order('random()').take) unless signed_in? sign_in(User.active.order('random()').take) unless signed_in?
end end
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing' 'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/templates/html', '/templates/html',
@@ -7,18 +7,28 @@ class PersonalizationSettingsController < ApplicationController
def create def create
account_config = account_config =
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key]) current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
authorize!(:create, account_config) authorize!(:create, account_config)
account_config.update!(encrypted_config_params) account_config.update!(account_config_params)
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.') redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end end
private private
def encrypted_config_params def account_config_params
params.require(:account_config).permit! attrs = params.require(:account_config).permit!
attrs[:value]&.transform_values! do |value|
if value.in?(%w[true false])
value == 'true'
else
value
end
end
attrs
end end
end end
+1
View File
@@ -6,6 +6,7 @@ class StartFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
before_action :load_template before_action :load_template
def show def show
+4 -1
View File
@@ -3,6 +3,7 @@
class SubmitFormController < ApplicationController class SubmitFormController < ApplicationController
layout 'form' layout 'form'
around_action :with_browser_locale, only: %i[show completed success]
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
@@ -32,7 +33,7 @@ class SubmitFormController < ApplicationController
cookies[:submitter_sid] = @submitter.signed_id cookies[:submitter_sid] = @submitter.signed_id
render @submitter.submission.template.archived_at? ? :archived : :show render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
end end
def update def update
@@ -58,4 +59,6 @@ class SubmitFormController < ApplicationController
def completed def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end end
def success; end
end end
@@ -10,12 +10,7 @@ class TemplatesUploadsController < ApplicationController
def create def create
url_params = create_file_params_from_url if params[:url].present? url_params = create_file_params_from_url if params[:url].present?
@template.account = current_account save_template!(@template, url_params)
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
@template.save!
documents = Templates::CreateAttachments.call(@template, url_params || params) documents = Templates::CreateAttachments.call(@template, url_params || params)
@@ -24,6 +19,8 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
rescue StandardError => e rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -32,6 +29,17 @@ class TemplatesUploadsController < ApplicationController
private private
def save_template!(template, url_params)
template.account = current_account
template.author = current_user
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
template.save!
template
end
def create_file_params_from_url def create_file_params_from_url
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
@@ -28,6 +28,12 @@ class UserSignaturesController < ApplicationController
end end
end end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
end
private private
def load_user_config def load_user_config
+2
View File
@@ -19,6 +19,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
import FolderAutocomplete from './elements/folder_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form' import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form' import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -51,6 +52,7 @@ window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
window.customElements.define('folder-autocomplete', FolderAutocomplete) window.customElements.define('folder-autocomplete', FolderAutocomplete)
window.customElements.define('signature-form', SignatureForm) window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm) window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => { document.addEventListener('turbo:submit-end', async (event) => {
@@ -32,6 +32,8 @@ export default class extends HTMLElement {
render = (item) => { render = (item) => {
const div = document.createElement('div') const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item.name div.textContent = item.name
return div return div
@@ -0,0 +1,19 @@
export default class extends HTMLElement {
connectedCallback () {
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'password'
input.value = prompt('Enter PDF password')
this.form.append(input)
this.form.requestSubmit()
this.remove()
}
get form () {
return this.closest('form')
}
}
@@ -56,6 +56,8 @@ export default class extends HTMLElement {
render = (item) => { render = (item) => {
const div = document.createElement('div') const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item[this.dataset.field] div.textContent = item[this.dataset.field]
return div return div
+1
View File
@@ -19,6 +19,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton), completedButton: JSON.parse(this.dataset.completedButton),
completedRedirectUrl: this.dataset.completedRedirectUrl,
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields) fields: JSON.parse(this.dataset.fields)
}) })
@@ -0,0 +1,52 @@
<template>
<div
v-if="uniqueAreas.length > 1"
dir="auto"
class="mb-2"
>
<div class="flex space-x-1 text-sm ml-1 -mt-3">
<span>
{{ t('appears_on') }}:
</span>
<a
v-for="area in uniqueAreas"
:key="area.uuid"
href="#"
class="link pr-0.5"
@click.prevent="scrollIntoArea(area)"
>
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'AppearsOn',
inject: ['t', 'scrollIntoArea'],
props: {
field: {
type: Object,
required: true
}
},
computed: {
isMultipleDocs () {
return this.attachmentUuids.length > 1
},
attachmentUuids () {
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
},
uniqueAreas () {
const areas = {}
this.field.areas?.forEach((area) => {
areas[area.attachment_uuid + area.page] ||= area
})
return Object.values(areas).slice(0, 6)
}
}
}
</script>
+37 -15
View File
@@ -1,8 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base" class="flex absolute lg:text-base"
dir="auto"
:style="computedStyle" :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 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, '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 }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -21,15 +22,20 @@
</span> </span>
</div> </div>
<div <div
v-if="isActive && withLabel && !area.option_uuid" v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
> >
{{ field.name || fieldNames[field.type] }} <template v-if="area.option_uuid && !option.value">
<template v-if="field.type === 'checkbox' && !field.name"> {{ optionValue(option) }}
{{ fieldIndex + 1 }}
</template> </template>
<template v-else-if="!field.required && field.type !== 'checkbox'"> <template v-else>
(optional) {{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
</template>
</template> </template>
</div> </div>
<div <div
@@ -102,11 +108,11 @@
:value="false" :value="false"
class="aspect-square base-radio" class="aspect-square base-radio"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" :checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)" @click="$emit('update:model-value', optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" v-else-if="!!modelValue && modelValue === optionValue(option)"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -121,11 +127,11 @@
:value="false" :value="false"
class="aspect-square base-checkbox" class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" :checked="!!modelValue && modelValue.includes(optionValue(option))"
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)" @change="updateMultipleSelectValue(optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -146,7 +152,8 @@
<div <div
v-else v-else
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" dir="auto"
class="flex items-center px-0.5 w-full"
> >
<span v-if="Array.isArray(modelValue)"> <span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }} {{ modelValue.join(', ') }}
@@ -171,6 +178,7 @@ export default {
IconPaperclip, IconPaperclip,
IconCheck IconCheck
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -241,6 +249,9 @@ export default {
payment: 'Payment' payment: 'Payment'
} }
}, },
option () {
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,
@@ -335,6 +346,17 @@ export default {
} }
}, },
methods: { methods: {
optionValue (option) {
if (option) {
if (option.value) {
return option.value
} else {
const index = this.field.options.indexOf(option)
return `${this.t('option')} ${index + 1}`
}
}
},
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { const monthFormats = {
M: 'numeric', M: 'numeric',
+15 -12
View File
@@ -83,22 +83,25 @@ export default {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`) return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
}, },
scrollIntoField (field) { scrollIntoField (field) {
this.areaRefs.find((area) => { if (field?.areas) {
if (area.field === field) { this.scrollIntoArea(field.areas[0])
const root = this.$root.$el.parentNode.getRootNode() }
const container = root.body || root.querySelector('div') },
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
if (container.style.overflow === 'hidden') { if (areaRef) {
this.scrollInContainer(area.$el) const root = this.$root.$el.parentNode.getRootNode()
} else { const container = root.body || root.querySelector('div')
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
return true if (container.style.overflow === 'hidden') {
this.scrollInContainer(areaRef.$el)
} else { } else {
return null areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
} }
})
return true
}
}, },
scrollInContainer (target) { scrollInContainer (target) {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
+4 -1
View File
@@ -1,5 +1,8 @@
<template> <template>
<div class="mx-auto max-w-md flex flex-col"> <div
class="mx-auto max-w-md flex flex-col"
dir="auto"
>
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto"> <p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck <IconCircleCheck
class="inline text-green-600" class="inline text-green-600"
+5 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
:for="field.uuid" :for="field.uuid"
@@ -15,6 +15,7 @@
{{ t('set_today') }} {{ t('set_today') }}
</button> </button>
</div> </div>
<AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
ref="input" ref="input"
@@ -31,11 +32,13 @@
<script> <script>
import { IconCalendarCheck } from '@tabler/icons-vue' import { IconCalendarCheck } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'DateStep', name: 'DateStep',
components: { components: {
IconCalendarCheck IconCalendarCheck,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
+59 -23
View File
@@ -24,7 +24,8 @@
<div <div
v-show="isFormVisible" v-show="isFormVisible"
id="form_container" id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded" class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<button <button
@@ -45,7 +46,7 @@
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
class="md:mx-16" :class="{ 'md:mx-16': isBreakpointMd }"
@submit.prevent="submitStep" @submit.prevent="submitStep"
> >
<input <input
@@ -64,7 +65,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
</div> </div>
<DateStep <DateStep
@@ -72,12 +73,13 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
<label <label
v-if="currentField.name" v-if="currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -86,13 +88,15 @@
v-else v-else
class="py-1" class="py-1"
/> />
<AppearsOn :field="currentField" />
<select <select
:id="currentField.uuid" :id="currentField.uuid"
dir="auto"
:required="currentField.required" :required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
> >
<option <option
value="" value=""
@@ -114,6 +118,7 @@
<label <label
v-if="currentField.name" v-if="currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -121,7 +126,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="option in currentField.options" v-for="(option, index) in currentField.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -134,11 +139,11 @@
type="radio" type="radio"
class="base-radio !h-7 !w-7" class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
:value="option.value" :value="option.value || `${t('option')} ${index + 1}`"
:required="currentField.required" :required="currentField.required"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ option.value || `${t('option')} ${index + 1}` }}
</span> </span>
</label> </label>
</div> </div>
@@ -197,9 +202,17 @@
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`" :onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required" :required="field.required"
:checked="!!values[field.uuid]" :checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]" @click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
v-if="field.title"
class="text-xl"
v-html="field.title"
/>
<span
v-else
class="text-xl"
> >
<span class="text-xl">
{{ field.name || field.type + ' ' + (index + 1) }} {{ field.name || field.type + ' ' + (index + 1) }}
</span> </span>
</label> </label>
@@ -215,7 +228,7 @@
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<SignatureStep <SignatureStep
v-else-if="currentField.type === 'signature'" v-else-if="currentField.type === 'signature'"
@@ -229,7 +242,7 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<InitialsStep <InitialsStep
@@ -243,8 +256,8 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<AttachmentStep <AttachmentStep
@@ -255,7 +268,7 @@
:field="currentField" :field="currentField"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<PhoneStep <PhoneStep
v-else-if="currentField.type === 'phone'" v-else-if="currentField.type === 'phone'"
@@ -265,7 +278,7 @@
:field="currentField" :field="currentField"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
<PaymentStep <PaymentStep
@@ -276,7 +289,7 @@
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
</div> </div>
@@ -319,9 +332,9 @@
v-else v-else
:is-demo="isDemo" :is-demo="isDemo"
:attribution="attribution" :attribution="attribution"
:completed-button="completedButton" :completed-button="completedRedirectUrl ? {} : completedButton"
:with-send-copy-button="withSendCopyButton" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
:with-download-button="withDownloadButton" :with-download-button="withDownloadButton && !completedRedirectUrl"
:with-confetti="withConfetti" :with-confetti="withConfetti"
:can-send-email="canSendEmail && !!submitter.email" :can-send-email="canSendEmail && !!submitter.email"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -362,6 +375,7 @@ import TextStep from './text_step'
import DateStep from './date_step' import DateStep from './date_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import { t } from './i18n' import { t } from './i18n'
export default { export default {
@@ -370,6 +384,7 @@ export default {
FieldAreas, FieldAreas,
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AppearsOn,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
@@ -385,6 +400,7 @@ export default {
provide () { provide () {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea,
t: this.t t: this.t
} }
}, },
@@ -420,6 +436,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
isBreakpointMd: {
type: Boolean,
required: false,
default: true
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@@ -475,6 +496,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
completedRedirectUrl: {
type: String,
required: false,
default: ''
},
completedButton: { completedButton: {
type: Object, type: Object,
required: false, required: false,
@@ -521,7 +547,7 @@ export default {
isButtonDisabled () { isButtonDisabled () {
if (this.recalculateButtonDisabledKey) { if (this.recalculateButtonDisabledKey) {
return this.isSubmitting || return this.isSubmitting ||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) || (this.currentField.required && ['image', 'file', 'multiple'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) || (this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted) (this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
} else { } else {
@@ -681,7 +707,7 @@ export default {
this.recalculateButtonDisabledKey = Math.random() this.recalculateButtonDisabledKey = Math.random()
if (scrollToArea) { if (scrollToArea) {
this.$refs.areas.scrollIntoField(step[0]) this.scrollIntoField(step[0])
} }
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus() this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
@@ -709,6 +735,12 @@ export default {
}) })
} }
}, },
scrollIntoField (field) {
return this.$refs.areas.scrollIntoField(field)
},
scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area)
},
async submitStep () { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
@@ -718,7 +750,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid] return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && (this.submittedValues[fields[0].uuid] === true ? false : !this.submittedValues[fields[0].uuid]?.length)
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
@@ -757,6 +789,10 @@ export default {
if (respData) { if (respData) {
this.onComplete(JSON.parse(respData)) this.onComplete(JSON.parse(respData))
} }
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
}
} }
}).catch(error => { }).catch(error => {
console.error(error) console.error(error)
+77 -10
View File
@@ -1,7 +1,10 @@
const en = { const en = {
submit_form: 'Submit Form', submit_form: 'Submit Form',
type_here: 'Type here', type_here_: 'Type here...',
optional: 'optional', optional: 'optional',
option: 'Option',
appears_on: 'Appears on',
page: 'Page',
select_your_option: 'Select your option', select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click', complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
submit: 'submit', submit: 'submit',
@@ -45,8 +48,10 @@ const en = {
const es = { const es = {
submit_form: 'Enviar Formulario', submit_form: 'Enviar Formulario',
type_here: 'Escribe aquí', type_here_: 'Escribe aquí...',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece en',
page: 'Página',
select_your_option: 'Selecciona tu opción', select_your_option: 'Selecciona tu opción',
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic', complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
submit: 'enviar', submit: 'enviar',
@@ -90,8 +95,10 @@ const es = {
const it = { const it = {
submit_form: 'Invia Modulo', submit_form: 'Invia Modulo',
type_here: 'Digita qui', type_here_: 'Digita qui...',
optional: 'opzionale', optional: 'opzionale',
appears_on: 'Compare su',
page: 'Pagina',
select_your_option: 'Seleziona la tua opzione', select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic', complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia', submit: 'invia',
@@ -135,8 +142,10 @@ const it = {
const de = { const de = {
submit_form: 'Formular absenden', submit_form: 'Formular absenden',
type_here: 'Hier eingeben', type_here_: 'Hier eingeben...',
optional: 'optional', optional: 'optional',
appears_on: 'Erscheint auf',
page: 'Seite',
select_your_option: 'Wähle deine Option', select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken', complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden', submit: 'absenden',
@@ -180,8 +189,10 @@ const de = {
const fr = { const fr = {
submit_form: 'Envoyer le Formulaire', submit_form: 'Envoyer le Formulaire',
type_here: 'Tapez ici', type_here_: 'Tapez ici...',
optional: 'facultatif', optional: 'facultatif',
appears_on: 'Apparaît sur',
page: 'Page',
select_your_option: 'Sélectionnez votre option', select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez', complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer', submit: 'envoyer',
@@ -225,8 +236,10 @@ const fr = {
const pl = { const pl = {
submit_form: 'Wyślij Formularz', submit_form: 'Wyślij Formularz',
type_here: 'Wpisz tutaj', type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny', optional: 'opcjonalny',
appears_on: 'Pojawia się na',
page: 'Strona',
select_your_option: 'Wybierz swoją opcję', select_your_option: 'Wybierz swoją opcję',
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij', complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij', submit: 'wyślij',
@@ -270,8 +283,10 @@ const pl = {
const uk = { const uk = {
submit_form: 'Надіслати Форму', submit_form: 'Надіслати Форму',
type_here: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
select_your_option: 'Виберіть варіант', select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть', complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати', submit: 'надіслати',
@@ -315,8 +330,10 @@ const uk = {
const cs = { const cs = {
submit_form: 'Odeslat formulář', submit_form: 'Odeslat formulář',
type_here: 'Zadejte zde', type_here_: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu', select_your_option: 'Vyberte svou volbu',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na', complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat', submit: 'odeslat',
@@ -360,8 +377,10 @@ const cs = {
const pt = { const pt = {
submit_form: 'Enviar Formulário', submit_form: 'Enviar Formulário',
type_here: 'Digite aqui', type_here_: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece em',
page: 'Página',
select_your_option: 'Selecione sua opção', select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique', complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar', submit: 'enviar',
@@ -403,7 +422,55 @@ const pt = {
pay_with_strip: 'Pagar com Stripe' pay_with_strip: 'Pagar com Stripe'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt } const he = {
submit_form: 'שלח טופס',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
appears_on: 'מופיע ב',
page: 'עמוד',
select_your_option: 'בחר את האפשרות שלך',
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
submit: 'שליחה',
next: 'הבא',
click_to_upload: 'לחץ להעלאה',
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
send_copy_via_email: 'שלח עותק באמצעות אימייל',
download: 'הורדה',
signature: 'חתימה',
initials: 'ציוני ראשי תיבות',
clear: 'נקה',
redraw: 'צייר מחדש',
draw_initials: 'צייר ציוני ראשי תיבות',
type_signature_here: 'הקלד חתימה כאן',
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
form_has_been_completed: 'הטופס הושלם',
create_a_free_account: 'צור חשבון חינם',
signed_with: 'חתום באמצעות',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
six_digits_code: 'קוד משתמש שש ספרות',
change_phone_number: 'שינוי מספר טלפון',
sending: 'שולח',
resend_code: 'שלח מחדש קוד',
verification_code_has_been_resent: 'קוד האימות נשלח מחדש בSMS',
please_fill_all_required_fields: 'אנא מלא את כל השדות הדרושים',
set_today: 'קבע היום',
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
draw_signature: 'צייר חתימה',
type_initial: 'הקלד ציוני ראשי תיבות',
draw: 'צייר',
type: 'הקלד',
type_text: 'הקלד טקסט',
date: 'תאריך',
email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד',
pay_with_strip: 'שלם עם סטרייפ'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he }
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0] const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else v-else
class="tooltip" class="tooltip ml-2"
:data-tip="t('draw_initials')" :data-tip="t('draw_initials')"
> >
<a <a
@@ -70,6 +70,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -103,10 +104,12 @@
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconTextSize, IconTextSize,
IconSignature, IconSignature,
@@ -2,12 +2,20 @@
<label <label
v-if="field.name" v-if="field.name"
:for="field.uuid" :for="field.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }}</label> >{{ field.name }}</label>
<div class="flex w-full"> <div class="flex w-full">
<input
v-if="modelValue.length === 0"
type="text"
:name="`values[${field.uuid}][]`"
:value="''"
class="hidden"
>
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="option in field.options" v-for="(option, index) in field.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -19,13 +27,13 @@
:ref="setInputRef" :ref="setInputRef"
type="checkbox" type="checkbox"
:name="`values[${field.uuid}][]`" :name="`values[${field.uuid}][]`"
:value="option.value" :value="optionValue(option, index)"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(option.value)" :checked="(modelValue || []).includes(optionValue(option, index))"
@change="onChange" @change="onChange"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ optionValue(option, index) }}
</span> </span>
</label> </label>
</div> </div>
@@ -36,6 +44,7 @@
<script> <script>
export default { export default {
name: 'MultiSelectStep', name: 'MultiSelectStep',
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -62,8 +71,15 @@ export default {
this.inputRefs.push(el) this.inputRefs.push(el)
} }
}, },
optionValue (option, index) {
if (option.value) {
return option.value
} else {
return `${this.t('option')} ${index + 1}`
}
},
onChange () { onChange () {
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value)) this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
} }
} }
} }
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else-if="withTypedSignature" v-else-if="withTypedSignature"
class="tooltip" class="tooltip ml-2"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
<a <a
@@ -89,6 +89,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -122,12 +123,14 @@
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
let isFontLoaded = false let isFontLoaded = false
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconCamera, IconCamera,
IconTextSize, IconTextSize,
+18 -5
View File
@@ -2,26 +2,36 @@
<label <label
v-if="field.name" v-if="field.name"
:for="field.uuid" :for="field.uuid"
class="label text-2xl mb-2" dir="auto"
>{{ field.name }} class="label text-2xl"
:class="{ 'mb-2': !field.description }"
><template v-if="field.title"><span v-html="field.title" /></template>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<div <div
v-else v-else
class="py-1" class="py-1"
/> />
<div
v-if="field.description"
class="mb-3 px-1 text-lg"
v-html="field.description"
/>
<AppearsOn :field="field" />
<div class="items-center flex"> <div class="items-center flex">
<input <input
v-if="!isTextArea" v-if="!isTextArea"
:id="field.uuid" :id="field.uuid"
v-model="text" v-model="text"
:maxlength="cellsMaxLegth" :maxlength="cellsMaxLegth"
dir="auto"
class="base-input !text-2xl w-full !pr-11 -mr-10" class="base-input !text-2xl w-full !pr-11 -mr-10"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''" :oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''" :oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
type="text" type="text"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@focus="$emit('focus')" @focus="$emit('focus')"
@@ -31,8 +41,9 @@
:id="field.uuid" :id="field.uuid"
ref="textarea" ref="textarea"
v-model="text" v-model="text"
dir="auto"
class="base-textarea !text-2xl w-full" class="base-textarea !text-2xl w-full"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
:required="field.required" :required="field.required"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@input="resizeTextarea" @input="resizeTextarea"
@@ -56,11 +67,13 @@
<script> <script>
import { IconAlignBoxLeftTop } from '@tabler/icons-vue' import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'TextStep', name: 'TextStep',
components: { components: {
IconAlignBoxLeftTop IconAlignBoxLeftTop,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
+3 -2
View File
@@ -61,6 +61,7 @@
v-if="field.type !== 'checkbox' || field.name" v-if="field.type !== 'checkbox' || field.name"
ref="name" ref="name"
:contenteditable="editable && !defaultField" :contenteditable="editable && !defaultField"
dir="auto"
class="pr-1 cursor-text outline-none block" class="pr-1 cursor-text outline-none block"
style="min-width: 2px" style="min-width: 2px"
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@@ -83,7 +84,7 @@
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >{{ t('required') }}</label>
</div> </div>
<button <button
v-else-if="editable" v-else-if="editable"
@@ -149,7 +150,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save'], inject: ['template', 'selectedAreaRef', 'save', 't'],
props: { props: {
area: { area: {
type: Object, type: Object,
+98 -14
View File
@@ -43,11 +43,11 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Sign Yourself {{ t('sign_yourself') }}
</span> </span>
</a> </a>
<a <a
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowEmptyTemplateAlert"
@@ -57,7 +57,7 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Send {{ t('send') }}
</span> </span>
</a> </a>
<button <button
@@ -76,7 +76,7 @@
width="22" width="22"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Save {{ t('save') }}
</span> </span>
</button> </button>
</template> </template>
@@ -147,6 +147,7 @@
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="defaultFields"
:allow-draw="!onlyDefinedFields"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -178,6 +179,7 @@
v-if="withUploadButton" v-if="withUploadButton"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:accept-file-types="acceptFileTypes"
@success="updateFromUpload" @success="updateFromUpload"
/> />
</div> </div>
@@ -198,6 +200,7 @@
v-if="sortedDocuments.length && !drawField && editable" v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields" :fields="template.fields"
:default-fields="defaultFields" :default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
/> />
@@ -214,16 +217,24 @@
> >
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
<p> <p>
Draw {{ drawField.name }} field on the document {{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
</p> </p>
<p> <div>
<button <button
class="base-button" class="base-button"
@click="[drawField = null, drawOption = null]" @click="clearDrawField"
> >
Cancel {{ t('cancel') }}
</button> </button>
</p> <a
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
href="#"
class="link block mt-3 text-sm"
@click.prevent="[drawField = null, drawOption = null]"
>
{{ t('or_add_field_without_drawing') }}
</a>
</div>
</div> </div>
</div> </div>
<div> <div>
@@ -232,9 +243,12 @@
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:default-fields="defaultFields" :default-fields="defaultFields"
:field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:only-defined-fields="onlyDefinedFields"
:editable="editable" :editable="editable"
@set-draw="[drawField = $event.field, drawOption = $event.option]" @set-draw="[drawField = $event.field, drawOption = $event.option]"
@set-drag="dragField = $event" @set-drag="dragField = $event"
@@ -262,6 +276,7 @@ import MobileFields from './mobile_fields'
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue' import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { en as i18nEn } from './i18n'
export default { export default {
name: 'TemplateBuilder', name: 'TemplateBuilder',
@@ -285,7 +300,9 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withPayment: this.withPayment, withPayment: this.withPayment,
@@ -302,6 +319,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
i18n: {
type: Object,
required: false,
default: () => ({})
},
backgroundColor: { backgroundColor: {
type: String, type: String,
required: false, required: false,
@@ -312,11 +334,26 @@ export default {
required: false, required: false,
default: true default: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
autosave: {
type: Boolean,
required: false,
default: true
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
default: () => [] default: () => []
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -344,6 +381,13 @@ export default {
return () => {} return () => {}
} }
}, },
onSave: {
type: Function,
required: false,
default () {
return () => {}
}
},
withStickySubmitters: { withStickySubmitters: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -379,6 +423,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: false
},
fetchOptions: { fetchOptions: {
type: Object, type: Object,
required: false, required: false,
@@ -470,6 +519,9 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
t (key) {
return this.i18n[key] || i18nEn[key] || key
},
startFieldDraw ({ name, type }) { startFieldDraw ({ name, type }) {
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name) const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
@@ -549,10 +601,21 @@ export default {
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }) ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, },
clearDrawField () {
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
const fieldIndex = this.template.fields.indexOf(this.drawField)
if (fieldIndex !== -1) {
this.template.fields.splice(fieldIndex, 1)
}
}
this.drawField = null
this.drawOption = null
},
onKeyUp (e) { onKeyUp (e) {
if (e.code === 'Escape') { if (e.code === 'Escape') {
this.drawField = null this.clearDrawField()
this.drawOption = null
this.selectedAreaRef.value = null this.selectedAreaRef.value = null
} }
@@ -645,7 +708,16 @@ export default {
} }
this.drawField.areas ||= [] this.drawField.areas ||= []
this.drawField.areas.push(area)
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
})
if (insertBeforeAreaIndex !== -1) {
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
} else {
this.drawField.areas.push(area)
}
if (this.template.fields.indexOf(this.drawField) === -1) { if (this.template.fields.indexOf(this.drawField) === -1) {
this.template.fields.push(this.drawField) this.template.fields.push(this.drawField)
@@ -661,7 +733,11 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const pageMask = documentRef.pageRefs[area.page].$refs.mask const pageMask = documentRef.pageRefs[area.page].$refs.mask
const type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text' let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
type = this.fieldTypes[0]
}
if (type === 'checkbox') { if (type === 'checkbox') {
const previousField = [...this.template.fields].reverse().find((f) => f.type === type) const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
@@ -887,7 +963,11 @@ export default {
headers: { ...this.fetchOptions.headers, ...options.headers } headers: { ...this.fetchOptions.headers, ...options.headers }
}) })
}, },
save () { save ({ force } = { force: false }) {
if (!this.autosave && !force) {
return Promise.resolve({})
}
this.$nextTick(() => { this.$nextTick(() => {
if (this.$el.closest('template-builder')) { if (this.$el.closest('template-builder')) {
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template) this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
@@ -907,6 +987,10 @@ export default {
} }
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(() => {
if (this.onSave) {
this.onSave(this.template)
}
}) })
} }
} }
@@ -5,6 +5,7 @@
> >
<span <span
ref="contenteditable" ref="contenteditable"
dir="auto"
:contenteditable="editable" :contenteditable="editable"
style="min-width: 2px" style="min-width: 2px"
:class="iconInline ? 'inline' : 'block'" :class="iconInline ? 'inline' : 'block'"
@@ -7,6 +7,7 @@
:number="index" :number="index"
:editable="editable" :editable="editable"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
:default-fields="defaultFields" :default-fields="defaultFields"
:draw-field="drawField" :draw-field="drawField"
@@ -41,6 +42,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
+6 -6
View File
@@ -29,7 +29,7 @@
{{ message }} {{ message }}
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span class="font-medium">Click to upload</span> or drag and drop files <span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
</div> </div>
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@ export default {
IconCloudUpload, IconCloudUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -91,13 +91,13 @@ export default {
}, },
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return 'Uploading...' return this.t('uploading')
} else if (this.isProcessing) { } else if (this.isProcessing) {
return 'Processing...' return this.t('processing_')
} else if (this.acceptFileTypes === 'image/*, application/pdf') { } else if (this.acceptFileTypes === 'image/*, application/pdf') {
return 'Add PDF documents or images' return this.t('add_pdf_documents_or_images')
} else { } else {
return 'Add documents or images' return this.t('add_documents_or_images')
} }
} }
}, },
+19 -16
View File
@@ -47,7 +47,7 @@
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >{{ t('required') }}</label>
</template> </template>
</div> </div>
<div <div
@@ -101,7 +101,8 @@
<input <input
v-model="field.default_value" v-model="field.default_value"
type="text" type="text"
placeholder="Default value" :placeholder="t('default_value')"
dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save" @blur="save"
> >
@@ -111,7 +112,7 @@
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
> >
Default value {{ t('default_value') }}
</label> </label>
</div> </div>
<div <div
@@ -121,7 +122,7 @@
> >
<select <select
v-model="field.preferences.format" v-model="field.preferences.format"
placeholder="Format" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="save" @change="save"
> >
@@ -138,7 +139,7 @@
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
> >
Format {{ t('format') }}
</label> </label>
</div> </div>
<li <li
@@ -152,7 +153,7 @@
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">Required</span> <span class="label-text">{{ t('required') }}</span>
</label> </label>
</li> </li>
<li <li
@@ -166,7 +167,7 @@
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">Read-only</span> <span class="label-text">{{ t('read_only') }}</span>
</label> </label>
</li> </li>
<hr class="pb-0.5 mt-0.5"> <hr class="pb-0.5 mt-0.5">
@@ -183,7 +184,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Page {{ area.page + 1 }} {{ t('page') }} {{ area.page + 1 }}
</a> </a>
</li> </li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"> <li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
@@ -196,7 +197,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Draw New Area {{ t('draw_new_area') }}
</a> </a>
</li> </li>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)"> <li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
@@ -209,7 +210,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Copy to All Pages {{ t('copy_to_all_pages') }}
</a> </a>
</li> </li>
</ul> </ul>
@@ -249,8 +250,9 @@
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6" class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
type="text" type="text"
dir="auto"
required required
:placeholder="`Option ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
@blur="save" @blur="save"
> >
<button <button
@@ -268,9 +270,10 @@
v-else v-else
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`Option ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
type="text" type="text"
required required
dir="auto"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="save"
> >
@@ -287,7 +290,7 @@
class="text-center text-sm w-full pb-1" class="text-center text-sm w-full pb-1"
@click="addOption" @click="addOption"
> >
+ Add option + {{ t('add_option') }}
</button> </button>
</div> </div>
</div> </div>
@@ -313,7 +316,7 @@ export default {
IconCopy, IconCopy,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'], inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -366,7 +369,7 @@ export default {
} else { } else {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field) const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field' const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}` return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
} }
@@ -419,7 +422,7 @@ export default {
this.template.documents.forEach((attachment) => { this.template.documents.forEach((attachment) => {
attachment.preview_images.forEach((page) => { attachment.preview_images.forEach((page) => {
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) { if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
field.areas.push({ ...JSON.parse(areaString), page: parseInt(page.filename) }) field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page: parseInt(page.filename) })
} }
}) })
}) })
@@ -78,7 +78,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -105,6 +105,7 @@
</label> </label>
<label <label
v-else v-else
ref="label"
tabindex="0" tabindex="0"
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between" class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
> >
@@ -155,13 +156,36 @@
{{ submitter.name }} {{ submitter.name }}
</span> </span>
</span> </span>
<button <div
v-if="!compact && submitters.length > 1 && editable" v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2" class="flex"
@click.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
</button> <button
title="Up"
class="relative w-2"
style="font-size: 10px; margin-bottom: -4px"
@click.stop="[move(submitter, -1), $refs.label.focus()] "
>
▲
</button>
<button
title="Down"
class="relative w-2"
style="font-size: 10px; margin-top: -4px"
@click.stop="[move(submitter, 1), $refs.label.focus()] "
>
▼
</button>
</div>
<button
v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2"
@click.stop="remove(submitter)"
>
<IconTrashX :width="18" />
</button>
</div>
</a> </a>
</li> </li>
<li v-if="submitters.length < 10 && editable"> <li v-if="submitters.length < 10 && editable">
@@ -175,7 +199,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -197,6 +221,7 @@ export default {
IconTrashX, IconTrashX,
IconChevronUp IconChevronUp
}, },
inject: ['t', 'save'],
props: { props: {
submitters: { submitters: {
type: Array, type: Array,
@@ -250,16 +275,16 @@ export default {
}, },
names () { names () {
return [ return [
'First Party', this.t('first_party'),
'Second Party', this.t('second_party'),
'Third Party', this.t('third_party'),
'Fourth Party', this.t('fourth_party'),
'Fifth Party', this.t('fifth_party'),
'Sixth Party', this.t('sixth_party'),
'Seventh Party', this.t('seventh_party'),
'Eighth Party', this.t('eighth_party'),
'Ninth Party', this.t('ninth_party'),
'Tenth Party' this.t('tenth_party')
] ]
}, },
selectedSubmitter () { selectedSubmitter () {
@@ -275,6 +300,23 @@ export default {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
move (submitter, direction) {
const currentIndex = this.submitters.indexOf(submitter)
this.submitters.splice(currentIndex, 1)
if (currentIndex + direction > this.submitters.length) {
this.submitters.unshift(submitter)
} else if (currentIndex + direction < 0) {
this.submitters.push(submitter)
} else {
this.submitters.splice(currentIndex + direction, 0, submitter)
}
this.selectSubmitter(submitter)
this.save()
},
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.submitters.length],
+16 -16
View File
@@ -29,7 +29,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
> >
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -54,7 +54,7 @@ import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckb
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment'], inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -90,20 +90,20 @@ export default {
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
text: 'Text', text: this.t('text'),
signature: 'Signature', signature: this.t('signature'),
initials: 'Initials', initials: this.t('initials'),
date: 'Date', date: this.t('date'),
image: 'Image', image: this.t('image'),
file: 'File', file: this.t('file'),
select: 'Select', select: this.t('select'),
checkbox: 'Checkbox', checkbox: this.t('checkbox'),
multiple: 'Multiple', multiple: this.t('multiple'),
radio: 'Radio', radio: this.t('radio'),
cells: 'Cells', cells: this.t('cells'),
stamp: 'Stamp', stamp: this.t('stamp'),
payment: 'Payment', payment: this.t('payment'),
phone: 'Phone' phone: this.t('phone')
} }
}, },
fieldIcons () { fieldIcons () {
+22 -7
View File
@@ -64,7 +64,7 @@
</template> </template>
</div> </div>
<div <div
v-if="editable" v-if="editable && !onlyDefinedFields"
class="grid grid-cols-3 gap-1 pb-2" class="grid grid-cols-3 gap-1 pb-2"
> >
<template <template
@@ -72,7 +72,7 @@
:key="type" :key="type"
> >
<button <button
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
draggable="true" draggable="true"
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative" class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
@@ -91,7 +91,7 @@
</div> </div>
</button> </button>
<div <div
v-else-if="type == 'phone'" v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex" class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }" :class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }"
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification." data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
@@ -120,7 +120,7 @@
</template> </template>
</div> </div>
<div <div
v-if="fields.length < 4 && editable" v-if="fields.length < 4 && editable && withHelp"
class="text-xs p-2 border border-base-200 rounded" class="text-xs p-2 border border-base-200 rounded"
> >
<ul class="list-disc list-outside ml-3"> <ul class="list-disc list-outside ml-3">
@@ -131,7 +131,7 @@
Drag &amp; drop any other field type on the page Drag &amp; drop any other field type on the page
</li> </li>
<li> <li>
Click on the field type above to add it to the form without drawing it on the document Click on the field type above to start drawing it
</li> </li>
</ul> </ul>
</div> </div>
@@ -154,12 +154,17 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'], inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -170,6 +175,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: true
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -180,6 +190,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
submitters: { submitters: {
type: Array, type: Array,
required: true required: true
@@ -285,7 +300,7 @@ export default {
this.fields.push(field) this.fields.push(field)
if (['signature', 'initials', 'cells', 'stamp'].includes(type)) { if (!['payment', 'file'].includes(type)) {
this.$emit('set-draw', { field }) this.$emit('set-draw', { field })
} }
+57
View File
@@ -0,0 +1,57 @@
const en = {
sign_yourself: 'Sign Yourself',
send: 'Send',
save: 'Save',
cancel: 'Cancel',
draw_field_on_the_document: 'Draw {field} field on the document',
click_to_upload: 'Click to upload',
or_drag_and_drop_files: 'or drag and drop files',
uploading: 'Uploading',
processing_: 'Processing...',
add_pdf_documents_or_images: 'Add PDF documents or images',
add_documents_or_images: 'Add documents or images',
required: 'Required',
default_value: 'Default value',
format: 'Format',
read_only: 'Read-only',
page: 'Page',
draw_new_area: 'Draw New Area',
copy_to_all_pages: 'Copy to All Pages',
add_option: 'Add option',
option: 'Option',
first_party: 'First Party',
second_party: 'Second Party',
third_party: 'Third Party',
fourth_party: 'Fourth Party',
fifth_party: 'Fifth Party',
sixth_party: 'Sixth Party',
seventh_party: 'Seventh Party',
eighth_party: 'Eighth Party',
ninth_party: 'Ninth Party',
tenth_party: 'Tenth Party',
add: 'Add',
or_add_field_without_drawing: 'Or add field without drawing',
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
field: 'Field',
group: 'Group',
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
draw_field: 'Draw {field} Field',
replace: 'Replace',
uploading_: 'Uploading...',
add_document: 'Add Document'
}
export { en }
@@ -9,14 +9,14 @@
class="inline" class="inline"
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span> Draw {{ fieldNames[drawField.type] }} Field </span> <span>{{ t('draw_field').replace('{field}', fieldNames[drawField.type]) }}</span>
</div> </div>
<a <a
href="#" href="#"
class="link block text-center" class="link block text-center"
@click.prevent="$emit('cancel')" @click.prevent="$emit('cancel')"
> >
Cancel {{ t('cancel') }}
</a> </a>
</span> </span>
<FieldSubmitter <FieldSubmitter
@@ -43,7 +43,7 @@ export default {
Field, Field,
FieldSubmitter FieldSubmitter
}, },
inject: ['save'], inject: ['save', 't'],
props: { props: {
drawField: { drawField: {
type: Object, type: Object,
@@ -48,7 +48,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
> >
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -78,7 +78,7 @@ export default {
IconPlus, IconPlus,
IconX IconX
}, },
inject: ['withPhone', 'withPayment', 'backgroundColor'], inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -94,6 +94,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
+18 -1
View File
@@ -33,7 +33,7 @@
<FieldArea <FieldArea
v-if="newArea" v-if="newArea"
:is-draw="true" :is-draw="true"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || defaultFieldType }"
:area="newArea" :area="newArea"
/> />
</div> </div>
@@ -60,6 +60,7 @@ export default {
components: { components: {
FieldArea FieldArea
}, },
inject: ['fieldTypes'],
props: { props: {
image: { image: {
type: Object, type: Object,
@@ -75,6 +76,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
@@ -110,6 +116,13 @@ export default {
} }
}, },
computed: { computed: {
defaultFieldType () {
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
return this.fieldTypes[0]
} else {
return 'text'
}
},
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
@@ -149,6 +162,10 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (!this.allowDraw) {
return
}
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField) {
return return
} }
+4 -4
View File
@@ -26,7 +26,7 @@ import Upload from './upload'
export default { export default {
name: 'ReplaceDocument', name: 'ReplaceDocument',
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -56,11 +56,11 @@ export default {
}, },
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return 'Uploading...' return this.t('uploading_')
} else if (this.isProcessing) { } else if (this.isProcessing) {
return 'Processing...' return this.t('processing_')
} else { } else {
return 'Replace' return this.t('replace')
} }
} }
}, },
+55 -10
View File
@@ -15,13 +15,13 @@
width="20" width="20"
/> />
<span v-if="isLoading"> <span v-if="isLoading">
Uploading... {{ t('uploading_') }}
</span> </span>
<span v-else-if="isProcessing"> <span v-else-if="isProcessing">
Processing... {{ t('processing_') }}
</span> </span>
<span v-else> <span v-else>
Add Document {{ t('add_document') }}
</span> </span>
</label> </label>
<form <form
@@ -50,7 +50,7 @@ export default {
IconUpload, IconUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -123,9 +123,30 @@ export default {
method: 'POST', method: 'POST',
body: JSON.stringify({ blobs }), body: JSON.stringify({ blobs }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => { }).then((resp) => {
this.$emit('success', data) if (resp.ok) {
this.$refs.input.value = '' resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else {
alert('Wrong password')
}
})
}
})
}
}).finally(() => { }).finally(() => {
this.isProcessing = false this.isProcessing = false
}) })
@@ -133,9 +154,33 @@ export default {
this.baseFetch(`/api/templates/${this.templateId}/documents`, { this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST', method: 'POST',
body: new FormData(this.$refs.form) body: new FormData(this.$refs.form)
}).then(resp => resp.json()).then((data) => { }).then((resp) => {
this.$emit('success', data) if (resp.ok) {
this.$refs.input.value = '' resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
const formData = new FormData(this.$refs.form)
formData.append('password', prompt('Enter PDF password'))
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: formData
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else {
alert('Wrong password')
}
})
}
})
}
}).finally(() => { }).finally(() => {
this.isLoading = false this.isLoading = false
}) })
+18 -7
View File
@@ -22,22 +22,33 @@ class ProcessSubmitterCompletionJob < ApplicationJob
end end
def enqueue_completed_emails(submitter) def enqueue_completed_emails(submitter)
user = submitter.submission.created_by_user || submitter.template.author submission = submitter.submission
user = submission.created_by_user || submitter.template.author
if submitter.template.account.users.exists?(id: user.id) && if submitter.template.account.users.exists?(id: user.id) &&
submitter.submission.preferences['send_email'] != false submission.preferences['send_email'] != false
bcc = submitter.submission.template.account.account_configs SubmitterMailer.completed_email(submitter, user).deliver_later!
.find_by(key: AccountConfig::BCC_EMAILS)&.value
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later! bcc = submission.preferences['bcc_completed'].presence ||
submission.template.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
end
end end
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } to = build_to_addresses(submitter)
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present? SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
end end
def build_to_addresses(submitter)
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter)
next_submitter_item = next_submitter_item =
submitter.submission.template_submitters.find do |e| submitter.submission.template_submitters.find do |e|
+11 -8
View File
@@ -33,7 +33,7 @@ class SubmitterMailer < ApplicationMailer
) )
end end
def completed_email(submitter, user, bcc: nil) def completed_email(submitter, user, to: nil)
@current_account = submitter.submission.template.account @current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
@submission = submitter.submission @submission = submitter.submission
@@ -43,7 +43,9 @@ class SubmitterMailer < ApplicationMailer
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
add_completed_email_attachments!(submitter) add_completed_email_attachments!(
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
)
subject = subject =
if @email_config if @email_config
@@ -55,8 +57,7 @@ class SubmitterMailer < ApplicationMailer
end end
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name, to: to || (user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name),
bcc:,
subject:) subject:)
end end
@@ -66,10 +67,12 @@ class SubmitterMailer < ApplicationMailer
Submissions::EnsureResultGenerated.call(@submitter) Submissions::EnsureResultGenerated.call(@submitter)
@documents = add_completed_email_attachments!(submitter)
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@documents = add_completed_email_attachments!(
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
)
subject = subject =
if @email_config if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:) ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
@@ -84,13 +87,13 @@ class SubmitterMailer < ApplicationMailer
private private
def add_completed_email_attachments!(submitter) def add_completed_email_attachments!(submitter, with_audit_log: true)
documents = Submitters.select_attachments_for_download(submitter) documents = Submitters.select_attachments_for_download(submitter)
total_size = 0 total_size = 0
audit_trail_data = nil audit_trail_data = nil
if submitter.submission.audit_trail.present? if with_audit_log && submitter.submission.audit_trail.present?
audit_trail_data = submitter.submission.audit_trail.download audit_trail_data = submitter.submission.audit_trail.download
total_size = audit_trail_data.size total_size = audit_trail_data.size
+1 -1
View File
@@ -25,7 +25,7 @@ class AccessToken < ApplicationRecord
belongs_to :user belongs_to :user
before_validation :set_sha256, on: :create before_validation :set_sha256
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) } attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
+1 -1
View File
@@ -6,7 +6,7 @@
<%= f.fields_for current_account do |ff| %> <%= f.fields_for current_account do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %> <%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %> <%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
+1
View File
@@ -8,6 +8,7 @@
<div class="flex flex-row space-x-4"> <div class="flex flex-row space-x-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
</div> </div>
</div> </div>
</div> </div>
+2 -1
View File
@@ -33,7 +33,8 @@
</div> </div>
<% end %> <% end %>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<%= form_for '', url: templates_upload_path, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<input type="hidden" name="form_id" value="<%= form_id %>">
<button type="submit" class="hidden"></button> <button type="submit" class="hidden"></button>
<file-dropzone data-submit-on-upload="true" class="w-full"> <file-dropzone data-submit-on-upload="true" class="w-full">
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed"> <label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
@@ -41,13 +41,6 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %> <%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
<% if Docuseal.multitenant? %>
<button id="talk_to_sales_button" class="hidden" type="submit" formaction="<%= enquiries_path %>" formnovalidate="true" formmethod="post"></button>
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="talk_to_sales" class="base-checkbox" onchange="window.talk_to_sales_button.click()">
<span class="label">I would like to talk to sales</span>
</label>
<% end %>
</div> </div>
<% end %> <% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
+1 -1
View File
@@ -6,7 +6,7 @@
<style> <style>
</style> </style>
</head> </head>
<body> <body dir="<%= yield&.match?(TextUtils::RTL_REGEXP) ? 'rtl' : 'auto' %>">
<%= yield %> <%= yield %>
<%= render partial: 'shared/mailer_attribution' %> <%= render partial: 'shared/mailer_attribution' %>
</body> </body>
+9 -3
View File
@@ -4,11 +4,16 @@
<div class="text-center"> <div class="text-center">
<div class="relative flex flex-col items-center"> <div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %> <%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-8"> <h1 class="text-6xl font-bold mt-4 mb-4">
DocuSeal DocuSeal
</h1> </h1>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<h2 class="mb-16 text-lg text-center text-gray-600"> <h2 class="mt-4 mb-16 text-lg text-center text-gray-600">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing. A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2> </h2>
</div> </div>
@@ -62,7 +67,8 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3> <h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>Open-source contributors are always ready to help! Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>
Open-source contributors are always ready to help!
</p> </p>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -8,17 +8,28 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<div class="flex items-center justify-between py-2.5 mx-1">
<span>
Attach audit log PDF
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
@@ -11,7 +11,7 @@
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :title, 'Button title', class: 'label' %> <%= ff.label :title, 'Button title', class: 'label' %>
<%= ff.text_field :title, class: 'base-input' %> <%= ff.text_field :title, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :url, 'Button URL', class: 'label' %> <%= ff.label :url, 'Button URL', class: 'label' %>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -11,12 +11,17 @@
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<% end %> <% end %>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -8,17 +8,33 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <div class="flex items-center">
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.label :subject, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, 'subject').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<div class="flex items-center justify-between py-2.5 mx-1">
<span>
Attach audit log PDF
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
+4 -3
View File
@@ -6,11 +6,11 @@
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
<%= f.label :first_name, class: 'label' %> <%= f.label :first_name, class: 'label' %>
<%= f.text_field :first_name, required: true, class: 'base-input' %> <%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :last_name, class: 'label' %> <%= f.label :last_name, class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input' %> <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -25,7 +25,8 @@
<p class="text-2xl font-bold mt-8 mb-4">Signature</p> <p class="text-2xl font-bold mt-8 mb-4">Signature</p>
<% signature = UserConfigs.load_signature(current_user) %> <% signature = UserConfigs.load_signature(current_user) %>
<% if signature %> <% if signature %>
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: 'Remove', disabled_with: 'Removing'), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>"> <img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>">
</div> </div>
<% end %> <% end %>
+5
View File
@@ -116,6 +116,11 @@
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center"> <a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %> <%= Docuseal::SUPPORT_EMAIL %>
</a> </a>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
+1 -1
View File
@@ -3,7 +3,7 @@
<div> <div>
<p class="font-bold">Send signature requests via SMS</p> <p class="font-bold">Send signature requests via SMS</p>
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Enterprise Unlock with DocuSeal Pro
<br> <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::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p> </p>
+1 -1
View File
@@ -3,7 +3,7 @@
<div> <div>
<p class="font-bold">Single Sign On with SAML 2.0</p> <p class="font-bold">Single Sign On with SAML 2.0</p>
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Enterprise Unlock with DocuSeal Pro
<br> <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::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p> </p>
+6 -6
View File
@@ -10,23 +10,23 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %></p> <p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center text-2xl font-semibold"> <div dir="auto" class="text-center text-2xl font-semibold">
Form has been submitted already <%= t('form_has_been_submitted_already') %>
</div> </div>
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %> <% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
<div> <div>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: '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' %> <%= 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> </div>
<% end %> <% end %>
<% if @template.submitters.to_a.size == 1 %> <% if @template.submitters.to_a.size == 1 %>
<div> <div>
<%= button_to button_title(title: 'Resubmit', disabled_with: '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' %> <%= 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> </div>
<% end %> <% end %>
</div> </div>
+8 -8
View File
@@ -4,7 +4,7 @@
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'banner' %>
<% unless @template.archived_at? %> <% unless @template.archived_at? %>
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p> <p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
<% end %> <% end %>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
@@ -15,9 +15,9 @@
<div> <div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p> <p class="text-lg font-bold mb-1"><%= @template.name %></p>
<% if @template.archived_at? %> <% if @template.archived_at? %>
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @template.account.name %></span>.</p> <p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
<% else %> <% else %>
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p> <p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -25,13 +25,13 @@
</div> </div>
<% unless @template.archived_at? %> <% unless @template.archived_at? %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
<div class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.label :email, class: 'label' %> <%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %> <%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t('provide_your_email_to_start') %>
<%= @error_message %> <%= @error_message %>
</div> </div>
<div class="form-control"> <div dir="auto" class="form-control">
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %> <%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@@ -18,7 +18,7 @@
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '' %> <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '', dir: 'auto' %>
</submitters-autocomplete> </submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>"> <div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
+187
View File
@@ -0,0 +1,187 @@
<div>
<div class="">
<input id="api_key" type="text" value="<%= start_form_url(slug: template.slug) %>" class="input font-mono input-bordered w-full bg-white" autocomplete="off" readonly>
<div class="mt-4">
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: template.slug), class: 'base-button w-full block', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</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>
<div class="mt-4">
<a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a>
</div>
</div>
+2 -2
View File
@@ -23,14 +23,14 @@
</submitters-autocomplete> </submitters-autocomplete>
<% if template.submitters.size > 1 %> <% if template.submitters.size > 1 %>
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
</submitters-autocomplete> </submitters-autocomplete>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size == 1 %> <% if template.submitters.size == 1 %>
<div class="form-control flex"> <div class="form-control flex">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
</submitters-autocomplete> </submitters-autocomplete>
</div> </div>
<% end %> <% end %>
+8 -3
View File
@@ -33,12 +33,17 @@
<div class="form-control space-y-2"> <div class="form-control space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :subject, class: 'label' %> <%= f.label :subject, class: 'label' %>
<%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full' %> <%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :message, 'Body', class: 'label' %> <div class="flex items-center">
<%= f.label :message, 'Body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %> <%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<%= render 'message_fields' %> <%= render 'message_fields' %>
+1 -1
View File
@@ -1,4 +1,4 @@
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%"> <field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %> <% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %> <% elsif field['type'].in?(['file', 'payment']) %>
+6 -1
View File
@@ -1,5 +1,5 @@
<%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %> <%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %>
<% options = [['via Email', 'email'], ['via Phone', 'phone'], ['Detailed', 'detailed']] %> <% options = [['via Email', 'email'], ['via Phone', 'phone'], %w[Detailed detailed], (Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 ? ['via Link', 'link'] : nil)].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block">
<div class="join"> <div class="join">
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
@@ -22,6 +22,11 @@
<div id="detailed" class="hidden"> <div id="detailed" class="hidden">
<%= render 'detailed_form', template: @template %> <%= render 'detailed_form', template: @template %>
</div> </div>
<% if Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 %>
<div id="link" class="hidden">
<%= render 'link_form', template: @template %>
</div>
<% end %>
</div> </div>
<%= content_for(:modal_extra) %> <%= content_for(:modal_extra) %>
<% end %> <% end %>
+6 -6
View File
@@ -35,7 +35,7 @@
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<div class="pb-2 pt-1.5 text-center"> <div class="pb-2 pt-1.5 text-center" dir="auto">
<%= item['name'].presence || document.filename.base %> <%= item['name'].presence || document.filename.base %>
</div> </div>
</a> </a>
@@ -74,13 +74,14 @@
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space"> <div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %> <% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %> <% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% submitter_field_counters = Hash.new { 0 } %>
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %> <% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %> <% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10"> <div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
<div class="border border-base-300 rounded-md px-2 py-1 mb-1"> <div class="border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span> <span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
<span class="text-lg"> <span class="text-lg" dir="auto">
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span> </span>
</div> </div>
@@ -125,23 +126,22 @@
<% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %> <% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>"> <a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
Submit Form Sign In-person
</a> </a>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="px-1.5 mb-4"> <div class="px-1.5 mb-4">
<% submitter_field_counters = Hash.new { 0 } %>
<% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %> <% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %>
<% submitter_field_counters[field['type']] += 1 %> <% submitter_field_counters[field['type']] += 1 %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% next if value.blank? %> <% next if value.blank? %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
<div class="text-xs font-medium uppercase mb-0.5"> <div class="text-xs font-medium uppercase mb-0.5" dir="auto">
<%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %>
</div> </div>
<div> <div dir="auto">
<% if field['type'].in?(%w[signature initials]) %> <% if field['type'].in?(%w[signature initials]) %>
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
@@ -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_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 %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% configs = Submitters::FormConfigs.call(submitter) %> <% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" 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-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>
+2 -2
View File
@@ -9,9 +9,9 @@
<div class="mr-3"> <div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @submitter.submission.template.account.name %></span>.</p> <p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.submission.template.account.name) %></p>
</div> </div>
</div> </div>
</div> </div>
+8 -8
View File
@@ -10,17 +10,17 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %></p> <p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %> <% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: '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: 'white-button w-full' %> <%= 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: 'white-button w-full' %>
<% if @submitter.submission.template.submitters.size != 1 %> <% if @submitter.submission.template.submitters.size != 1 %>
<div class="divider">OR</div> <div class="divider uppercase"><%= t('or') %></div>
<% else %> <% else %>
<div class="py-2"></div> <div class="py-2"></div>
<% end %> <% end %>
@@ -28,18 +28,18 @@
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full"> <download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %> <%= svg_icon('download', class: 'w-6 h-6') %>
<span>Download documents</span> <span><%= t('download_documents') %></span>
</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-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %> <%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
<span>Downloading</span> <span><%= t('downloading') %></span>
</span> </span>
</download-button> </download-button>
</div> </div>
<% if @submitter.submission.template.submitters.size == 1 %> <% if @submitter.submission.template.submitters.size == 1 %>
<div class="divider">OR</div> <div class="divider uppercase"><%= t('or') %></div>
<div> <div>
<%= button_to button_title(title: 'Resubmit', disabled_with: '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' %> <%= 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' %>
</div> </div>
<% end %> <% end %>
</div> </div>
+17
View File
@@ -0,0 +1,17 @@
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center p-2 mb-4 justify-center">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('circle_check', class: 'w-10 h-10 text-green-600') %>
</div>
<div class="text-2xl">
<%= t('completed_successfully') %>
</div>
</div>
</div>
</div>
</div>
</div>
<%= render 'shared/attribution', link_path: '/start' %>
+1 -1
View File
@@ -1,7 +1,7 @@
<%= render 'shared/turbo_modal', title: 'Rename Folder' do %> <%= render 'shared/turbo_modal', title: 'Rename Folder' do %>
<%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> <%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<div class="form-control my-6"> <div class="form-control my-6">
<%= f.text_field :name, required: true, placeholder: 'Folder Name...', class: 'base-input w-full', autofocus: true %> <%= f.text_field :name, required: true, placeholder: 'Folder Name...', class: 'base-input w-full', autofocus: true, dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Rename', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Rename', disabled_with: 'Saving'), class: 'base-button' %>
+2 -1
View File
@@ -1,4 +1,4 @@
<%= form_for '', url: templates_upload_path, method: :post, class: 'inline', html: { enctype: 'multipart/form-data' } do %> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'inline', html: { enctype: 'multipart/form-data' } do %>
<button type="submit" class="btn btn-ghost text-base" onclick="[event.preventDefault(), window.upload_template.click()]"> <button type="submit" class="btn btn-ghost text-base" onclick="[event.preventDefault(), window.upload_template.click()]">
<span class="enabled"> <span class="enabled">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
@@ -13,6 +13,7 @@
</span> </span>
</span> </span>
</button> </button>
<input type="hidden" name="form_id" value="<%= form_id %>">
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple> <input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>"> <input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %> <% end %>
+1 -1
View File
@@ -4,7 +4,7 @@
<%= hidden_field_tag :base_template_id, @base_template.id %> <%= hidden_field_tag :base_template_id, @base_template.id %>
<% end %> <% end %>
<div class="form-control mt-6"> <div class="form-control mt-6">
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input' %> <%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="mt-3 mb-4 flex items-center justify-between"> <div class="mt-3 mb-4 flex items-center justify-between">
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]"> <a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
+1 -1
View File
@@ -78,7 +78,7 @@
<p>Send an invitation to fill and complete the form</p> <p>Send an invitation to fill and complete the form</p>
<div class="space-y-2 flex flex-col"> <div class="space-y-2 flex flex-col">
<% if can?(:create, Submission) %> <% if can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1">Send to Recipients</span> <span class="mr-1">Send to Recipients</span>
<% end %> <% end %>
+2 -2
View File
@@ -2,11 +2,11 @@
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :first_name, class: 'label' %> <%= f.label :first_name, class: 'label' %>
<%= f.text_field :first_name, required: true, class: 'base-input' %> <%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :last_name, class: 'label' %> <%= f.label :last_name, class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input' %> <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
+1 -1
View File
@@ -7,7 +7,7 @@
<% end %> <% 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::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %> <%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
Unlock more user roles with DocuSeal Enterprise. Unlock more user roles with DocuSeal Pro.
<span class="link font-medium">Learn More</a> <span class="link font-medium">Learn More</a>
</a> </a>
</div> </div>
+1 -1
View File
@@ -22,7 +22,7 @@ module DocuSeal
config.active_storage.routes_prefix = '' config.active_storage.routes_prefix = ''
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE pt] 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.fallbacks = [:en] config.i18n.fallbacks = [:en]
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) } config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
+1 -1
View File
@@ -82,7 +82,7 @@ Rails.application.configure do
ActiveRecord::Encryption.configure(**config.active_record.encryption) ActiveRecord::Encryption.configure(**config.active_record.encryption)
# Raises error for missing translations. # Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names. # Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true # config.action_view.annotate_rendered_view_with_filenames = true
+1 -1
View File
@@ -85,7 +85,7 @@ Rails.application.configure do
user_name: ENV.fetch('SMTP_USERNAME', nil), user_name: ENV.fetch('SMTP_USERNAME', nil),
password: ENV.fetch('SMTP_PASSWORD', nil), password: ENV.fetch('SMTP_PASSWORD', nil),
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'), authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
}.compact }.compact
end end
@@ -1,4 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
Rails.application.config.filter_parameters += %i[password token otp_attempt passw secret token _key crypt salt Rails.application.config.filter_parameters += %i[password token otp_attempt passw secret token _key crypt salt
certificate otp ssn] certificate otp ssn file]
+19
View File
@@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'twitter_cldr/shared/bidi'
require 'twitter_cldr/shared/code_point'
require 'twitter_cldr/resources/loader'
module TwitterCldr
RESOURCES_DIR = File.join(Gem::Specification.find_by_name('twitter_cldr').gem_dir, 'resources')
module_function
def resources
@resources ||= TwitterCldr::Resources::Loader.new
end
def get_resource(...)
resources.get_resource(...)
end
end
+214 -11
View File
@@ -1,29 +1,232 @@
en-US: en: &en
email: Email
digitally_signed_by: Digitally signed by digitally_signed_by: Digitally signed by
role: Role role: Role
provide_your_email_to_start: Provide your email to start
start: Start
starting: Starting
form_has_been_deleted_by_html: 'Form has been deleted by <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Invited by <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: You have been invited to submit a form
signed_on_time: 'Signed on %{time}'
form_has_been_submitted_already: Form has been submitted already
send_copy_to_email: Send copy to Email
sending: Sending
resubmit: Resubmit
form_has_been_deleted_by_html: 'Form has been deleted by <span class="font-semibold">%{name}</span>.'
or: or
download_documents: Download documents
downloading: Downloading
completed_successfully: Completed Successfully
es: &es
digitally_signed_by: Firmado digitalmente por
role: Rol
email: Correo electrónico
digitally_signed_by: Firmado digitalmente por
role: Rol
provide_your_email_to_start: Proporciona tu correo electrónico para comenzar
start: Comenzar
starting: Comenzando
form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Invitado por <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Has sido invitado/a a enviar un formulario
signed_on_time: 'Firmado en %{time}'
form_has_been_submitted_already: El formulario ya ha sido enviado
send_copy_to_email: Enviar copia a Correo electrónico
sending: Enviando
resubmit: Reenviar
or: o
download_documents: Descargar documentos
downloading: Descargando
completed_successfully: Completado exitosamente
it:
digitally_signed_by: Signé numériquement par
role: Rôle
email: Email
digitally_signed_by: Firmato digitalmente da
role: Ruolo
provide_your_email_to_start: Fornisci la tua email per iniziare
start: Inizia
starting: Iniziando
form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Invitato da <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Sei stato invitato a inviare un modulo
signed_on_time: 'Firmato alle %{time}'
form_has_been_submitted_already: Il modulo è già stato inviato
send_copy_to_email: Invia copia via email
sending: Inviando
resubmit: Reinvia
or: o
download_documents: Scarica documenti
downloading: Scaricamento
completed_successfully: Completato con successo
fr: &fr
email: Email
digitally_signed_by: Signé numériquement par
role: Rôle
provide_your_email_to_start: Entrez votre email pour commencer
start: Démarrer
starting: Démarrage
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Invité par <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Vous avez été invité à soumettre un formulaire
signed_on_time: 'Signé à %{time}'
form_has_been_submitted_already: Le formulaire a déjà été soumis
send_copy_to_email: Envoyer une copie par email
sending: Envoi
resubmit: Soumettre à nouveau
or: ou
download_documents: Télécharger des documents
downloading: Téléchargement
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
role: Função
provide_your_email_to_start: Forneça o seu email para começar
start: Iniciar
starting: Iniciando
form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Convidado por <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Foi convidado a submeter um formulário
signed_on_time: 'Assinado em %{time}'
form_has_been_submitted_already: O formulário já foi submetido
send_copy_to_email: Enviar cópia para Email
sending: Enviando
resubmit: Reenviar
or: ou
download_documents: Baixar documentos
downloading: Baixando
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
role: Rolle
provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten
start: Starten
starting: Starten
form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.'
invited_by_html: 'Eingeladen von <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Du wurdest eingeladen, ein Formular einzureichen
signed_on_time: 'Unterzeichnet um %{time}'
form_has_been_submitted_already: Formular wurde bereits eingereicht
send_copy_to_email: Kopie per E-Mail senden
sending: Senden
resubmit: Erneut einreichen
or: oder
download_documents: Dokumente herunterladen
downloading: Herunterladen
completed_successfully: Erfolgreich abgeschlossen
pl:
email: Email
digitally_signed_by: Podpis cyfrowy przez
role: Rola
provide_your_email_to_start: Podaj swój adres email, aby rozpocząć
start: Rozpocznij
starting: Rozpoczynanie
form_has_been_deleted_by_html: 'Formularz został usunięty przez <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Zaproszony przez <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Zostałeś zaproszony do przesłania formularza
signed_on_time: 'Podpisano %{time}'
form_has_been_submitted_already: Formularz został już przesłany
send_copy_to_email: Wyślij kopię na Email
sending: Wysyłanie
resubmit: Prześlij ponownie
or: lub
download_documents: Pobierz dokumenty
downloading: Pobieranie
completed_successfully: Zakończono pomyślnie
uk:
email: Email
digitally_signed_by: Цифровий підпис від
role: Роль
provide_your_email_to_start: Введіть свій email, щоб почати
start: Почати
starting: Початок
form_has_been_deleted_by_html: 'Форму було видалено користувачем <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Запрошений користувачем <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Вас запросили подати форму
signed_on_time: 'Підписаний %{time}'
form_has_been_submitted_already: Форма вже була подана
send_copy_to_email: Надіслати копію на Email
sending: Надсилання
resubmit: Подати знову
or: або
download_documents: Завантажити документи
downloading: Завантаження
completed_successfully: Успішно завершено
cs:
email: Email
digitally_signed_by: Digitálně podepsáno uživatelem
role: Role
provide_your_email_to_start: Zadejte svůj email pro zahájení
start: Zahájit
starting: Zahajování
form_has_been_deleted_by_html: 'Formulář byl smazán uživatelem <span class="font-semibold">%{name}</span>.'
invited_by_html: 'Pozván uživatelem <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: Byli jste pozváni k odeslání formuláře
signed_on_time: 'Podepsáno %{time}'
form_has_been_submitted_already: Formulář již byl odeslán
send_copy_to_email: Odeslat kopii na Email
sending: Odesílání
resubmit: Odeslat znovu
or: nebo
download_documents: Stáhnout dokumenty
downloading: Stahování
completed_successfully: Úspěšně dokončeno
he:
email: דוא"ל
digitally_signed_by: חתום דיגיטלית על ידי
role: תפקיד
provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל
start: התחל
starting: מתחיל
form_has_been_deleted_by_html: 'הטופס נמחק על ידי <span class="font-semibold">%{name}</span>.'
invited_by_html: 'הוזמן על ידי <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: הוזמנת להגיש טופס
signed_on_time: 'חתום ב-%{time}'
form_has_been_submitted_already: הטופס כבר נשלח
send_copy_to_email: שלח עותק לדוא"ל
sending: שולח
resubmit: שלח מחדש
or: או
download_documents: הורד מסמכים
downloading: מוריד
completed_successfully: הושלם בהצלחה
en-US:
<<: *en
date: date:
formats: formats:
default: "%m/%d/%Y" default: "%m/%d/%Y"
en-GB: en-GB:
digitally_signed_by: Digitally signed by <<: *en
role: Role
date: date:
formats: formats:
default: "%d/%m/%Y" default: "%d/%m/%Y"
es-ES: es-ES:
digitally_signed_by: Firmado digitalmente por <<: *es
role: Rol
fr-FR: fr-FR:
digitally_signed_by: Signé numériquement par <<: *fr
role: Rôle
pt-PT: pt-PT:
digitally_signed_by: Assinado digitalmente por <<: *pt
role: Função
de-DE: de-DE:
digitally_signed_by: Digital signiert von <<: *de
role: Rolle
+1 -1
View File
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'development') environment ENV.fetch('RAILS_ENV', 'development')
# Specifies the `pidfile` that Puma will use. # Specifies the `pidfile` that Puma will use.
# pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid") pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
# Specifies the number of `workers` to boot in clustered mode. # Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together # Workers are forked web server processes. If using threads and workers together
+6 -2
View File
@@ -59,7 +59,7 @@ Rails.application.routes.draw do
resource :newsletter, only: %i[show update] resource :newsletter, only: %i[show update]
resources :enquiries, only: %i[create] resources :enquiries, only: %i[create]
resources :users, only: %i[new create edit update destroy] resources :users, only: %i[new create edit update destroy]
resource :user_signature, only: %i[edit update] resource :user_signature, only: %i[edit update destroy]
resources :submissions, only: %i[show destroy] resources :submissions, only: %i[show destroy]
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
resource :templates_upload, only: %i[create] resource :templates_upload, only: %i[create]
@@ -81,6 +81,10 @@ Rails.application.routes.draw do
get :completed get :completed
end end
resources :submit_form, only: %i[], path: '' do
get :success, on: :collection
end
resources :submit_form, only: %i[show update], path: 's', param: 'slug' do resources :submit_form, only: %i[show update], path: 's', param: 'slug' do
get :completed get :completed
end end
@@ -109,7 +113,7 @@ Rails.application.routes.draw do
resources :users, only: %i[index] resources :users, only: %i[index]
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
if !Docuseal.multitenant? || Docuseal.demo? if !Docuseal.multitenant? || Docuseal.demo?
resources :api, only: %i[index], controller: 'api_settings' resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings' resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
end end
resource :account, only: %i[show update] resource :account, only: %i[show update]
+5
View File
@@ -29,6 +29,7 @@ module Docuseal
CERTS = JSON.parse(ENV.fetch('CERTS', '{}')) CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
TIMESERVER_URL = ENV.fetch('TIMESERVER_URL', nil) TIMESERVER_URL = ENV.fetch('TIMESERVER_URL', nil)
VERSION_FILE_PATH = Rails.root.join('.version')
DEFAULT_URL_OPTIONS = { DEFAULT_URL_OPTIONS = {
host: HOST, host: HOST,
@@ -37,6 +38,10 @@ module Docuseal
module_function module_function
def version
@version ||= VERSION_FILE_PATH.read.strip if VERSION_FILE_PATH.exist?
end
def multitenant? def multitenant?
ENV['MULTITENANT'] == 'true' ENV['MULTITENANT'] == 'true'
end end
+46 -14
View File
@@ -1,46 +1,78 @@
# frozen_string_literal: true # frozen_string_literal: true
class PdfProcessor < HexaPDF::Content::Processor class PdfProcessor < HexaPDF::Content::Processor
attr_reader :result, :contents attr_accessor :handler, :serializer
attr_accessor :handler
class ParseTextHandler
attr_accessor :pos, :num, :search_chars, :handler
def initialize(handler)
@num = 0
@pos = 0
@handler = handler
@search_chars = handler.search_chars
end
TJS = %i[TJ Tj].freeze
def call(processor, operator, operands)
return unless TJS.include?(operator)
processor.send(:decode_text, *operands).chars.each do |char|
handler.tokens << [char, [@num, @pos]] if search_chars.include?(char)
@pos += 1
end
@pos = 0
@num += 1
end
end
def initialize(page) def initialize(page)
super super
@contents = ''.b
@result = []
@serializer = HexaPDF::Serializer.new @serializer = HexaPDF::Serializer.new
end end
def serialize(operator, operands)
operators[operator].serialize(serializer, *operands)
end
def process(operator, operands = []) def process(operator, operands = [])
super super
contents << @operators[operator].serialize( handler.call(self, operator, operands)
@serializer,
*handler.call(self, operator, operands)
)
end end
def self.call(data, pdf_handler, result_handler, acc = {}) def self.call(data, process_handler, result_handler, acc = {})
doc = HexaPDF::Document.new(io: StringIO.new(data)) doc = HexaPDF::Document.new(io: StringIO.new(data))
doc.pages.each do |page| doc.pages.each do |page|
processor = PdfProcessor.new(page) processor = PdfProcessor.new(page)
processor.handler = pdf_handler process_handler_instance = process_handler.new
processor.handler = ParseTextHandler.new(process_handler_instance)
page.process_contents(processor) page.process_contents(processor)
page.contents = processor.contents if process_handler_instance.tokens?
processor = PdfProcessor.new(page)
processor.handler = process_handler_instance
processor.result.each do |item| page.process_contents(processor)
page.contents = process_handler_instance.contents
end
process_handler_instance.result.each do |item|
result_handler.call(item, page, acc) result_handler.call(item, page, acc)
end end
end end
new_io = StringIO.new new_io = StringIO.new
doc.write(new_io) doc.write(new_io, validate: false)
[new_io.tap(&:rewind).read, acc] [new_io.tap(&:rewind).read, acc]
end end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
module PdfUtils
module_function
def encrypted?(data, password: nil)
HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
false
rescue HexaPDF::EncryptionError
true
end
def decrypt(data, password)
encrypted_doc = HexaPDF::Document.new(io: StringIO.new(data), decryption_opts: { password: })
decrypted_doc = HexaPDF::Document.new
encrypted_doc.pages.each do |page|
decrypted_doc.pages << decrypted_doc.import(page)
end
decrypted_io = StringIO.new
decrypted_doc.write(decrypted_io, validate: false)
decrypted_io.tap(&:rewind).read
end
end
+2
View File
@@ -8,6 +8,7 @@ module ReplaceEmailVariables
SUBMITTER_EMAIL = '{{submitter.email}}' SUBMITTER_EMAIL = '{{submitter.email}}'
SUBMITTER_NAME = '{{submitter.name}}' SUBMITTER_NAME = '{{submitter.name}}'
SUBMITTER_ID = '{{submitter.id}}' SUBMITTER_ID = '{{submitter.id}}'
SUBMITTER_SLUG = '{{submitter.slug}}'
SUBMISSION_LINK = '{{submission.link}}' SUBMISSION_LINK = '{{submission.link}}'
SUBMISSION_ID = '{{submission.id}}' SUBMISSION_ID = '{{submission.id}}'
SUBMISSION_SUBMITTERS = '{{submission.submitters}}' SUBMISSION_SUBMITTERS = '{{submission.submitters}}'
@@ -25,6 +26,7 @@ module ReplaceEmailVariables
text = text.gsub(TEMPLATE_NAME, submitter.template.name) if submitter.template text = text.gsub(TEMPLATE_NAME, submitter.template.name) if submitter.template
text = text.gsub(TEMPLATE_ID, submitter.template.id.to_s) if submitter.template text = text.gsub(TEMPLATE_ID, submitter.template.id.to_s) if submitter.template
text = text.gsub(SUBMITTER_ID, submitter.id.to_s) text = text.gsub(SUBMITTER_ID, submitter.id.to_s)
text = text.gsub(SUBMITTER_SLUG, submitter.slug.to_s)
text = text.gsub(SUBMISSION_ID, submitter.submission.id.to_s) if submitter.submission text = text.gsub(SUBMISSION_ID, submitter.submission.id.to_s) if submitter.submission
text = text.gsub(SUBMITTER_EMAIL, submitter.email) if submitter.email text = text.gsub(SUBMITTER_EMAIL, submitter.email) if submitter.email
text = text.gsub(SUBMITTER_NAME, submitter.name || submitter.email || submitter.phone) text = text.gsub(SUBMITTER_NAME, submitter.name || submitter.email || submitter.phone)
+22 -12
View File
@@ -11,7 +11,7 @@ module Submissions
submission_preferences = Submitters.normalize_preferences(template.account, user, attrs) submission_preferences = Submitters.normalize_preferences(template.account, user, attrs)
submission_preferences = preferences.merge(submission_preferences) submission_preferences = preferences.merge(submission_preferences)
set_submission_preferences = submission_preferences.slice('send_email') set_submission_preferences = submission_preferences.slice('send_email', 'bcc_completed')
set_submission_preferences['send_email'] = true if params['send_completed_email'] set_submission_preferences['send_email'] = true if params['send_completed_email']
submission = template.submissions.new(created_by_user: user, source:, submission = template.submissions.new(created_by_user: user, source:,
@@ -98,19 +98,28 @@ module Submissions
next if field_configs.blank? next if field_configs.blank?
f['readonly'] = field_configs['readonly'] if field_configs['readonly'].present? assign_field_attrs(f, field_configs)
f['default_value'] = field_configs['default_value'] if field_configs['default_value'].present? &&
!f['type'].in?(%w[signature image initials file])
next if field_configs['validation_pattern'].blank?
f['validation'] = {
'pattern' => field_configs['validation_pattern'],
'message' => field_configs['invalid_message']
}.compact_blank
end end
end end
def assign_field_attrs(field, attrs)
field['title'] = attrs['title'] if attrs['title'].present?
field['description'] = attrs['description'] if attrs['description'].present?
field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
field['required'] = attrs['required'] if attrs.key?('required')
field['default_value'] = attrs['default_value'] if attrs['default_value'].present? &&
!field['type'].in?(%w[signature image initials file])
return field if attrs['validation_pattern'].blank?
field['validation'] = {
'pattern' => attrs['validation_pattern'],
'message' => attrs['invalid_message']
}.compact_blank
field
end
def find_submitter_uuid(template, attrs, index) def find_submitter_uuid(template, attrs, index)
attrs[:uuid].presence || attrs[:uuid].presence ||
template.submitters.find { |e| e['name'] == attrs[:role] }&.dig('uuid') || template.submitters.find { |e| e['name'] == attrs[:role] }&.dig('uuid') ||
@@ -130,7 +139,8 @@ module Submissions
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil, sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {}, values: attrs[:values] || {},
preferences: preferences.merge(submitter_preferences) preferences: preferences.merge(submitter_preferences)
.merge({ default_values: attrs[:values] }.compact_blank), .merge({ default_values: attrs[:values] }.compact_blank)
.except('bcc_completed'),
uuid: uuid:
) )
end end
+10 -4
View File
@@ -27,6 +27,8 @@ module Submissions
'GBP' => '£' 'GBP' => '£'
}.freeze }.freeze
RTL_REGEXP = TextUtils::RTL_REGEXP
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
@@ -147,7 +149,7 @@ module Submissions
[ [
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" }, submission.template_submitters.size > 1 && { text: "#{item['name']}\n" },
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] }, submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] },
submitter.name && { text: "#{submitter.name}\n" }, submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
submitter.phone && { text: "#{submitter.phone}\n" } submitter.phone && { text: "#{submitter.phone}\n" }
].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0] ].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0]
) )
@@ -185,11 +187,13 @@ module Submissions
composer.formatted_text_box( composer.formatted_text_box(
[ [
{ {
text: field['name'].to_s.upcase.presence || text: TextUtils.maybe_rtl_reverse(field['name'].to_s).upcase.presence ||
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase, "#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase,
font_size: 6 font_size: 6
} }
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0] ].compact_blank,
align: field['name'].to_s.match?(RTL_REGEXP) ? :right : :left,
line_spacing: 1.8, padding: [0, 0, 5, 0]
), ),
if field['type'].in?(%w[image signature initials stamp]) if field['type'].in?(%w[image signature initials stamp])
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
@@ -233,7 +237,9 @@ module Submissions
value = value.join(', ') if value.is_a?(Array) value = value.join(', ') if value.is_a?(Array)
composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0]) composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
padding: [0, 0, 10, 0])
end end
] ]
end end
+14 -8
View File
@@ -13,6 +13,8 @@ module Submissions
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co' SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co' SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co'
RTL_REGEXP = TextUtils::RTL_REGEXP
TEXT_LEFT_MARGIN = 1 TEXT_LEFT_MARGIN = 1
TEXT_TOP_MARGIN = 1 TEXT_TOP_MARGIN = 1
@@ -56,10 +58,12 @@ module Submissions
height = page.box.height height = page.box.height
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, font: pdf.fonts.add(FONT_NAME), font_size:)
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center,
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
font: pdf.fonts.add(FONT_NAME), font_size:)
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
canvas = page.canvas(type: :overlay) canvas = page.canvas(type: :overlay)
@@ -155,7 +159,7 @@ module Submissions
when 'cells' when 'cells'
cell_width = area['cell_w'] * width cell_width = area['cell_w'] * width
value.chars.each_with_index do |char, index| TextUtils.maybe_rtl_reverse(value).chars.each_with_index do |char, index|
text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME), text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME),
font_size:) font_size:)
@@ -168,14 +172,16 @@ module Submissions
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end end
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME), value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
font_size:)
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
font_size:)
lines = layouter.fit([text], area['w'] * width, height).lines lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height) box_height = lines.sum(&:height)
if box_height > (area['h'] * height) + 1 if box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), text = HexaPDF::Layout::TextFragment.create(value,
font: pdf.fonts.add(FONT_NAME), font: pdf.fonts.add(FONT_NAME),
font_size: (font_size / 1.4).to_i) font_size: (font_size / 1.4).to_i)
@@ -250,9 +256,9 @@ module Submissions
sign_params[:signature_size] = 10_000 sign_params[:signature_size] = 10_000
end end
pdf.sign(io, **sign_params) pdf.sign(io, write_options: { validate: false }, **sign_params)
else else
pdf.write(io, incremental: true) pdf.write(io, incremental: true, validate: false)
end end
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
+2
View File
@@ -59,6 +59,8 @@ module Submitters
preferences['email_message_uuid'] = email_message.uuid if email_message preferences['email_message_uuid'] = email_message.uuid if email_message
preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email') preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email')
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
preferences preferences
end end
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more