mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ef1c309e0 | ||
|
|
a32dcc0dc5 | ||
|
|
16a2fde4b7 | ||
|
|
7856f7a134 | ||
|
|
c545924fe9 | ||
|
|
b1510bb88c | ||
|
|
31d2024ce5 | ||
|
|
23c49f8706 | ||
|
|
62eb0cda6b | ||
|
|
9f48c6e625 | ||
|
|
83ce401cf2 | ||
|
|
541b025ad0 | ||
|
|
5fd018df46 | ||
|
|
2822b0086a | ||
|
|
e27606f443 | ||
|
|
6c95cd5945 | ||
|
|
d723146060 | ||
|
|
3b03e61ae3 | ||
|
|
172513b96c | ||
|
|
68be42a48d | ||
|
|
0370e363bc | ||
|
|
ced88476cc | ||
|
|
c5d71505ef | ||
|
|
489f9859be | ||
|
|
a87ca65839 | ||
|
|
fa78d0976d | ||
|
|
4aaab2563b | ||
|
|
e073cf2c27 | ||
|
|
173d406b1f | ||
|
|
3bcbad6f04 | ||
|
|
e67854fe56 | ||
|
|
7510e0cb6b | ||
|
|
30c913b54f | ||
|
|
2cda9cd736 | ||
|
|
342d29f272 | ||
|
|
13f76c9944 | ||
|
|
ce4e55f387 | ||
|
|
712107ba5a | ||
|
|
1aa867893e | ||
|
|
4f6142f9c0 | ||
|
|
c303349666 | ||
|
|
e85df170f2 | ||
|
|
01333e5b2e | ||
|
|
514987138a | ||
|
|
0814c6d065 | ||
|
|
542a443fb8 | ||
|
|
f2c6bb1c3d | ||
|
|
b85c9cfdb7 | ||
|
|
e07b50c22d | ||
|
|
4dde9890e0 | ||
|
|
66d1ae2f64 | ||
|
|
d130835902 | ||
|
|
52f66368a3 | ||
|
|
89d0810625 | ||
|
|
06d3d1202b | ||
|
|
0698aad8e1 | ||
|
|
3575a25935 | ||
|
|
cdd034e72d | ||
|
|
26f7d5cf0d | ||
|
|
3c69430b94 | ||
|
|
c0162bccc8 | ||
|
|
1adb800f94 | ||
|
|
333a5d6b7b | ||
|
|
9aa68fe955 | ||
|
|
46af87d89a | ||
|
|
1066131143 | ||
|
|
a25022532e | ||
|
|
5162f9a9dc | ||
|
|
cc5694de43 | ||
|
|
f734ba280d | ||
|
|
500d0da563 | ||
|
|
6626a2b602 | ||
|
|
07e66a6e53 | ||
|
|
deef9c7d3b | ||
|
|
2ec8954dc8 | ||
|
|
c1af803e5b | ||
|
|
10abdbeba4 | ||
|
|
1dbde7a6d8 | ||
|
|
a27eb18670 | ||
|
|
44a49a4533 | ||
|
|
fc11078cae | ||
|
|
a42ae0b39e | ||
|
|
3163cdc761 | ||
|
|
5439c63ce7 | ||
|
|
b78287242b | ||
|
|
b869ae38b3 | ||
|
|
4a320e9ba6 | ||
|
|
0025928a35 | ||
|
|
c227a1988a | ||
|
|
3ade45f541 | ||
|
|
17da8bde80 | ||
|
|
59d9a59769 | ||
|
|
7210badde9 | ||
|
|
21659a73f3 | ||
|
|
c0e49ebeea | ||
|
|
2d24eac4a3 | ||
|
|
14c3c8181b | ||
|
|
036f6049a2 | ||
|
|
236e4cb35f | ||
|
|
ee77cb0982 | ||
|
|
d474ad05e5 | ||
|
|
dbf1fa37c5 | ||
|
|
7444bb8b68 | ||
|
|
f7132445a1 | ||
|
|
0127dd738e |
No files matched your search
@@ -21,7 +21,9 @@ gem 'jwt'
|
|||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
gem 'omniauth-github'
|
||||||
gem 'omniauth-google-oauth2'
|
gem 'omniauth-google-oauth2'
|
||||||
|
gem 'omniauth-microsoft-office365'
|
||||||
gem 'omniauth-rails_csrf_protection'
|
gem 'omniauth-rails_csrf_protection'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
|
|||||||
+10
-2
@@ -246,7 +246,7 @@ GEM
|
|||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
hexapdf (0.33.0)
|
hexapdf (0.34.1)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
@@ -328,18 +328,24 @@ GEM
|
|||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
|
omniauth-github (2.0.1)
|
||||||
|
omniauth (~> 2.0)
|
||||||
|
omniauth-oauth2 (~> 1.8)
|
||||||
omniauth-google-oauth2 (1.1.1)
|
omniauth-google-oauth2 (1.1.1)
|
||||||
jwt (>= 2.0)
|
jwt (>= 2.0)
|
||||||
oauth2 (~> 2.0.6)
|
oauth2 (~> 2.0.6)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-oauth2 (~> 1.8.0)
|
omniauth-oauth2 (~> 1.8.0)
|
||||||
|
omniauth-microsoft-office365 (0.0.8)
|
||||||
|
omniauth
|
||||||
|
omniauth-oauth2
|
||||||
omniauth-oauth2 (1.8.0)
|
omniauth-oauth2 (1.8.0)
|
||||||
oauth2 (>= 1.4, < 3)
|
oauth2 (>= 1.4, < 3)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
omniauth-rails_csrf_protection (1.0.1)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
openssl (3.1.0)
|
openssl (3.2.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.0.4)
|
pagy (6.0.4)
|
||||||
@@ -589,7 +595,9 @@ DEPENDENCIES
|
|||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
|
omniauth-github
|
||||||
omniauth-google-oauth2
|
omniauth-google-oauth2
|
||||||
|
omniauth-microsoft-office365
|
||||||
omniauth-rails_csrf_protection
|
omniauth-rails_csrf_protection
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
pdf-reader
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- [x] PDF form fields builder (WYSIWYG)
|
||||||
- [x] 10 field types available (Signature, Date, File, Checkbox etc.)
|
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- [x] Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- [x] Automated emails via SMTP
|
||||||
- [x] Files storage on AWS S3, Google Storage, or Azure
|
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- [x] Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- [x] PDF signature verification
|
||||||
- [x] Users management
|
- [x] Users management
|
||||||
@@ -76,7 +76,7 @@ Run the app under a custom domain over https using docker compose (make sure you
|
|||||||
HOST=your-domain-name.com docker-compose up
|
HOST=your-domain-name.com docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
## For Companies
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountConfigsController < ApplicationController
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config
|
||||||
|
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
|
AccountConfig::FORCE_MFA
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.required(:account_config).permit!.tap do |attrs|
|
||||||
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
|
|||||||
LOCALE_OPTIONS = {
|
LOCALE_OPTIONS = {
|
||||||
'en-US' => 'English (United States)',
|
'en-US' => 'English (United States)',
|
||||||
'en-GB' => 'English (United Kingdom)',
|
'en-GB' => 'English (United Kingdom)',
|
||||||
|
'fr-FR' => 'French (France)',
|
||||||
'es-ES' => 'Spanish (Spain)',
|
'es-ES' => 'Spanish (Spain)',
|
||||||
'pt-PT' => 'Portuguese (Portugal)',
|
'pt-PT' => 'Portuguese (Portugal)',
|
||||||
'de-DE' => 'German (Germany)'
|
'de-DE' => 'German (Germany)'
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module Api
|
|||||||
|
|
||||||
def paginate(relation)
|
def paginate(relation)
|
||||||
result = relation.order(id: :desc)
|
result = relation.order(id: :desc)
|
||||||
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
result = result.where('id < ?', params[:after]) if params[:after].present?
|
||||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
result = result.where('id > ?', params[:before]) if params[:before].present?
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
load_and_authorize_resource :template, only: :create
|
load_and_authorize_resource :template, only: :create
|
||||||
load_and_authorize_resource :submission, only: %i[show index]
|
load_and_authorize_resource :submission, only: %i[show index destroy]
|
||||||
|
|
||||||
before_action only: :create do
|
before_action only: :create do
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
@@ -13,7 +13,12 @@ module Api
|
|||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
if params[:template_folder].present?
|
||||||
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
||||||
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.as_json(serialize_params),
|
data: submissions.as_json(serialize_params),
|
||||||
@@ -49,35 +54,14 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
is_send_email = !params[:send_email].in?(['false', false])
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
|
params[:send_sms] = false unless params.key?(:send_sms)
|
||||||
|
|
||||||
submissions =
|
submissions = create_submissions(@template, params)
|
||||||
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
|
||||||
Submissions.create_from_emails(template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :api,
|
|
||||||
mark_as_sent: is_send_email,
|
|
||||||
emails:)
|
|
||||||
else
|
|
||||||
submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template)
|
|
||||||
|
|
||||||
Submissions.create_from_submitters(
|
Submissions.send_signature_requests(submissions)
|
||||||
template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :api,
|
|
||||||
mark_as_sent: is_send_email,
|
|
||||||
submitters_order: params[:submitters_order] || 'preserved',
|
|
||||||
submissions_attrs:
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions, send_email: is_send_email)
|
render json: submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
|
||||||
|
|
||||||
save_default_value_attachments!(attachments, submitters)
|
|
||||||
|
|
||||||
render json: submitters
|
|
||||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -90,9 +74,39 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def create_submissions(template, params)
|
||||||
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
|
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
||||||
|
Submissions.create_from_emails(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: is_send_email,
|
||||||
|
emails:,
|
||||||
|
params:)
|
||||||
|
else
|
||||||
|
submissions_attrs, attachments = normalize_submissions_params!(submissions_params, template)
|
||||||
|
|
||||||
|
submissions = Submissions.create_from_submitters(
|
||||||
|
template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: is_send_email,
|
||||||
|
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
||||||
|
submissions_attrs:,
|
||||||
|
params:
|
||||||
|
)
|
||||||
|
|
||||||
|
save_default_value_attachments!(attachments, submissions.flat_map(&:submitters))
|
||||||
|
|
||||||
|
submissions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
only: %i[id source submitters_order created_at updated_at],
|
only: %i[id source submitters_order created_at updated_at],
|
||||||
|
methods: %i[audit_log_url],
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
completed_at opened_at sent_at
|
completed_at opened_at sent_at
|
||||||
@@ -105,11 +119,19 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{
|
key = params.key?(:submission) ? :submission : :submissions
|
||||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key,
|
|
||||||
{ values: {}, readonly_fields: [],
|
params.permit(
|
||||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
key => [
|
||||||
}])
|
[:send_email, :send_sms, {
|
||||||
|
message: %i[subject body],
|
||||||
|
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role,
|
||||||
|
:completed, :phone, :application_key,
|
||||||
|
{ values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
).fetch(key, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
def normalize_submissions_params!(submissions_params, template)
|
||||||
@@ -126,7 +148,8 @@ module Api
|
|||||||
values, new_attachments =
|
values, new_attachments =
|
||||||
Submitters::NormalizeValues.call(template,
|
Submitters::NormalizeValues.call(template,
|
||||||
default_values,
|
default_values,
|
||||||
submitter[:role] || template.submitters[index]['name'])
|
submitter[:role] || template.submitters[index]['name'],
|
||||||
|
throw_errors: true)
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class TemplatesCloneController < ApiBaseController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, @template)
|
||||||
|
|
||||||
|
template = current_account.templates.new(source: :api)
|
||||||
|
|
||||||
|
template.application_key = params[:application_key]
|
||||||
|
template.name = params[:name] || "#{@template.name} (Clone)"
|
||||||
|
template.account = @template.account
|
||||||
|
template.author = current_user
|
||||||
|
template.assign_attributes(@template.slice(:folder_id, :fields, :schema, :submitters))
|
||||||
|
|
||||||
|
if params[:folder_name].present?
|
||||||
|
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
end
|
||||||
|
|
||||||
|
template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template:, original_template: @template)
|
||||||
|
|
||||||
|
render json: template.as_json(serialize_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialize_params
|
||||||
|
{
|
||||||
|
include: { author: { only: %i[id email first_name last_name] },
|
||||||
|
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,6 +8,8 @@ module Api
|
|||||||
templates = Templates.search(@templates, params[:q])
|
templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
templates = params[:archived] ? templates.archived : templates.active
|
templates = params[:archived] ? templates.archived : templates.active
|
||||||
|
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||||
|
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||||
|
|
||||||
@@ -51,11 +53,13 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name,
|
params.require(:template).permit(
|
||||||
schema: [%i[attachment_uuid name]],
|
:name,
|
||||||
submitters: [%i[name uuid]],
|
schema: [%i[attachment_uuid name]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
submitters: [%i[name uuid]],
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
|
{ options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -44,8 +44,9 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
|
||||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
render_to_string(partial: 'shared/button_title',
|
||||||
|
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class DashboardController < ApplicationController
|
|||||||
|
|
||||||
before_action :maybe_redirect_product_url
|
before_action :maybe_redirect_product_url
|
||||||
before_action :maybe_render_landing
|
before_action :maybe_render_landing
|
||||||
|
before_action :maybe_redirect_mfa_setup
|
||||||
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
@@ -62,6 +63,17 @@ class DashboardController < ApplicationController
|
|||||||
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_mfa_setup
|
||||||
|
return unless signed_in?
|
||||||
|
return if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
|
||||||
|
account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::FORCE_MFA)
|
||||||
|
|
||||||
|
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_render_landing
|
def maybe_render_landing
|
||||||
return if signed_in?
|
return if signed_in?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ErrorsController < ActionController::Base
|
||||||
|
ENTERPRISE_FEATURE_MESSAGE =
|
||||||
|
'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing'
|
||||||
|
|
||||||
|
ENTERPRISE_PATHS = [
|
||||||
|
'/templates/html',
|
||||||
|
'/api/templates/html',
|
||||||
|
'/templates/pdf',
|
||||||
|
'/api/templates/pdf'
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def show
|
||||||
|
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||||
|
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |f|
|
||||||
|
f.json do
|
||||||
|
render json: { status: error_status_code }, status: error_status_code
|
||||||
|
end
|
||||||
|
|
||||||
|
f.html { render error_status_code.to_s, status: error_status_code }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def error_status_code
|
||||||
|
@error_status_code ||=
|
||||||
|
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||||
|
request.env['action_dispatch.exception']).status_code
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController
|
|||||||
authorize!(:update, current_user)
|
authorize!(:update, current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
before_action :set_provision_url, only: %i[show new]
|
||||||
current_user.otp_secret ||= User.generate_otp_secret
|
|
||||||
|
|
||||||
current_user.save!
|
def show; end
|
||||||
|
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
def new; end
|
||||||
end
|
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
@error_message = 'Code is invalid'
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,4 +39,16 @@ class MfaSetupController < ApplicationController
|
|||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_provision_url
|
||||||
|
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
current_user.otp_secret ||= User.generate_otp_secret
|
||||||
|
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -12,7 +12,7 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @account_config.save
|
if @account_config.value.present? ? @account_config.save : @account_config.delete
|
||||||
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
else
|
else
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
render plain: io.tap(&:rewind).read
|
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_document_tempfile_path(attachment)
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
|||||||
@@ -14,21 +14,20 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
||||||
|
.order(id: :desc)
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||||
else
|
else
|
||||||
@submitter.assign_attributes(
|
if @template.submitters.to_a.size > 1 && @submitter.new_record?
|
||||||
uuid: @template.submitters.first['uuid'],
|
@error_message = 'Not found'
|
||||||
ip: request.remote_ip,
|
|
||||||
ua: request.user_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template,
|
return render :show
|
||||||
template_submitters: @template.submitters,
|
end
|
||||||
source: :link)
|
|
||||||
|
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -46,8 +45,23 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def assign_submission_attributes(submitter, template)
|
||||||
|
submitter.assign_attributes(
|
||||||
|
uuid: template.submitters.first['uuid'],
|
||||||
|
ip: request.remote_ip,
|
||||||
|
ua: request.user_agent,
|
||||||
|
preferences: { 'send_email' => true }
|
||||||
|
)
|
||||||
|
|
||||||
|
submitter.submission ||= Submission.new(template:,
|
||||||
|
template_submitters: template.submitters,
|
||||||
|
source: :link)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email, :phone).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,23 +34,30 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
|
|
||||||
|
if params[:is_custom_message] != '1'
|
||||||
|
params.delete(:subject)
|
||||||
|
params.delete(:body)
|
||||||
|
end
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
Submissions.create_from_emails(template: @template,
|
Submissions.create_from_emails(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
emails: params[:emails])
|
emails: params[:emails],
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions, params)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
|
def show
|
||||||
|
@submission = Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submission],
|
||||||
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||||
|
|
||||||
|
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: @submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
|
render 'submissions/show', layout: 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -38,6 +38,10 @@ class SubmitFormController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if submitter.completed_at?
|
||||||
|
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
|
event_type: 'send_email',
|
||||||
|
created_at: 24.hours.ago..Time.current)
|
||||||
|
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||||
|
alert: 'Email has been sent already.')
|
||||||
|
end
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if !Docuseal.multitenant? && params[:permanently].present?
|
if params[:permanently].present?
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
|
|
||||||
|
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
'Template has been removed.'
|
'Template has been removed.'
|
||||||
else
|
else
|
||||||
@template.update!(deleted_at: Time.current)
|
@template.update!(deleted_at: Time.current)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create_file_params_from_url
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
tempfile.write(conn.get(params[:url]).body)
|
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||||
tempfile.rewind
|
tempfile.rewind
|
||||||
|
|
||||||
file = ActionDispatch::Http::UploadedFile.new(
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TimestampServerController < ApplicationController
|
||||||
|
before_action :build_encrypted_config
|
||||||
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
|
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
|
||||||
|
|
||||||
|
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
|
||||||
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
end
|
||||||
|
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def test_timeserver_url(url)
|
||||||
|
pdf = HexaPDF::Document.new
|
||||||
|
pdf.pages.add
|
||||||
|
|
||||||
|
pkcs = Accounts.load_signing_pkcs(current_account)
|
||||||
|
|
||||||
|
pdf.sign(StringIO.new,
|
||||||
|
reason: 'Test',
|
||||||
|
certificate: pkcs.certificate,
|
||||||
|
key: pkcs.key,
|
||||||
|
certificate_chain: pkcs.ca_certs || [],
|
||||||
|
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
|
||||||
|
|
||||||
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:encrypted_config).permit(:value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,7 +7,9 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config.update!(encrypted_config_params)
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
|
||||||
|
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
|
this.appElem.classList.add('md:h-screen')
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.inputId) {
|
if (this.dataset.inputId) {
|
||||||
document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (this.dataset.params === 'true') {
|
||||||
|
const params = new URLSearchParams(this.input.value)
|
||||||
|
|
||||||
|
params.set('timezone', timezone)
|
||||||
|
|
||||||
|
this.input.value = params.toString()
|
||||||
|
} else {
|
||||||
|
this.input.value = timezone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get input () {
|
||||||
|
return document.getElementById(this.dataset.inputId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: true,
|
withConfetti: true,
|
||||||
|
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),
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute text-[1.5vw] lg:text-base"
|
class="flex absolute lg:text-base"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
|
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel"
|
v-if="isActive && withLabel && !area.option_uuid"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
{{ field.name || fieldNames[field.type] }}
|
||||||
@@ -87,6 +87,44 @@
|
|||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'radio' && area.option_uuid"
|
||||||
|
class="w-full p-[0.2vw] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="radio"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-radio"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value"
|
||||||
|
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'multiple' && area.option_uuid"
|
||||||
|
class="w-full p-[0.2vw] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="checkbox"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-checkbox"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex items-center"
|
||||||
@@ -102,6 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span v-if="Array.isArray(modelValue)">
|
||||||
@@ -173,6 +212,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
textOverflowChars: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
@@ -251,6 +295,37 @@ export default {
|
|||||||
height: h * 100 + '%'
|
height: h * 100 + '%'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue () {
|
||||||
|
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
||||||
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.field.type === 'text' && this.$refs.textContainer) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateMultipleSelectValue (value) {
|
||||||
|
if (this.modelValue?.includes(value)) {
|
||||||
|
const newValue = [...this.modelValue]
|
||||||
|
|
||||||
|
newValue.splice(newValue.indexOf(value), 1)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
} else {
|
||||||
|
const newValue = this.modelValue ? [...this.modelValue] : []
|
||||||
|
|
||||||
|
newValue.push(value)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="modelValue.length">
|
<div v-if="value.length">
|
||||||
<div
|
<div
|
||||||
v-for="(val, index) in modelValue"
|
v-for="(val, index) in value"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex mb-2"
|
class="flex mb-2"
|
||||||
>
|
>
|
||||||
@@ -26,10 +26,7 @@
|
|||||||
{{ attachmentsIndex[val].filename }}
|
{{ attachmentsIndex[val].filename }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button @click.prevent="removeAttachment(val)">
|
||||||
v-if="modelValue"
|
|
||||||
@click.prevent="removeAttachment(val)"
|
|
||||||
>
|
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
:width="18"
|
:width="18"
|
||||||
:heigh="19"
|
:heigh="19"
|
||||||
@@ -91,18 +88,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['attached', 'update:model-value'],
|
emits: ['attached', 'update:model-value'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', this.modelValue || [])
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAttachment (uuid) {
|
removeAttachment (uuid) {
|
||||||
this.modelValue.splice(this.modelValue.indexOf(uuid), 1)
|
this.value.splice(this.value.indexOf(uuid), 1)
|
||||||
|
|
||||||
this.$emit('update:model-value', this.modelValue)
|
this.$emit('update:model-value', this.value)
|
||||||
},
|
},
|
||||||
onUpload (attachments) {
|
onUpload (attachments) {
|
||||||
attachments.forEach((attachment) => {
|
attachments.forEach((attachment) => {
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$emit('update:model-value', [...this.modelValue, ...attachments.map(a => a.uuid)])
|
this.$emit('update:model-value', [...this.value, ...attachments.map(a => a.uuid)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,12 +99,12 @@
|
|||||||
{{ t('select_your_option') }}
|
{{ t('select_your_option') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="option in currentField.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
:selected="values[currentField.uuid] == option"
|
:selected="values[currentField.uuid] == option.value"
|
||||||
:value="option"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option }}
|
{{ option.value }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,24 +119,24 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="option in currentField.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="currentField.uuid + option"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="currentField.uuid + option"
|
:id="option.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="base-radio !h-7 !w-7"
|
class="base-radio !h-7 !w-7"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
:value="option"
|
:value="option.value"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option }}
|
{{ option.value }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,6 +223,7 @@
|
|||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousSignatureValue"
|
:previous-value="previousSignatureValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@@ -386,6 +387,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withTypedSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -513,9 +519,15 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
|
this.fields.forEach((field) => {
|
||||||
|
if (field.default_value && !field.readonly) {
|
||||||
|
this.values[field.uuid] = field.default_value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (this.goToLast) {
|
if (this.goToLast) {
|
||||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.values[f.uuid])))
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1
|
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||||
|
|
||||||
const indexesList = [this.stepFields.length - 1]
|
const indexesList = [this.stepFields.length - 1]
|
||||||
|
|
||||||
@@ -635,12 +647,20 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
|
const currentFieldUuid = this.currentField.uuid
|
||||||
|
|
||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
} else {
|
} else {
|
||||||
return fetch(this.baseUrl + this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form)
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -672,8 +692,6 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
|
||||||
|
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
this.isSecondWalkthrough = true
|
this.isSecondWalkthrough = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ const en = {
|
|||||||
please_fill_all_required_fields: 'Please fill all required fields',
|
please_fill_all_required_fields: 'Please fill all required fields',
|
||||||
set_today: 'Set Today',
|
set_today: 'Set Today',
|
||||||
toggle_multiline_text: 'Toggle Multiline Text',
|
toggle_multiline_text: 'Toggle Multiline Text',
|
||||||
|
draw_signature: 'Draw signature',
|
||||||
|
type_initial: 'Type initials',
|
||||||
|
draw: 'Draw',
|
||||||
|
type: 'Type',
|
||||||
|
type_text: 'Type text',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
email_has_been_sent: 'Email has been sent'
|
email_has_been_sent: 'Email has been sent'
|
||||||
}
|
}
|
||||||
@@ -71,6 +76,11 @@ const es = {
|
|||||||
set_today: 'Establecer Hoy',
|
set_today: 'Establecer Hoy',
|
||||||
date: 'Fecha',
|
date: 'Fecha',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilínea',
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
|
draw_signature: 'Dibujar firma',
|
||||||
|
type_initial: 'Escribir iniciales',
|
||||||
|
draw: 'Dibujar',
|
||||||
|
type: 'Escribir',
|
||||||
|
type_text: 'Escribir texto',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +118,11 @@ const it = {
|
|||||||
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
set_today: 'Imposta Oggi',
|
set_today: 'Imposta Oggi',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
draw_signature: 'Disegna firma',
|
||||||
|
type_initial: 'Inserisci iniziali',
|
||||||
|
draw: 'Disegna',
|
||||||
|
type: 'Inserisci',
|
||||||
|
type_text: 'Inserisci testo',
|
||||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
email_has_been_sent: "L'email è stata inviata"
|
||||||
}
|
}
|
||||||
@@ -146,6 +161,11 @@ const de = {
|
|||||||
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
set_today: 'Heute einstellen',
|
set_today: 'Heute einstellen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
draw_signature: 'Unterschrift zeichnen',
|
||||||
|
type_initial: 'Initialen eingeben',
|
||||||
|
draw: 'Zeichnen',
|
||||||
|
type: 'Eingeben',
|
||||||
|
type_text: 'Text eingeben',
|
||||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
||||||
}
|
}
|
||||||
@@ -184,6 +204,11 @@ const fr = {
|
|||||||
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
set_today: "Définir Aujourd'hui",
|
set_today: "Définir Aujourd'hui",
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
|
draw_signature: 'Dessiner une signature',
|
||||||
|
type_initial: 'Saisir les initiales',
|
||||||
|
draw: 'Dessiner',
|
||||||
|
type: 'Saisir',
|
||||||
|
type_text: 'Saisir du texte',
|
||||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
email_has_been_sent: "L'email a été envoyé"
|
||||||
}
|
}
|
||||||
@@ -222,6 +247,11 @@ const pl = {
|
|||||||
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
set_today: 'Ustaw Dziś',
|
set_today: 'Ustaw Dziś',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
draw_signature: 'Rysuj podpis',
|
||||||
|
type_initial: 'Wprowadź inicjały',
|
||||||
|
draw: 'Rysuj',
|
||||||
|
type: 'Wprowadź',
|
||||||
|
type_text: 'Wprowadź tekst',
|
||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
email_has_been_sent: 'E-mail został wysłany'
|
||||||
}
|
}
|
||||||
@@ -260,6 +290,11 @@ const uk = {
|
|||||||
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
set_today: 'Задати Сьогодні',
|
set_today: 'Задати Сьогодні',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
|
draw_signature: 'Намалюйте підпис',
|
||||||
|
type_initial: 'Введіть ініціали',
|
||||||
|
draw: 'Підпис',
|
||||||
|
type: 'Текст',
|
||||||
|
type_text: 'Введіть текст',
|
||||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений'
|
||||||
}
|
}
|
||||||
@@ -298,6 +333,11 @@ const cs = {
|
|||||||
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
set_today: 'Nastavit Dnes',
|
set_today: 'Nastavit Dnes',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
draw_signature: 'Nakreslit podpis',
|
||||||
|
type_initial: 'Zadat iniciály',
|
||||||
|
draw: 'Kreslit',
|
||||||
|
type: 'Zadat',
|
||||||
|
type_text: 'Zadat text',
|
||||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
email_has_been_sent: 'E-mail byl odeslán'
|
||||||
}
|
}
|
||||||
@@ -336,6 +376,11 @@ const pt = {
|
|||||||
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
set_today: 'Definir Hoje',
|
set_today: 'Definir Hoje',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
draw_signature: 'Desenhar assinatura',
|
||||||
|
type_initial: 'Inserir iniciais',
|
||||||
|
draw: 'Desenhar',
|
||||||
|
type: 'Inserir',
|
||||||
|
type_text: 'Inserir texto',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado'
|
email_has_been_sent: 'Email enviado'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,43 @@
|
|||||||
>{{ field.name || t('initials') }}</label>
|
>{{ field.name || t('initials') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isDrawInitials"
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('type_initials')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline font-medium btn-sm"
|
||||||
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconTextSize :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-outline font-medium btn-sm"
|
||||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -31,7 +51,7 @@
|
|||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -63,7 +83,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
@@ -81,13 +101,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
components: {
|
components: {
|
||||||
IconReload,
|
IconReload,
|
||||||
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,25 +7,25 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="option in field.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="field.uuid + option"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="field.uuid + option"
|
:id="option.uuid"
|
||||||
:ref="setInputRef"
|
:ref="setInputRef"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
:value="option"
|
:value="option.value"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:checked="(modelValue || []).includes(option)"
|
:checked="(modelValue || []).includes(option.value)"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option }}
|
{{ option.value }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,17 +6,37 @@
|
|||||||
>{{ field.name || t('signature') }}</label>
|
>{{ field.name || t('signature') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isTextSignature"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Type text"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconSignature :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="withTypedSignature"
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('type_text')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -24,7 +44,7 @@
|
|||||||
data-tip="Take photo"
|
data-tip="Take photo"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="btn btn-outline btn-sm btn-circle"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
@@ -33,12 +53,15 @@
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
@change="drawImage"
|
@change="drawImage"
|
||||||
>
|
>
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('upload') }}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -47,7 +70,7 @@
|
|||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -79,7 +102,8 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
style="padding: 1px; 0"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
@@ -95,7 +119,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
@@ -107,6 +131,7 @@ export default {
|
|||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
@@ -124,6 +149,11 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withTypedSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
v-if="!isTextArea"
|
v-if="!isTextArea"
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
v-model="text"
|
v-model="text"
|
||||||
|
:maxlength="cellsMaxLegth"
|
||||||
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:pattern="field.validation?.pattern"
|
:pattern="field.validation?.pattern"
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="!isTextArea"
|
v-if="!isTextArea && field.type !== 'cells'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('toggle_multiline_text')"
|
:data-tip="t('toggle_multiline_text')"
|
||||||
>
|
>
|
||||||
@@ -80,6 +81,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
cellsMaxLegth () {
|
||||||
|
if (this.field.type === 'cells') {
|
||||||
|
const area = this.field.areas?.[0]
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
return parseInt(area.w / area.cell_w) + 1
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="index === 0"
|
v-if="index === 0 && editable"
|
||||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||||
style="left: -4px"
|
style="left: -4px"
|
||||||
@mousedown.stop="startResizeCell"
|
@mousedown.stop="startResizeCell"
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:button-width="27"
|
:button-width="27"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:button-classes="'px-1'"
|
:button-classes="'px-1'"
|
||||||
:menu-classes="'bg-white rounded-t-none'"
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@@ -58,15 +60,15 @@
|
|||||||
<span
|
<span
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
ref="name"
|
ref="name"
|
||||||
contenteditable
|
:contenteditable="editable && !defaultField"
|
||||||
class="pr-1 cursor-text outline-none block"
|
class="pr-1 cursor-text outline-none block"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@focus="onNameFocus"
|
@focus="onNameFocus"
|
||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ field.name || defaultName }}</span>
|
>{{ optionIndexText }} {{ field.name || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isNameFocus && field.type !== 'checkbox'"
|
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
>Required</label>
|
>Required</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else-if="editable"
|
||||||
class="pr-1"
|
class="pr-1"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click.prevent="$emit('remove')"
|
@click.prevent="$emit('remove')"
|
||||||
@@ -93,18 +95,30 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="opacity-50 flex items-center justify-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
:class="bgColors[submitterIndex]"
|
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1 h-full"
|
class="flex justify-center items-center space-x-1 h-full"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="field?.default_value"
|
||||||
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="textContainer"
|
||||||
|
class="flex items-center px-0.5"
|
||||||
|
>
|
||||||
|
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="fieldIcons[field.type]"
|
:is="fieldIcons[field.type]"
|
||||||
|
v-else
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10"
|
class="max-h-10 opacity-50"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +127,7 @@
|
|||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type"
|
v-if="field?.type && editable"
|
||||||
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
@touchstart="startTouchResize"
|
@touchstart="startTouchResize"
|
||||||
@@ -126,6 +140,7 @@ import FieldSubmitter from './field_submitter'
|
|||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { IconX } from '@tabler/icons-vue'
|
import { IconX } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -145,6 +160,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -157,6 +182,7 @@ export default {
|
|||||||
isResize: false,
|
isResize: false,
|
||||||
isDragged: false,
|
isDragged: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
|
textOverflowChars: 0,
|
||||||
dragFrom: { x: 0, y: 0 }
|
dragFrom: { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -164,6 +190,13 @@ export default {
|
|||||||
defaultName: Field.computed.defaultName,
|
defaultName: Field.computed.defaultName,
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
optionIndexText () {
|
||||||
|
if (this.area.option_uuid && this.field.options) {
|
||||||
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
cells () {
|
cells () {
|
||||||
const cells = []
|
const cells = []
|
||||||
|
|
||||||
@@ -185,30 +218,30 @@ export default {
|
|||||||
},
|
},
|
||||||
borderColors () {
|
borderColors () {
|
||||||
return [
|
return [
|
||||||
'border-red-500',
|
'border-red-500/50',
|
||||||
'border-sky-500',
|
'border-sky-500/50',
|
||||||
'border-emerald-500',
|
'border-emerald-500/50',
|
||||||
'border-yellow-300',
|
'border-yellow-300/50',
|
||||||
'border-purple-600',
|
'border-purple-600/50',
|
||||||
'border-pink-500',
|
'border-pink-500/50',
|
||||||
'border-cyan-500',
|
'border-cyan-500/50',
|
||||||
'border-orange-500',
|
'border-orange-500/50',
|
||||||
'border-lime-500',
|
'border-lime-500/50',
|
||||||
'border-indigo-500'
|
'border-indigo-500/50'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
'bg-red-100',
|
'bg-red-100/50',
|
||||||
'bg-sky-100',
|
'bg-sky-100/50',
|
||||||
'bg-emerald-100',
|
'bg-emerald-100/50',
|
||||||
'bg-yellow-100',
|
'bg-yellow-100/50',
|
||||||
'bg-purple-100',
|
'bg-purple-100/50',
|
||||||
'bg-pink-100',
|
'bg-pink-100/50',
|
||||||
'bg-cyan-100',
|
'bg-cyan-100/50',
|
||||||
'bg-orange-100',
|
'bg-orange-100/50',
|
||||||
'bg-lime-100',
|
'bg-lime-100/50',
|
||||||
'bg-indigo-100'
|
'bg-indigo-100/50'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
@@ -225,6 +258,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'field.default_value' () {
|
||||||
|
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value.length)) {
|
||||||
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value)) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
@@ -262,12 +309,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||||
this.field.options ||= ['']
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
(this.field.areas || []).forEach((area) => {
|
(this.field.areas || []).forEach((area) => {
|
||||||
@@ -313,6 +362,10 @@ export default {
|
|||||||
startDrag (e) {
|
startDrag (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
class="mx-auto pl-3 md:pl-4 h-full"
|
class="mx-auto pl-3 md:pl-4 h-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="$slots.buttons || withTitle"
|
||||||
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
|
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<a
|
<a
|
||||||
v-if="withLogo"
|
v-if="withLogo"
|
||||||
href="/"
|
href="/"
|
||||||
@@ -15,8 +16,10 @@
|
|||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
|
v-if="withTitle"
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
:editable="editable"
|
||||||
|
class="text-xl md:text-3xl font-semibold focus:text-clip"
|
||||||
:icon-stroke-width="2.3"
|
:icon-stroke-width="2.3"
|
||||||
@update:model-value="updateName"
|
@update:model-value="updateName"
|
||||||
/>
|
/>
|
||||||
@@ -27,10 +30,25 @@
|
|||||||
name="buttons"
|
name="buttons"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<a
|
||||||
|
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
||||||
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
|
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||||
|
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
||||||
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
|
>
|
||||||
|
<IconWritingSign
|
||||||
|
width="20"
|
||||||
|
class="inline"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
Sign Yourself
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
:href="`/templates/${template.id}/submissions/new`"
|
:href="`/templates/${template.id}/submissions/new`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="btn btn-primary text-base"
|
class="white-button md:!px-6"
|
||||||
@click="maybeShowEmptyTemplateAlert"
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconUsersPlus
|
<IconUsersPlus
|
||||||
@@ -38,7 +56,7 @@
|
|||||||
class="inline"
|
class="inline"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Recipients
|
Send
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
@@ -63,7 +81,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex md:max-h-[calc(100vh-60px)]">
|
<div
|
||||||
|
class="flex"
|
||||||
|
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
@@ -76,6 +97,8 @@
|
|||||||
:item="item"
|
:item="item"
|
||||||
:document="sortedDocuments[index]"
|
:document="sortedDocuments[index]"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-replace-button="withUploadButton"
|
||||||
|
:editable="editable"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@@ -87,10 +110,10 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="sticky bottom-0 py-2"
|
class="sticky bottom-0 py-2"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:style="withStickySubmitters ? { backgroundColor } : {}"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length"
|
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -104,7 +127,7 @@
|
|||||||
class="pr-3.5 pl-0.5"
|
class="pr-3.5 pl-0.5"
|
||||||
>
|
>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
v-if="!sortedDocuments.length"
|
v-if="!sortedDocuments.length && withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -120,16 +143,20 @@
|
|||||||
:areas-index="fieldAreasIndex[document.uuid]"
|
:areas-index="fieldAreasIndex[document.uuid]"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragFieldType"
|
:is-drag="!!dragField"
|
||||||
|
:default-fields="defaultFields"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
|
:editable="editable"
|
||||||
|
:base-url="baseUrl"
|
||||||
@draw="onDraw"
|
@draw="onDraw"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
/>
|
/>
|
||||||
<DocumentControls
|
<DocumentControls
|
||||||
v-if="isBreakpointLg"
|
v-if="isBreakpointLg && editable"
|
||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||||
|
:with-replace-button="withUploadButton"
|
||||||
:document="document"
|
:document="document"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -142,10 +169,11 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
v-if="sortedDocuments.length && isBreakpointLg"
|
v-if="sortedDocuments.length && isBreakpointLg && editable"
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
|
v-if="withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
@@ -153,34 +181,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<MobileDrawField
|
||||||
v-if="drawField"
|
v-if="drawField && isBreakpointLg"
|
||||||
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
|
:draw-field="drawField"
|
||||||
>
|
:fields="template.fields"
|
||||||
<span
|
:submitters="template.submitters"
|
||||||
class="rounded bg-base-200 px-4 py-2 rounded-full inline-flex space-x-2 mx-auto items-center mb-4 z-20 md:hidden"
|
:selected-submitter="selectedSubmitter"
|
||||||
>
|
class="md:hidden"
|
||||||
<component
|
:editable="editable"
|
||||||
:is="fieldIcons[drawField.type]"
|
@cancel="[drawField = null, drawOption = null]"
|
||||||
:width="20"
|
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||||
:height="20"
|
/>
|
||||||
class="inline"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
Draw {{ fieldNames[drawField.type] }} Field
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="link block text-center"
|
|
||||||
@click.prevent="drawField = null"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<FieldType
|
<FieldType
|
||||||
v-if="sortedDocuments.length && !drawField"
|
v-if="sortedDocuments.length && !drawField && editable"
|
||||||
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
||||||
:model-value="''"
|
:model-value="''"
|
||||||
@update:model-value="startFieldDraw($event)"
|
@update:model-value="startFieldDraw($event)"
|
||||||
@@ -208,7 +221,8 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="drawField"
|
v-if="drawField"
|
||||||
class="sticky inset-0 bg-base-100 h-full"
|
class="sticky inset-0 h-full"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
||||||
<p>
|
<p>
|
||||||
@@ -217,7 +231,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="drawField = null"
|
@click="[drawField = null, drawOption = null]"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -230,11 +244,13 @@
|
|||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:default-fields="defaultFields"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
@set-draw="drawField = $event"
|
:editable="editable"
|
||||||
@set-drag="dragFieldType = $event"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
@set-drag="dragField = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@drag-end="dragFieldType = null"
|
@drag-end="dragField = null"
|
||||||
@scroll-to-area="scrollToArea"
|
@scroll-to-area="scrollToArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,13 +263,14 @@
|
|||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import Dropzone from './dropzone'
|
import Dropzone from './dropzone'
|
||||||
import Fields from './fields'
|
import Fields from './fields'
|
||||||
|
import MobileDrawField from './mobile_draw_field'
|
||||||
import Document from './document'
|
import Document from './document'
|
||||||
import Logo from './logo'
|
import Logo from './logo'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
|
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
@@ -263,9 +280,11 @@ export default {
|
|||||||
Upload,
|
Upload,
|
||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
|
MobileDrawField,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
FieldType,
|
FieldType,
|
||||||
IconX,
|
IconX,
|
||||||
|
IconWritingSign,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
@@ -300,6 +319,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -320,6 +354,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withUploadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -338,7 +382,8 @@ export default {
|
|||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
drawField: null,
|
drawField: null,
|
||||||
dragFieldType: null
|
drawOption: null,
|
||||||
|
dragField: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -370,6 +415,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
this.defaultSubmitters.forEach((name, index) => {
|
||||||
|
const submitter = (this.template.submitters[index] ||= {})
|
||||||
|
|
||||||
|
submitter.name = name
|
||||||
|
submitter.uuid ||= v4()
|
||||||
|
})
|
||||||
|
|
||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
@@ -406,10 +458,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = ['']
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawField = field
|
this.drawField = field
|
||||||
|
this.drawOption = null
|
||||||
},
|
},
|
||||||
undo () {
|
undo () {
|
||||||
if (this.undoStack.length > 1) {
|
if (this.undoStack.length > 1) {
|
||||||
@@ -459,10 +512,11 @@ export default {
|
|||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
||||||
this.removeArea(this.selectedAreaRef.value)
|
this.removeArea(this.selectedAreaRef.value)
|
||||||
|
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
@@ -505,6 +559,16 @@ export default {
|
|||||||
},
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
|
if (this.drawOption) {
|
||||||
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
|
|
||||||
|
if (areaWithoutOption && !this.drawField.areas.find((a) => a.option_uuid === this.drawField.options[0].uuid)) {
|
||||||
|
areaWithoutOption.option_uuid = this.drawField.options[0].uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
area.option_uuid = this.drawOption.uuid
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
this.drawField.areas.push(area)
|
||||||
|
|
||||||
@@ -513,6 +577,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
|
|
||||||
@@ -562,14 +627,14 @@ export default {
|
|||||||
onDropfield (area) {
|
onDropfield (area) {
|
||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
type: this.dragFieldType,
|
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
required: this.dragFieldType !== 'checkbox'
|
required: this.dragField.type !== 'checkbox',
|
||||||
|
...this.dragField
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
||||||
field.options = ['']
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
@@ -583,27 +648,27 @@ export default {
|
|||||||
|
|
||||||
let baseArea
|
let baseArea
|
||||||
|
|
||||||
if (this.selectedField?.type === this.dragFieldType) {
|
if (this.selectedField?.type === field.type) {
|
||||||
baseArea = this.selectedAreaRef.value
|
baseArea = this.selectedAreaRef.value
|
||||||
} else if (previousField?.areas?.length) {
|
} else if (previousField?.areas?.length) {
|
||||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
} else {
|
} else {
|
||||||
if (['checkbox'].includes(this.dragFieldType)) {
|
if (['checkbox'].includes(field.type)) {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 30 / area.maskW,
|
w: area.maskW / 30 / area.maskW,
|
||||||
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (this.dragFieldType === 'image') {
|
} else if (field.type === 'image') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (this.dragFieldType === 'signature') {
|
} else if (field.type === 'signature') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
}
|
}
|
||||||
} else if (this.dragFieldType === 'initials') {
|
} else if (field.type === 'initials') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 10 / area.maskW,
|
w: area.maskW / 10 / area.maskW,
|
||||||
h: area.maskW / 35 / area.maskW
|
h: area.maskW / 35 / area.maskW
|
||||||
@@ -620,7 +685,7 @@ export default {
|
|||||||
fieldArea.h = baseArea.h
|
fieldArea.h = baseArea.h
|
||||||
fieldArea.y = fieldArea.y - baseArea.h / 2
|
fieldArea.y = fieldArea.y - baseArea.h / 2
|
||||||
|
|
||||||
if (this.dragFieldType === 'cells') {
|
if (field.type === 'cells') {
|
||||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
contenteditable
|
:contenteditable="editable"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
:class="iconInline ? 'inline' : 'block'"
|
:class="iconInline ? 'inline' : 'block'"
|
||||||
class="peer outline-none focus:block"
|
class="peer outline-none focus:block"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||||
@@ -69,6 +69,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
iconStrokeWidth: {
|
iconStrokeWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
/>
|
/>
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
|
v-if="withReplaceButton"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -67,6 +68,11 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withReplaceButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
:key="image.id"
|
:key="image.id"
|
||||||
:ref="setPageRefs"
|
:ref="setPageRefs"
|
||||||
:number="index"
|
:number="index"
|
||||||
|
:editable="editable"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
|
:default-fields="defaultFields"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:image="image"
|
:image="image"
|
||||||
@@ -34,15 +36,30 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
drawField: {
|
drawField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -56,6 +73,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
basePreviewUrl () {
|
||||||
|
if (this.baseUrl) {
|
||||||
|
return new URL(this.baseUrl).origin
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
numberOfPages () {
|
numberOfPages () {
|
||||||
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||||
},
|
},
|
||||||
@@ -66,7 +90,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || {
|
||||||
metadata: lazyloadMetadata,
|
metadata: lazyloadMetadata,
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: `/preview/${this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group"
|
class="border border-base-300 rounded rounded-tr-none relative group"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative group/contenteditable-container">
|
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||||
<div
|
<div
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<div class="flex items-center p-1 space-x-1">
|
<div class="flex items-center p-1 space-x-1">
|
||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:button-width="20"
|
:button-width="20"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
ref="name"
|
ref="name"
|
||||||
:model-value="field.name || defaultName"
|
:model-value="field.name || defaultName"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
:icon-stroke-width="1.6"
|
:icon-stroke-width="1.6"
|
||||||
@@ -31,34 +34,47 @@
|
|||||||
v-if="isNameFocus"
|
v-if="isNameFocus"
|
||||||
class="flex items-center relative"
|
class="flex items-center relative"
|
||||||
>
|
>
|
||||||
<input
|
<template v-if="field.type != 'phone'">
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
<input
|
||||||
v-model="field.required"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
type="checkbox"
|
v-model="field.required"
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
type="checkbox"
|
||||||
@mousedown.prevent
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
>
|
@mousedown.prevent
|
||||||
<label
|
>
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
<label
|
||||||
class="label text-xs"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
@click.prevent="field.required = !field.required"
|
class="label text-xs"
|
||||||
@mousedown.prevent
|
@click.prevent="field.required = !field.required"
|
||||||
>Required</label>
|
@mousedown.prevent
|
||||||
|
>Required</label>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="editable"
|
||||||
class="flex items-center space-x-1"
|
class="flex items-center space-x-1"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="field && !field.areas.length"
|
||||||
|
title="Draw"
|
||||||
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
@click="$emit('set-draw', { field })"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<span
|
<span
|
||||||
v-if="field.areas?.length"
|
v-if="!defaultField"
|
||||||
class="dropdown dropdown-end"
|
class="dropdown dropdown-end"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Areas"
|
title="Settings"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconSettings
|
||||||
:width="18"
|
:width="18"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
@@ -66,8 +82,60 @@
|
|||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'text'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.default_value"
|
||||||
|
type="text"
|
||||||
|
placeholder="Default value"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.default_value"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Default value
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'phone'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.required"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">Required</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type === 'text'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.readonly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">Read-only</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<hr class="pb-0.5 mt-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in field.areas || []"
|
v-for="(area, index) in field.areas || []"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -84,11 +152,11 @@
|
|||||||
Page {{ area.page + 1 }}
|
Page {{ area.page + 1 }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@click.prevent="$emit('set-draw', field)"
|
@click.prevent="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
<IconNewSection
|
<IconNewSection
|
||||||
:width="20"
|
:width="20"
|
||||||
@@ -113,18 +181,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-else
|
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||||
title="Areas"
|
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
|
||||||
@click="$emit('set-draw', field)"
|
|
||||||
>
|
|
||||||
<IconShape
|
|
||||||
:width="18"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="relative text-transparent group-hover:text-base-content"
|
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
@@ -133,48 +190,58 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
|
|
||||||
<button
|
|
||||||
title="Up"
|
|
||||||
class="relative"
|
|
||||||
style="font-size: 10px; margin-bottom: -2px"
|
|
||||||
@click="$emit('move-up')"
|
|
||||||
>
|
|
||||||
▲
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Down"
|
|
||||||
class="relative"
|
|
||||||
style="font-size: 10px; margin-top: -2px"
|
|
||||||
@click="$emit('move-down')"
|
|
||||||
>
|
|
||||||
▼
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.options"
|
v-if="field.options"
|
||||||
|
ref="options"
|
||||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
class="flex space-x-1.5 items-center"
|
class="flex space-x-1.5 items-center"
|
||||||
>
|
>
|
||||||
<span class="text-sm w-3.5">
|
<span class="text-sm w-3.5">
|
||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'multiple'].includes(field.type) && (index > 0 || field.areas.find((a) => a.option_uuid) || !field.areas.length) && !field.areas.find((a) => a.option_uuid === option.uuid)"
|
||||||
|
class="items-center flex w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="option.value"
|
||||||
|
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Draw"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.prevent="$emit('set-draw', { field, option })"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="field.options[index]"
|
v-else
|
||||||
|
v-model="option.value"
|
||||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-sm w-3.5"
|
class="text-sm w-3.5"
|
||||||
@click="[field.options.splice(index, 1), save()]"
|
tabindex="-1"
|
||||||
|
@click="removeOption(option)"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -182,7 +249,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="field.options"
|
v-if="field.options"
|
||||||
class="text-center text-sm w-full pb-1"
|
class="text-center text-sm w-full pb-1"
|
||||||
@click="[field.options.push(''), save()]"
|
@click="addOption"
|
||||||
>
|
>
|
||||||
+ Add option
|
+ Add option
|
||||||
</button>
|
</button>
|
||||||
@@ -194,26 +261,38 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy } from '@tabler/icons-vue'
|
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateField',
|
name: 'TemplateField',
|
||||||
components: {
|
components: {
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
|
IconSettings,
|
||||||
IconShape,
|
IconShape,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save'],
|
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isNameFocus: false
|
isNameFocus: false
|
||||||
@@ -259,19 +338,45 @@ export default {
|
|||||||
}, 1)
|
}, 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maybeFocusOnOptionArea (option) {
|
||||||
|
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
this.selectedAreaRef.value = area
|
||||||
|
}
|
||||||
|
},
|
||||||
scrollToFirstArea () {
|
scrollToFirstArea () {
|
||||||
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
||||||
},
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
},
|
},
|
||||||
|
addOption () {
|
||||||
|
this.field.options.push({ value: '', uuid: v4() })
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
|
inputs[inputs.length - 1]?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeOption (option) {
|
||||||
|
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||||
|
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
this.field.options ||= ['']
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
(this.field.areas || []).forEach((area) => {
|
(this.field.areas || []).forEach((area) => {
|
||||||
|
|||||||
@@ -1,5 +1,91 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dropdown">
|
<div v-if="mobileView">
|
||||||
|
<div class="flex space-x-2 items-end">
|
||||||
|
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span
|
||||||
|
class="w-3 h-3 flex-shrink-0 rounded-full"
|
||||||
|
:class="colors[submitters.indexOf(selectedSubmitter)]"
|
||||||
|
/>
|
||||||
|
<Contenteditable
|
||||||
|
v-model="selectedSubmitter.name"
|
||||||
|
class="cursor-text"
|
||||||
|
:icon-inline="true"
|
||||||
|
:editable="editable"
|
||||||
|
:select-on-edit-click="true"
|
||||||
|
:icon-width="18"
|
||||||
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown dropdown-top dropdown-end">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
|
||||||
|
>
|
||||||
|
<IconChevronUp
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="editable"
|
||||||
|
tabindex="0"
|
||||||
|
class="rounded-md min-w-max mb-2"
|
||||||
|
:class="menuClasses"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(submitter, index) in submitters"
|
||||||
|
:key="submitter.uuid"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-2 group justify-between items-center"
|
||||||
|
:class="{ 'active': submitter === selectedSubmitter }"
|
||||||
|
@click.prevent="selectSubmitter(submitter)"
|
||||||
|
>
|
||||||
|
<span class="py-1 flex items-center">
|
||||||
|
<span
|
||||||
|
class="rounded-full w-3 h-3 ml-1 mr-3"
|
||||||
|
:class="colors[index]"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ submitter.name }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="submitters.length > 1 && editable"
|
||||||
|
class="px-2"
|
||||||
|
@click.stop="remove(submitter)"
|
||||||
|
>
|
||||||
|
<IconTrashX :width="18" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-if="submitters.length < 10 && editable">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-2"
|
||||||
|
@click.prevent="addSubmitter"
|
||||||
|
>
|
||||||
|
<IconUserPlus
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="py-1">
|
||||||
|
Add {{ names[submitters.length] }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="dropdown"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
v-if="compact"
|
v-if="compact"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -14,7 +100,7 @@
|
|||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 w-full flex justify-between"
|
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
@@ -25,12 +111,13 @@
|
|||||||
v-model="selectedSubmitter.name"
|
v-model="selectedSubmitter.name"
|
||||||
class="cursor-text"
|
class="cursor-text"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
|
:editable="editable"
|
||||||
:select-on-edit-click="true"
|
:select-on-edit-click="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
|
||||||
<IconPlus
|
<IconPlus
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
@@ -38,6 +125,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="editable || !compact"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
@@ -62,7 +150,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="!compact && submitters.length > 1"
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
class="hidden group-hover:block px-2"
|
class="hidden group-hover:block px-2"
|
||||||
@click.stop="remove(submitter)"
|
@click.stop="remove(submitter)"
|
||||||
>
|
>
|
||||||
@@ -70,7 +158,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 10">
|
<li v-if="submitters.length < 10 && editable">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -81,7 +169,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
Add Submitter
|
Add {{ names[submitters.length] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -90,7 +178,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -100,18 +188,29 @@ export default {
|
|||||||
IconUserPlus,
|
IconUserPlus,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrashX
|
IconTrashX,
|
||||||
|
IconChevronUp
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
mobileView: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -140,16 +239,16 @@ export default {
|
|||||||
},
|
},
|
||||||
names () {
|
names () {
|
||||||
return [
|
return [
|
||||||
'First Submitter',
|
'First Party',
|
||||||
'Second Submitter',
|
'Second Party',
|
||||||
'Third Submitter',
|
'Third Party',
|
||||||
'Fourth Submitter',
|
'Fourth Party',
|
||||||
'Fifth Submitter',
|
'Fifth Party',
|
||||||
'Sixth Submitter',
|
'Sixth Party',
|
||||||
'Seventh Submitter',
|
'Seventh Party',
|
||||||
'Eighth Submitter',
|
'Eighth Party',
|
||||||
'Ninth Submitter',
|
'Ninth Party',
|
||||||
'Tenth Submitter'
|
'Tenth Party'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
selectedSubmitter () {
|
selectedSubmitter () {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</slot>
|
</slot>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="editable"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@@ -65,6 +66,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
buttonWidth: {
|
buttonWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -5,26 +5,67 @@
|
|||||||
class="w-full rounded-lg"
|
class="w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
|
:editable="editable"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1 mt-2">
|
<div
|
||||||
|
class="mb-1 mt-2"
|
||||||
|
@dragover.prevent="onFieldDragover"
|
||||||
|
@drop="save"
|
||||||
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
|
:editable="editable && !dragField"
|
||||||
|
:default-field="defaultFields.find((f) => f.name === field.name)"
|
||||||
|
:draggable="editable"
|
||||||
|
@dragstart="dragField = field"
|
||||||
|
@dragend="dragField = null"
|
||||||
@remove="removeField"
|
@remove="removeField"
|
||||||
@move-up="move(field, -1)"
|
|
||||||
@move-down="move(field, 1)"
|
|
||||||
@scroll-to="$emit('scroll-to-area', $event)"
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1 pb-2">
|
<div v-if="submitterDefaultFields.length">
|
||||||
|
<hr class="mb-2">
|
||||||
|
<template
|
||||||
|
v-for="field in submitterDefaultFields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
draggable="true"
|
||||||
|
class="border border-base-300 rounded rounded-tr-none relative group mb-2"
|
||||||
|
@dragstart="onDragstart({ type: 'text', ...field })"
|
||||||
|
@dragend="$emit('drag-end')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between relative cursor-grab">
|
||||||
|
<div class="flex items-center p-1 space-x-1">
|
||||||
|
<IconDrag />
|
||||||
|
<FieldType
|
||||||
|
:model-value="field.type || 'text'"
|
||||||
|
:editable="false"
|
||||||
|
:button-width="20"
|
||||||
|
/>
|
||||||
|
<span class="block pl-0.5">
|
||||||
|
{{ field.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="grid grid-cols-3 gap-1 pb-2"
|
||||||
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
@@ -32,37 +73,14 @@
|
|||||||
<button
|
<button
|
||||||
v-if="withPhone || type != 'phone'"
|
v-if="withPhone || type != 'phone'"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="flex items-center justify-center border border-dashed border-base-300 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 }"
|
||||||
@dragstart="onDragstart(type)"
|
@dragstart="onDragstart({ type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="addField(type)"
|
@click="addField(type)"
|
||||||
>
|
>
|
||||||
<div class="w-0 absolute left-0">
|
<div class="flex items-console group-hover:bg-base-200/50 transition-all cursor-grab h-full absolute left-0">
|
||||||
<svg
|
<IconDrag class=" my-auto" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="cursor-grab"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="none"
|
|
||||||
d="M0 0h24v24H0z"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col px-2 py-2">
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
<component :is="icon" />
|
<component :is="icon" />
|
||||||
@@ -100,7 +118,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4"
|
v-if="fields.length < 4 && editable"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
@@ -123,12 +141,15 @@ import { v4 } from 'uuid'
|
|||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock } from '@tabler/icons-vue'
|
import { IconLock } from '@tabler/icons-vue'
|
||||||
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
|
FieldType,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||||
@@ -137,6 +158,16 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -152,16 +183,44 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
dragField: null
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
|
},
|
||||||
|
submitterDefaultFields () {
|
||||||
|
return this.defaultFields.filter((f) => {
|
||||||
|
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDragstart (fieldType) {
|
onDragstart (field) {
|
||||||
this.$emit('set-drag', fieldType)
|
this.$emit('set-drag', field)
|
||||||
|
},
|
||||||
|
onFieldDragover (e) {
|
||||||
|
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid
|
||||||
|
|
||||||
|
if (this.dragField && targetFieldUuid && this.dragField.uuid !== targetFieldUuid) {
|
||||||
|
const field = this.fields.find((f) => f.uuid === targetFieldUuid)
|
||||||
|
|
||||||
|
const currentIndex = this.fields.indexOf(this.dragField)
|
||||||
|
const targetIndex = this.fields.indexOf(field)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
this.fields.splice(targetIndex + 1, 0, this.dragField)
|
||||||
|
this.fields.splice(currentIndex, 1)
|
||||||
|
} else {
|
||||||
|
this.fields.splice(targetIndex, 0, this.dragField)
|
||||||
|
this.fields.splice(currentIndex + 1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
@@ -178,26 +237,6 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
move (field, direction) {
|
|
||||||
const currentIndex = this.submitterFields.indexOf(field)
|
|
||||||
const fieldsIndex = this.fields.indexOf(field)
|
|
||||||
|
|
||||||
this.fields.splice(fieldsIndex, 1)
|
|
||||||
|
|
||||||
if (currentIndex + direction > this.submitterFields.length) {
|
|
||||||
const firstIndex = this.fields.indexOf(this.submitterFields[0])
|
|
||||||
|
|
||||||
this.fields.splice(firstIndex, 0, field)
|
|
||||||
} else if (currentIndex + direction < 0) {
|
|
||||||
const lastIndex = this.fields.indexOf(this.submitterFields[this.submitterFields.length - 1])
|
|
||||||
|
|
||||||
this.fields.splice(lastIndex + 1, 0, field)
|
|
||||||
} else {
|
|
||||||
this.fields.splice(fieldsIndex + direction, 0, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
removeField (field) {
|
removeField (field) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
@@ -214,11 +253,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = ['']
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fields.push(field)
|
this.fields.push(field)
|
||||||
|
|
||||||
|
if (['signature', 'initials', 'cells'].includes(type)) {
|
||||||
|
this.$emit('set-draw', { field })
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="none"
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DragIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
|
||||||
|
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[drawField.type]"
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
class="inline"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span> Draw {{ fieldNames[drawField.type] }} Field </span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link block text-center"
|
||||||
|
@click.prevent="$emit('cancel')"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<FieldSubmitter
|
||||||
|
:model-value="selectedSubmitter.uuid"
|
||||||
|
:submitters="submitters"
|
||||||
|
:editable="editable"
|
||||||
|
:mobile-view="true"
|
||||||
|
@new-submitter="save"
|
||||||
|
@remove="removeSubmitter"
|
||||||
|
@name-change="save"
|
||||||
|
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import FieldSubmitter from './field_submitter'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileDrawField',
|
||||||
|
components: {
|
||||||
|
Field,
|
||||||
|
FieldSubmitter
|
||||||
|
},
|
||||||
|
inject: ['save'],
|
||||||
|
props: {
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
submitters: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['change-submitter', 'cancel'],
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeSubmitter (submitter) {
|
||||||
|
[...this.fields].forEach((field) => {
|
||||||
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
|
this.removeField(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.submitters.splice(this.submitters.indexOf(submitter), 1)
|
||||||
|
|
||||||
|
if (this.selectedSubmitter === submitter) {
|
||||||
|
this.$emit('change-submitter', this.submitters[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeField (field) {
|
||||||
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:area="item.area"
|
:area="item.area"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
|
:editable="editable"
|
||||||
|
:default-field="defaultFields.find((f) => f.name === item.field.name)"
|
||||||
@start-resize="resizeDirection = $event"
|
@start-resize="resizeDirection = $event"
|
||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@start-drag="isMove = true"
|
@start-drag="isMove = true"
|
||||||
@@ -39,8 +41,8 @@
|
|||||||
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
@pointerdown="onStartDraw"
|
@pointerdown="onStartDraw"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@@ -68,6 +70,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -77,6 +84,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -141,6 +153,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.showMask = true
|
this.showMask = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
@@ -12,10 +12,14 @@
|
|||||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
||||||
@click="$emit('scroll-to', item)"
|
@click="$emit('scroll-to', item)"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between w-full">
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="flex justify-between w-full"
|
||||||
|
>
|
||||||
<div style="width: 26px" />
|
<div style="width: 26px" />
|
||||||
<div class="">
|
<div class="">
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
|
v-if="withReplaceButton"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
@@ -95,11 +99,21 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
},
|
},
|
||||||
|
withReplaceButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -10,29 +10,30 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||||
|
|
||||||
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless is_all_completed
|
return if Accounts.load_webhook_configs(submitter.account).blank?
|
||||||
return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at)
|
|
||||||
|
|
||||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|
||||||
enqueue_emails(submitter)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_emails(submitter)
|
def enqueue_completed_emails(submitter)
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = submitter.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
|
||||||
bcc = submitter.submission.template.account.account_configs
|
bcc = submitter.submission.template.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
||||||
|
|
||||||
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
|
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
to = submitter.submission.submitters.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
||||||
|
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
||||||
end
|
end
|
||||||
@@ -49,6 +50,6 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
|
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
|
||||||
|
|
||||||
Submitters.send_signature_requests([next_submitter], send_email: true)
|
Submitters.send_signature_requests([next_submitter])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(submitter, subject: params['subject'], body: params['body']).deliver_now!
|
SubmitterMailer.invitation_email(submitter).deliver_now!
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,26 @@
|
|||||||
class SubmitterMailer < ApplicationMailer
|
class SubmitterMailer < ApplicationMailer
|
||||||
MAX_ATTACHMENTS_SIZE = 10.megabytes
|
MAX_ATTACHMENTS_SIZE = 10.megabytes
|
||||||
|
|
||||||
def invitation_email(submitter, body: nil, subject: nil)
|
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
|
||||||
|
|
||||||
|
def invitation_email(submitter)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@body = body.presence
|
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
if submitter.preferences['email_message_uuid']
|
||||||
|
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
||||||
|
end
|
||||||
|
|
||||||
|
@body = @email_message&.body.presence
|
||||||
|
@subject = @email_message&.subject.presence
|
||||||
|
|
||||||
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config || subject.present?
|
if @email_config || @subject
|
||||||
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'You are invited to submit a form'
|
DEFAULT_INVITATION_SUBJECT
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.friendly_name,
|
mail(to: @submitter.friendly_name,
|
||||||
@@ -31,7 +39,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: 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)
|
||||||
|
|
||||||
@@ -58,7 +66,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
@documents = add_completed_email_attachments!(submitter)
|
@documents = add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config
|
||||||
@@ -86,7 +94,26 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
total_size = audit_trail_data.size
|
total_size = audit_trail_data.size
|
||||||
end
|
end
|
||||||
|
|
||||||
documents.each do |attachment|
|
total_size = add_attachments_with_size_limit(documents, total_size)
|
||||||
|
|
||||||
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
|
|
||||||
|
file_fields = submitter.submission.template_fields.select { |e| e['type'] == 'file' }
|
||||||
|
|
||||||
|
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
||||||
|
storage_attachments =
|
||||||
|
submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten)
|
||||||
|
|
||||||
|
add_attachments_with_size_limit(storage_attachments, total_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
documents
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_attachments_with_size_limit(storage_attachments, current_size)
|
||||||
|
total_size = current_size
|
||||||
|
|
||||||
|
storage_attachments.each do |attachment|
|
||||||
total_size += attachment.byte_size
|
total_size += attachment.byte_size
|
||||||
|
|
||||||
break if total_size >= MAX_ATTACHMENTS_SIZE
|
break if total_size >= MAX_ATTACHMENTS_SIZE
|
||||||
@@ -94,9 +121,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
attachments[attachment.filename.to_s] = attachment.download
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
end
|
end
|
||||||
|
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
total_size
|
||||||
|
|
||||||
documents
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_address_for_submitter(submitter)
|
def from_address_for_submitter(submitter)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
# fk_rails_... (user_id => users.id)
|
# fk_rails_... (user_id => users.id)
|
||||||
#
|
#
|
||||||
class AccessToken < ApplicationRecord
|
class AccessToken < ApplicationRecord
|
||||||
TOKEN_LENGTH = 22
|
TOKEN_LENGTH = 43
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :account_configs, dependent: :destroy
|
has_many :account_configs, dependent: :destroy
|
||||||
|
has_many :email_messages, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
has_many :template_folders, dependent: :destroy
|
has_many :template_folders, dependent: :destroy
|
||||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class AccountConfig < ApplicationRecord
|
|||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
|
FORCE_MFA = 'force_mfa'
|
||||||
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
SUBMITTER_REMAILERS = 'submitter_reminders'
|
SUBMITTER_REMAILERS = 'submitter_reminders'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||||
"Alternatively, you can download your copy using:\n\n" \
|
"Alternatively, you can download your copy using:\n\n" \
|
||||||
"{{documents.links}}\n\n" \
|
"{{documents.link}}\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
'{{account.name}}'
|
'{{account.name}}'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: email_messages
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# body :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# subject :text not null
|
||||||
|
# uuid :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# author_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_email_messages_on_account_id (account_id)
|
||||||
|
# index_email_messages_on_sha1 (sha1)
|
||||||
|
# index_email_messages_on_uuid (uuid)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
# fk_rails_... (author_id => users.id)
|
||||||
|
#
|
||||||
|
class EmailMessage < ApplicationRecord
|
||||||
|
belongs_to :author, class_name: 'User'
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def set_sha1
|
||||||
|
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
|||||||
FILES_STORAGE_KEY = 'active_storage',
|
FILES_STORAGE_KEY = 'active_storage',
|
||||||
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
||||||
ESIGN_CERTS_KEY = 'esign_certs',
|
ESIGN_CERTS_KEY = 'esign_certs',
|
||||||
|
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
|
||||||
APP_URL_KEY = 'app_url',
|
APP_URL_KEY = 'app_url',
|
||||||
WEBHOOK_URL_KEY = 'webhook_url'
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
].freeze
|
].freeze
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# deleted_at :datetime
|
||||||
|
# preferences :text not null
|
||||||
|
# slug :string not null
|
||||||
# source :text not null
|
# source :text not null
|
||||||
# submitters_order :string not null
|
# submitters_order :string not null
|
||||||
# template_fields :text
|
# template_fields :text
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||||
|
# index_submissions_on_slug (slug) UNIQUE
|
||||||
# index_submissions_on_template_id (template_id)
|
# index_submissions_on_template_id (template_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
@@ -34,13 +37,18 @@ class Submission < ApplicationRecord
|
|||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
has_many :submission_events, dependent: :destroy
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|
||||||
|
attribute :preferences, :string, default: -> { {} }
|
||||||
|
|
||||||
serialize :template_fields, JSON
|
serialize :template_fields, JSON
|
||||||
serialize :template_schema, JSON
|
serialize :template_schema, JSON
|
||||||
serialize :template_submitters, JSON
|
serialize :template_submitters, JSON
|
||||||
|
serialize :preferences, JSON
|
||||||
|
|
||||||
attribute :source, :string, default: 'link'
|
attribute :source, :string, default: 'link'
|
||||||
attribute :submitters_order, :string, default: 'random'
|
attribute :submitters_order, :string, default: 'random'
|
||||||
|
|
||||||
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
|
|
||||||
has_many :template_schema_documents,
|
has_many :template_schema_documents,
|
||||||
@@ -49,7 +57,7 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
|
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
|
||||||
scope :completed, -> { left_joins(:submitters).where.not(submitters: { completed_at: nil }).distinct }
|
scope :completed, -> { where.not(id: pending.select(:submission_id)) }
|
||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
@@ -62,4 +70,11 @@ class Submission < ApplicationRecord
|
|||||||
random: 'random',
|
random: 'random',
|
||||||
preserved: 'preserved'
|
preserved: 'preserved'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
|
def audit_trail_url
|
||||||
|
return if audit_trail.blank?
|
||||||
|
|
||||||
|
Rails.application.routes.url_helpers.rails_storage_proxy_url(audit_trail, **Docuseal.default_url_options)
|
||||||
|
end
|
||||||
|
alias audit_log_url audit_trail_url
|
||||||
end
|
end
|
||||||
@@ -39,6 +39,7 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
send_email: 'send_email',
|
send_email: 'send_email',
|
||||||
send_reminder_email: 'send_reminder_email',
|
send_reminder_email: 'send_reminder_email',
|
||||||
send_sms: 'send_sms',
|
send_sms: 'send_sms',
|
||||||
|
send_2fa_sms: 'send_2fa_sms',
|
||||||
open_email: 'open_email',
|
open_email: 'open_email',
|
||||||
click_email: 'click_email',
|
click_email: 'click_email',
|
||||||
click_sms: 'click_sms',
|
click_sms: 'click_sms',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
# name :string
|
# name :string
|
||||||
# opened_at :datetime
|
# opened_at :datetime
|
||||||
# phone :string
|
# phone :string
|
||||||
|
# preferences :text not null
|
||||||
# sent_at :datetime
|
# sent_at :datetime
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# ua :string
|
# ua :string
|
||||||
@@ -37,9 +38,11 @@ class Submitter < ApplicationRecord
|
|||||||
has_one :account, through: :template
|
has_one :account, through: :template
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
|
attribute :preferences, :string, default: -> { {} }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
serialize :values, JSON
|
serialize :values, JSON
|
||||||
|
serialize :preferences, JSON
|
||||||
|
|
||||||
has_many_attached :documents
|
has_many_attached :documents
|
||||||
has_many_attached :attachments
|
has_many_attached :attachments
|
||||||
@@ -61,7 +64,7 @@ class Submitter < ApplicationRecord
|
|||||||
|
|
||||||
def friendly_name
|
def friendly_name
|
||||||
if name.present? && email.present? && email.exclude?(',')
|
if name.present? && email.present? && email.exclude?(',')
|
||||||
"#{name} <#{email}>"
|
%("#{name.delete('"')}" <#{email}>)
|
||||||
else
|
else
|
||||||
email
|
email
|
||||||
end
|
end
|
||||||
|
|||||||
+15
-14
@@ -4,19 +4,20 @@
|
|||||||
#
|
#
|
||||||
# Table name: templates
|
# Table name: templates
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# application_key :string
|
||||||
# fields :text not null
|
# deleted_at :datetime
|
||||||
# name :string not null
|
# fields :text not null
|
||||||
# schema :text not null
|
# name :string not null
|
||||||
# slug :string not null
|
# schema :text not null
|
||||||
# source :text not null
|
# slug :string not null
|
||||||
# submitters :text not null
|
# source :text not null
|
||||||
# created_at :datetime not null
|
# submitters :text not null
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :bigint not null
|
# updated_at :datetime not null
|
||||||
# author_id :bigint not null
|
# account_id :bigint not null
|
||||||
# folder_id :bigint not null
|
# author_id :bigint not null
|
||||||
|
# folder_id :bigint not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
# fk_rails_... (folder_id => template_folders.id)
|
# fk_rails_... (folder_id => template_folders.id)
|
||||||
#
|
#
|
||||||
class Template < ApplicationRecord
|
class Template < ApplicationRecord
|
||||||
DEFAULT_SUBMITTER_NAME = 'First Submitter'
|
DEFAULT_SUBMITTER_NAME = 'First Party'
|
||||||
|
|
||||||
belongs_to :author, class_name: 'User'
|
belongs_to :author, class_name: 'User'
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|||||||
+7
-2
@@ -52,11 +52,16 @@ class User < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
|
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
has_many :user_configs, dependent: :destroy
|
has_many :user_configs, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
||||||
|
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
|
|
||||||
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
devise :registerable, :omniauthable, omniauth_providers: %i[google_oauth2 microsoft_office365 github]
|
||||||
|
end
|
||||||
|
|
||||||
attribute :role, :string, default: ADMIN_ROLE
|
attribute :role, :string, default: ADMIN_ROLE
|
||||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
@@ -94,7 +99,7 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
def friendly_name
|
def friendly_name
|
||||||
if full_name.present?
|
if full_name.present?
|
||||||
"#{full_name} <#{email}>"
|
%("#{full_name.delete('"')}" <#{email}>)
|
||||||
else
|
else
|
||||||
email
|
email
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,6 +36,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if can?(:manage, AccountConfig) %>
|
||||||
|
<div class="px-1 mt-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Preferences</h2>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORCE_MFA) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Force 2FA with Authenticator App
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TYPED_SIGNATURE) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Allow typed text signatures
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"submitters": [
|
"submitters": [
|
||||||
{
|
{
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>",
|
||||||
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
||||||
"values": {
|
"values": {
|
||||||
"Form Text Field Name": "Default Value"
|
"Form Text Field Name": "Default Value"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:create, ::Template) %>
|
<% if can?(:create, ::Template) %>
|
||||||
<%= render 'templates/upload_button' %>
|
<%= render 'templates/upload_button' %>
|
||||||
<%= link_to new_template_path, class: 'btn btn-primary text-base btn-md gap-2', data: { turbo_frame: :modal } do %>
|
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||||
<span class="hidden md:block">Create</span>
|
<span class="hidden md:block">Create</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :email, class: 'label' %>
|
<%= f.label :email, class: 'label' %>
|
||||||
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input' %>
|
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input', placeholder: 'Enter email to continue' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -16,7 +16,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<% if devise_mapping.omniauthable? %>
|
||||||
<%= button_to button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
<div class="space-y-4">
|
||||||
|
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
||||||
|
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
||||||
|
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
||||||
|
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
|
||||||
|
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
|
||||||
|
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
|
||||||
|
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<% if devise_mapping.omniauthable? %>
|
||||||
<%= button_to button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
<div class="space-y-4">
|
||||||
|
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
||||||
|
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
||||||
|
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
||||||
|
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
|
||||||
|
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
|
||||||
|
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
|
||||||
|
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -97,5 +97,31 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<% encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) %>
|
||||||
|
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
|
||||||
|
<div class="flex-grow max-w-xl">
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Timestamp Server</h2>
|
||||||
|
</div>
|
||||||
|
<%= form_for encrypted_config, url: timestamp_server_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :value, class: 'label' do %>
|
||||||
|
<span class="flex items-center space-x-1">
|
||||||
|
<span>
|
||||||
|
Timeserver URL
|
||||||
|
</span>
|
||||||
|
<span class="tooltip" data-tip="URL of the trusted timeserver to be used to generate timestamp signatures.">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<%= f.url_field :value, autocomplete: 'off', class: 'base-input', placeholder: 'URL (optional)' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" /><path d="M10 12l4 0" /></svg>
|
||||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
|
||||||
|
<path fill="#ff5722" d="M6 6H22V22H6z" transform="rotate(-180 14 14)"></path><path fill="#4caf50" d="M26 6H42V22H26z" transform="rotate(-180 34 14)"></path><path fill="#ffc107" d="M26 26H42V42H26z" transform="rotate(-180 34 34)"></path><path fill="#03a9f4" d="M6 26H22V42H6z" transform="rotate(-180 14 34)"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 463 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<%= form_for '', url: mfa_setup_path, data: { turbo_frame: :_top }, html: { id: 'mfa_form' } do |f| %>
|
||||||
|
<p class="text-center">
|
||||||
|
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control my-6 space-y-2">
|
||||||
|
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
|
||||||
|
<span>
|
||||||
|
<%= @error_message %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= f.button button_title(title: 'Save'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -1,19 +1,3 @@
|
|||||||
<%= render 'shared/turbo_modal', title: 'Setup 2FA' do %>
|
<%= render 'shared/turbo_modal', title: 'Setup 2FA' do %>
|
||||||
<%= form_for '', url: mfa_setup_path, data: { turbo_frame: :_top } do |f| %>
|
<%= render 'mfa_setup/form' %>
|
||||||
<p class="text-center">
|
|
||||||
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control my-6 space-y-2">
|
|
||||||
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
|
|
||||||
<span>
|
|
||||||
<%= @error_message %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-control mt-4">
|
|
||||||
<%= f.button button_title(title: 'Save'), class: 'base-button' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="max-w-lg mx-auto px-2">
|
||||||
|
<h1 class="text-4xl font-bold text-center mt-8">Setup 2FA</h1>
|
||||||
|
<%= render 'mfa_setup/form' %>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
if (!window.customElements.get('autosize-field')) {
|
||||||
|
window.customElements.define('autosize-field', class extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
|
||||||
|
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get field() {
|
||||||
|
return this.closest('field-value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
<%= icon %>
|
<%= icon %>
|
||||||
<% if title %>
|
<% if title %>
|
||||||
<span><%= title %></span>
|
<span class="<%= local_assigns[:title_class] %>"><%= title %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="disabled">
|
<span class="disabled">
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
<span><%= disabled_with %>...</span>
|
<span class="<%= local_assigns[:title_class] %>"><%= disabled_with %>...</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
||||||
<%= render 'shared/title' %>
|
<%= render 'shared/title' %>
|
||||||
</a>
|
</a>
|
||||||
<%= render 'shared/github' %>
|
<span class="hidden sm:inline">
|
||||||
|
<%= render 'shared/github' %>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? %>
|
<% if signed_in? %>
|
||||||
<div class="space-x-4 flex items-center">
|
<div class="space-x-4 flex items-center">
|
||||||
@@ -16,13 +18,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center justify-center space-x-4 mr-1">
|
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||||
<% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
|
<%= render 'shared/navbar_buttons' %>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
|
|
||||||
Console
|
|
||||||
<span class="badge badge-warning ml-1">New</span>
|
|
||||||
<% end %>
|
|
||||||
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -75,7 +71,8 @@
|
|||||||
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
|
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
|
||||||
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
|
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<span class="flex items-center justify-center space-x-1">
|
||||||
<span>Create free Account</span>
|
<span class="hidden sm:block">Create free Account</span>
|
||||||
|
<span class="md:hidden">Sign Up</span>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
|
||||||
|
Upgrade
|
||||||
|
<% end %>
|
||||||
|
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script defer data-domain="<%= ENV.fetch('PLAUSIBLE_DOMAIN', nil) %>" src="https://plausible.io/js/script.manual.js"></script>
|
|
||||||
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("turbo:load", function (e) {
|
|
||||||
if (e.detail.url.match(/sign_in|sign_up|password/)) {
|
|
||||||
plausible('pageview')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<div class="block w-full md:w-52 flex-none">
|
<div class="block w-full md:w-52 flex-none">
|
||||||
<menu-active>
|
<menu-active>
|
||||||
<ul class="menu px-0">
|
<ul class="menu px-0">
|
||||||
<li class="menu-title py-0"><span>Settings</span></li>
|
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">← Back</a></li>
|
||||||
|
<li class="menu-title py-0 !bg-transparent"><span class="!bg-transparent">Settings</span></li>
|
||||||
<li></li>
|
<li></li>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
@@ -70,16 +71,23 @@
|
|||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
Plans
|
Plans
|
||||||
|
<span class="badge badge-warning">New</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
Console
|
<% if Docuseal.multitenant? %> API <% else %> Console <% end %>
|
||||||
<span class="badge badge-warning">New</span>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<li>
|
||||||
|
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
|
Embedding
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</menu-active>
|
</menu-active>
|
||||||
|
|||||||
@@ -24,9 +24,11 @@
|
|||||||
<%= 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: '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' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<% if @template.submitters.to_a.size == 1 %>
|
||||||
<%= 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: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<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: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
<%= 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 class="form-control !mt-0">
|
||||||
<%= f.label :email, class: 'label' %>
|
<%= f.label :email, class: 'label' %>
|
||||||
<%= f.email_field :email, value: current_user&.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: 'Provide your email to start' %>
|
||||||
|
<%= @error_message %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid <%= 'md:grid-cols-2' if template.submitters.size > 1 %> gap-4">
|
<div class="grid <%= 'md:grid-cols-2' if template.submitters.size > 1 %> gap-4">
|
||||||
<% template.submitters.each do |item| %>
|
<% template.submitters.each_with_index do |item, index| %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<% if template.submitters.size > 1 %>
|
<% if template.submitters.size > 1 %>
|
||||||
<label class="label pt-0 pb-1 text-xs">
|
<label class="label pt-0 pb-1 text-xs">
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name" required>
|
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '' %>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Email (optional)">
|
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Email (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.email : '' %>">
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Phone (optional)">
|
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Phone (optional)">
|
||||||
@@ -34,10 +34,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<% if params[:selfsign].blank? %>
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<span>Add New</span>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
</a>
|
<span>Add New</span>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
|
|||||||
@@ -18,14 +18,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
<% template.submitters.each do |item| %>
|
<% template.submitters.each_with_index do |item, index| %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<label class="label pt-0 pb-1 text-xs">
|
<label class="label pt-0 pb-1 text-xs">
|
||||||
<span class="label-text"> <%= item['name'] %></span>
|
<span class="label-text"> <%= item['name'] %></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Email" required>
|
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Email', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.email : '' %>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -33,10 +33,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<% if params[:selfsign].blank? %>
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<span>Add New</span>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
</a>
|
<span>Add New</span>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'shared/logo', width: 40, height: 40 %>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid <%= template.submitters.size > 1 ? 'md:grid-cols-2 gap-4' : 'gap-1' %>">
|
<div class="grid <%= template.submitters.size > 1 ? 'md:grid-cols-2 gap-4' : 'gap-1' %>">
|
||||||
<% template.submitters.each do |item| %>
|
<% template.submitters.each_with_index do |item, index| %>
|
||||||
<submitter-item class="grid <%= template.submitters.size > 1 ? 'gap-4' : 'md:grid-cols-2 gap-1' %>">
|
<submitter-item class="grid <%= template.submitters.size > 1 ? 'gap-4' : 'md:grid-cols-2 gap-1' %>">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<% if template.submitters.size > 1 %>
|
<% if template.submitters.size > 1 %>
|
||||||
@@ -19,18 +19,18 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Phone" required>
|
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Phone', required: index.zero? %>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% if template.submitters.size > 1 %>
|
<% if template.submitters.size > 1 %>
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)">
|
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>">
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if template.submitters.size == 1 %>
|
<% if template.submitters.size == 1 %>
|
||||||
<div class="form-control flex">
|
<div class="form-control flex">
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)">
|
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>">
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -40,10 +40,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<% if params[:selfsign].blank? %>
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<span>Add New</span>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
</a>
|
<span>Add New</span>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: template.submissions.last&.submitters_order.in?(['preserved', nil]) %>
|
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: template.submissions.last&.submitters_order.in?(['preserved', nil]) %>
|
||||||
<span class="label">Preserve submitters order</span>
|
<span class="label">Preserve order</span>
|
||||||
<span class="tooltip" data-tip="When checked, notifications will be sent to the second submitter once the form is completed by the previous submitter. Uncheck this option to send notifications to all submitters simultaneously right away.">
|
<span class="tooltip" data-tip="When checked, notifications will be sent to the second party once the form is completed by the previous party. Uncheck this option to send notifications to all parties simultaneously right away.">
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<div class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||||
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' %>
|
<% elsif field['type'] == 'file' %>
|
||||||
|
<autosize-field></autosize-field>
|
||||||
<div class="px-0.5 flex flex-col justify-center">
|
<div class="px-0.5 flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
<a target="_blank" href="<%= attachments_index[val].url %>">
|
<a target="_blank" href="<%= attachments_index[val].url %>">
|
||||||
@@ -11,9 +12,16 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<div class="w-full p-[0.2vw] flex items-center justify-center">
|
<div class="w-full flex items-center justify-center">
|
||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
|
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
||||||
|
<% option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] } %>
|
||||||
|
<% if option && Array.wrap(value).include?(option['value']) %>
|
||||||
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% elsif field['type'] == 'cells' %>
|
<% elsif field['type'] == 'cells' %>
|
||||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||||
<div class="w-full flex items-center">
|
<div class="w-full flex items-center">
|
||||||
@@ -24,10 +32,12 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex items-center px-0.5">
|
||||||
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</field-value>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= render 'shared/turbo_modal', title: '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'], ['Detailed', 'detailed']] %>
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
<div style="max-width: 1600px" class="mx-auto pl-4">
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
||||||
<a href="<%= template_path(@submission.template) %>" class="flex space-x-3 py-1">
|
<a href="<%= signed_in? && @submission.template.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
||||||
<span><%= render 'shared/logo', width: 40, height: 40 %></span>
|
<span><%= render 'submissions/logo' %></span>
|
||||||
<span class="text-3xl font-semibold focus:text-clip"><%= @submission.template.name %></span>
|
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><%= @submission.template.name %></span>
|
||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<% if @submission.audit_trail.present? %>
|
<% if @submission.audit_trail.present? %>
|
||||||
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
|
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
|
||||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||||
Audit Log
|
<span class="hidden md:inline">
|
||||||
|
Audit Log
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
|
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
|
||||||
<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</span>
|
<span class="hidden md:inline">Download</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 class="hidden md:inline">Downloading</span>
|
||||||
</span>
|
</span>
|
||||||
</download-button>
|
</download-button>
|
||||||
<% elsif @submission.submitters.to_a.size == 1 %>
|
<% elsif @submission.submitters.to_a.size == 1 %>
|
||||||
@@ -53,7 +55,7 @@
|
|||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
@@ -61,7 +63,7 @@
|
|||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']] %>
|
<% value = values[field['uuid']] %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: current_account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.template.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,18 +111,18 @@
|
|||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||||
<span>
|
<span>
|
||||||
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) : 'Not completed yet' %>
|
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.template.account.timezone), format: :long, locale: @submission.template.account.locale) : 'Not completed yet' %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) %>
|
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<%= render 'send_sms_button', submitter: %>
|
<%= render 'send_sms_button', submitter: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if 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
|
Submit Form
|
||||||
@@ -160,7 +162,7 @@
|
|||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<%= svg_icon('check', class: 'w-6 h-6') %>
|
<%= svg_icon('check', class: 'w-6 h-6') %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<%= l(Date.parse(value), locale: current_account.locale, format: :long) %>
|
<%= l(Date.parse(value), locale: @submission.template.account.locale, format: :long) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= Array.wrap(value).join(', ') %>
|
<%= Array.wrap(value).join(', ') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -172,3 +174,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'scripts/autosize_field' %>
|
||||||
@@ -1 +1,3 @@
|
|||||||
<%= render 'docuseal_logo' %>
|
<div class="flex mt-4">
|
||||||
|
<%= render 'docuseal_logo' %>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
<% data_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 %>
|
||||||
<% completed_button_params = submitter.submission.template.account.account_configs.find_by(key: AccountConfig::FORM_COMPLETED_BUTTON_KEY)&.value || {} %>
|
<% configs = Submitters::FormConfigs.call(submitter) %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= completed_button_params.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-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>
|
<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-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<% if @submitter.submission.template.submitters.size == 1 %>
|
<% if @submitter.submission.template.submitters.size == 1 %>
|
||||||
<div class="divider">OR</div>
|
<div class="divider">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 }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= 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: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,8 @@
|
|||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
<div class="mt-4 flex">
|
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %>
|
||||||
<%= render 'banner' %>
|
<%= render 'banner' %>
|
||||||
</div>
|
|
||||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||||
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']] %>
|
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
||||||
@@ -42,3 +41,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'scripts/autosize_field' %>
|
||||||
@@ -3,14 +3,10 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
<p>Alternatively, you can download your copy using:</p>
|
<p>Alternatively, you can review and download your copy using:</p>
|
||||||
<% @documents.each do |document| %>
|
<p>
|
||||||
<ul>
|
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %>
|
||||||
<li>
|
</p>
|
||||||
<%= link_to document.filename.to_s, rails_blob_url(document) %>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<% end %>
|
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @current_account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -35,7 +35,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
<% if current_user.email == submitter.email %>
|
||||||
|
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
|
||||||
|
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white md:w-36 flex">
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="hidden md:inline">Sign now</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||||
<% if !submission.deleted_at? && can?(:destroy, submission) %>
|
<% if !submission.deleted_at? && can?(:destroy, submission) %>
|
||||||
@@ -85,7 +96,18 @@
|
|||||||
</form>
|
</form>
|
||||||
<% elsif !is_submission_completed %>
|
<% elsif !is_submission_completed %>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
|
<% if current_user.email == submitter.email %>
|
||||||
|
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
|
||||||
|
<button onclick="event.stopPropagation()" class="btn btn-xs btn-outline btn-neutral bg-white md:w-36">
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="hidden md:inline">Sign now</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,10 +52,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if (!template.deleted_at? || !Docuseal.multitenant?) && can?(:destroy, template) %>
|
<% if can?(:destroy, template) %>
|
||||||
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Remove' : 'Archive' %>">
|
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Delete' : 'Archive' %>">
|
||||||
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
<%= button_to template_path(template), data: { turbo_confirm: template.deleted_at? ? 'Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' : 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
<%= svg_icon(template.deleted_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %>
|
||||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<% if can?(:update, template) %>
|
<% if can?(:update, template) %>
|
||||||
<span class="pl-1 tooltip tooltip-right opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
|
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
|
||||||
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
|
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
|
||||||
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
|
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
|
||||||
</a>
|
</a>
|
||||||
@@ -23,13 +23,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex md:justify-between space-x-2 flex-none pt-1">
|
<div class="flex md:justify-between space-x-2 flex-none pt-1">
|
||||||
|
<% if !template.deleted_at? %>
|
||||||
|
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
|
||||||
|
<% end %>
|
||||||
<% if !template.deleted_at? && can?(:destroy, template) %>
|
<% if !template.deleted_at? && can?(:destroy, template) %>
|
||||||
<%= button_to button_title(title: 'Remove', disabled_with: 'Removing', icon: svg_icon('trash', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
|
<%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:create, template) %>
|
<% if can?(:create, template) %>
|
||||||
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %>
|
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %>
|
||||||
<%= svg_icon('copy', class: 'w-6 h-6') %>
|
<%= svg_icon('copy', class: 'w-6 h-6') %>
|
||||||
<span>Clone</span>
|
<span class="hidden md:inline">Clone</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !template.deleted_at? && can?(:update, template) %>
|
<% if !template.deleted_at? && can?(:update, template) %>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
<%= f.text_field :name, required: true, placeholder: 'Template Name', class: 'base-input' %>
|
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 mb-4 flex items-center justify-between">
|
<div class="mt-3 mb-4 flex items-center justify-between">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
|
||||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
</a>
|
</a>
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
<input id="folder_name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
<input id="folder_name" placeholder="Folder Name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5">
|
||||||
Change Folder
|
Change Folder
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user