mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce1323e849 | ||
|
|
fc3c1e65e3 | ||
|
|
f1ebd8f912 | ||
|
|
98f5bba832 | ||
|
|
e864e8a47f | ||
|
|
a573545ead | ||
|
|
15ff4386e5 | ||
|
|
7a4aa3bf87 | ||
|
|
c1ba6288c8 | ||
|
|
d0ba180729 | ||
|
|
2edc76414b | ||
|
|
452db5dc39 | ||
|
|
21c4c53feb | ||
|
|
6292789cac | ||
|
|
d445bbc02d | ||
|
|
8627f58cbc | ||
|
|
66ab0059bb | ||
|
|
3f06932277 | ||
|
|
59cf1b435e | ||
|
|
c9a5b0e66e | ||
|
|
bcd5e1da98 | ||
|
|
f74ad050b4 | ||
|
|
b6f43c9b23 | ||
|
|
298c64a119 | ||
|
|
8b80679b3a | ||
|
|
82a2bc561a | ||
|
|
e019542d3f | ||
|
|
9356247ef0 | ||
|
|
4db77c18e3 | ||
|
|
0d0b248988 |
No files matched your search
@@ -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,
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="uniqueAreas.length > 1"
|
v-if="uniqueAreas.length > 1"
|
||||||
|
dir="auto"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-1 text-sm ml-1 -mt-3">
|
<div class="flex space-x-1 text-sm ml-1 -mt-3">
|
||||||
@@ -40,7 +41,7 @@ export default {
|
|||||||
uniqueAreas () {
|
uniqueAreas () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
|
|
||||||
this.field.areas.forEach((area) => {
|
this.field.areas?.forEach((area) => {
|
||||||
areas[area.attachment_uuid + area.page] ||= area
|
areas[area.attachment_uuid + area.page] ||= area
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -204,7 +204,15 @@
|
|||||||
:checked="!!values[field.uuid]"
|
:checked="!!values[field.uuid]"
|
||||||
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span
|
||||||
|
v-if="field.title"
|
||||||
|
class="text-xl"
|
||||||
|
v-html="field.title"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-xl"
|
||||||
|
>
|
||||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -324,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"
|
||||||
@@ -488,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,
|
||||||
@@ -737,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]?.length
|
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)
|
||||||
@@ -776,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)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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',
|
option: 'Option',
|
||||||
appears_on: 'Appears on',
|
appears_on: 'Appears on',
|
||||||
@@ -48,7 +48,7 @@ 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',
|
appears_on: 'Aparece en',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
@@ -95,7 +95,7 @@ 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',
|
appears_on: 'Compare su',
|
||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
@@ -142,7 +142,7 @@ 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',
|
appears_on: 'Erscheint auf',
|
||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
@@ -189,7 +189,7 @@ 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',
|
appears_on: 'Apparaît sur',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
@@ -236,7 +236,7 @@ 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',
|
appears_on: 'Pojawia się na',
|
||||||
page: 'Strona',
|
page: 'Strona',
|
||||||
@@ -283,7 +283,7 @@ const pl = {
|
|||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
submit_form: 'Надіслати Форму',
|
submit_form: 'Надіслати Форму',
|
||||||
type_here: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
appears_on: "З'являється на",
|
appears_on: "З'являється на",
|
||||||
page: 'Сторінка',
|
page: 'Сторінка',
|
||||||
@@ -330,7 +330,7 @@ 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',
|
appears_on: 'Zobrazuje se na',
|
||||||
page: 'Stránka',
|
page: 'Stránka',
|
||||||
@@ -377,7 +377,7 @@ 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',
|
appears_on: 'Aparece em',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
@@ -422,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]
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,21 @@
|
|||||||
v-if="field.name"
|
v-if="field.name"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ field.name }}
|
: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" />
|
<AppearsOn :field="field" />
|
||||||
<div class="items-center flex">
|
<div class="items-center flex">
|
||||||
<input
|
<input
|
||||||
@@ -24,7 +31,7 @@
|
|||||||
: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')"
|
||||||
@@ -34,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"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</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"
|
||||||
@@ -179,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>
|
||||||
@@ -199,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)"
|
||||||
/>
|
/>
|
||||||
@@ -244,6 +246,7 @@
|
|||||||
:with-help="withHelp"
|
: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"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
@@ -299,6 +302,7 @@ export default {
|
|||||||
save: this.save,
|
save: this.save,
|
||||||
t: this.t,
|
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,
|
||||||
@@ -345,6 +349,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
fieldTypes: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -372,6 +381,13 @@ export default {
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSave: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -717,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)
|
||||||
@@ -967,6 +987,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(() => {
|
||||||
|
if (this.onSave) {
|
||||||
|
this.onSave(this.template)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', 't'],
|
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -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."
|
||||||
@@ -190,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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -115,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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,16 +22,21 @@ 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
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
|
||||||
bcc = submitter.submission.template.account.account_configs
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
submission.template.account.account_configs
|
||||||
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
||||||
|
|
||||||
SubmitterMailer.completed_email(submitter, user, to: bcc).deliver_later! if bcc
|
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
||||||
|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
to = build_to_addresses(submitter)
|
to = build_to_addresses(submitter)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -65,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:)
|
||||||
@@ -83,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
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|
||||||
|
|||||||
@@ -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' %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body dir="auto">
|
<body dir="<%= yield&.match?(TextUtils::RTL_REGEXP) ? 'rtl' : 'auto' %>">
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<%= render partial: 'shared/mailer_attribution' %>
|
<%= render partial: 'shared/mailer_attribution' %>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<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', dir: 'auto' %>
|
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
@@ -24,6 +24,12 @@
|
|||||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
<%= 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' %>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<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">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<%= ff.label :subject, class: 'label' %>
|
<%= ff.label :subject, class: 'label' %>
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
<%= 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' %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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="["js_1","react_1","vue_1"]" 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="<%= Docuseal::CDN_URL %>/js/form.js"></script>
|
||||||
|
|
||||||
|
<docuseal-form data-src="<%= start_form_url(slug: template.slug) %>"></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"><script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">></script></span>
|
||||||
|
|
||||||
|
<span style="color: #f4bf75"><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">></span><span style="color: #f4bf75"></docuseal-form></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 "react"
|
||||||
|
import { DocusealForm } from '@docuseal/react'
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<DocusealForm
|
||||||
|
src="<%= start_form_url(slug: template.slug) %>"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<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"><</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">/></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="'<%= start_form_url(slug: template.slug) %>'"
|
||||||
|
/>
|
||||||
|
</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"><template></span>
|
||||||
|
<span style="color: #f4bf75"><DocusealForm</span>
|
||||||
|
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
|
||||||
|
<span style="color: #f4bf75">/></span>
|
||||||
|
<span style="color: #f4bf75"></template></span>
|
||||||
|
|
||||||
|
<span style="color: #f4bf75"><</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">></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"></</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">></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>
|
||||||
@@ -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 %>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<% 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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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 %>
|
||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -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
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
+45
-13
@@ -1,39 +1,71 @@
|
|||||||
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ module Submissions
|
|||||||
'GBP' => '£'
|
'GBP' => '£'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
RTL_REGEXP = Submissions::GenerateResultAttachments::RTL_REGEXP
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -149,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: "#{maybe_rtl_reverse(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]
|
||||||
)
|
)
|
||||||
@@ -187,7 +187,7 @@ module Submissions
|
|||||||
composer.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: maybe_rtl_reverse(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
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ module Submissions
|
|||||||
|
|
||||||
value = value.join(', ') if value.is_a?(Array)
|
value = value.join(', ') if value.is_a?(Array)
|
||||||
|
|
||||||
composer.formatted_text_box([{ text: maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
||||||
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
padding: [0, 0, 10, 0])
|
padding: [0, 0, 10, 0])
|
||||||
end
|
end
|
||||||
@@ -298,14 +298,6 @@ module Submissions
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_rtl_reverse(text)
|
|
||||||
if text.match?(RTL_REGEXP)
|
|
||||||
text.reverse
|
|
||||||
else
|
|
||||||
text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_logo(column, _submission = nil)
|
def add_logo(column, _submission = nil)
|
||||||
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ 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 = /\A[\p{Hebrew}\p{Arabic}].*[\p{Hebrew}\p{Arabic}]\z/
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
|
|
||||||
TEXT_LEFT_MARGIN = 1
|
TEXT_LEFT_MARGIN = 1
|
||||||
TEXT_TOP_MARGIN = 1
|
TEXT_TOP_MARGIN = 1
|
||||||
@@ -159,7 +159,7 @@ module Submissions
|
|||||||
when 'cells'
|
when 'cells'
|
||||||
cell_width = area['cell_w'] * width
|
cell_width = area['cell_w'] * width
|
||||||
|
|
||||||
maybe_rtl_reverse(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:)
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ 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
|
||||||
|
|
||||||
value = maybe_rtl_reverse(Array.wrap(value).join(', '))
|
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size:)
|
font_size:)
|
||||||
@@ -318,14 +318,6 @@ module Submissions
|
|||||||
pdf
|
pdf
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_rtl_reverse(text)
|
|
||||||
if text.match?(RTL_REGEXP)
|
|
||||||
text.reverse
|
|
||||||
else
|
|
||||||
text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def sign_reason(name)
|
def sign_reason(name)
|
||||||
format(SIGN_REASON, name:)
|
format(SIGN_REASON, name:)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module Templates
|
|||||||
PDF_CONTENT_TYPE = 'application/pdf'
|
PDF_CONTENT_TYPE = 'application/pdf'
|
||||||
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
|
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
|
||||||
InvalidFileType = Class.new(StandardError)
|
InvalidFileType = Class.new(StandardError)
|
||||||
|
PdfEncrypted = Class.new(StandardError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -19,6 +20,15 @@ module Templates
|
|||||||
document = template.documents.create!(blob:)
|
document = template.documents.create!(blob:)
|
||||||
|
|
||||||
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
|
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
|
||||||
|
data = maybe_decrypt_pdf_or_raise(document_data, params)
|
||||||
|
|
||||||
|
if data != document_data
|
||||||
|
blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: StringIO.new(new_data), filename: blob.filename
|
||||||
|
)
|
||||||
|
document_data = data
|
||||||
|
end
|
||||||
|
|
||||||
annotations =
|
annotations =
|
||||||
document_data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(document_data)
|
document_data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(document_data)
|
||||||
|
|
||||||
@@ -41,6 +51,8 @@ module Templates
|
|||||||
data = file.read
|
data = file.read
|
||||||
|
|
||||||
if file.content_type == PDF_CONTENT_TYPE
|
if file.content_type == PDF_CONTENT_TYPE
|
||||||
|
data = maybe_decrypt_pdf_or_raise(data, params)
|
||||||
|
|
||||||
annotations = data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(data)
|
annotations = data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(data)
|
||||||
metadata = { 'identified' => true, 'analyzed' => true,
|
metadata = { 'identified' => true, 'analyzed' => true,
|
||||||
'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)),
|
'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)),
|
||||||
@@ -56,6 +68,16 @@ module Templates
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_decrypt_pdf_or_raise(data, params)
|
||||||
|
if data.size < ANNOTATIONS_SIZE_LIMIT && PdfUtils.encrypted?(data)
|
||||||
|
PdfUtils.decrypt(data, params[:password])
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
rescue HexaPDF::EncryptionError
|
||||||
|
raise PdfEncrypted
|
||||||
|
end
|
||||||
|
|
||||||
def handle_file_types(_document_data, blob)
|
def handle_file_types(_document_data, blob)
|
||||||
raise InvalidFileType, blob.content_type
|
raise InvalidFileType, blob.content_type
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TextUtils
|
||||||
|
RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def maybe_rtl_reverse(text)
|
||||||
|
if text.match?(RTL_REGEXP)
|
||||||
|
TwitterCldr::Shared::Bidi
|
||||||
|
.from_string(ArabicLetterConnector.transform(text), direction: :RTL)
|
||||||
|
.reorder_visually!.to_s
|
||||||
|
else
|
||||||
|
text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user