mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
df561a6211 | ||
|
|
50d3404f3b | ||
|
|
607fc8d338 | ||
|
|
8c366dfc38 | ||
|
|
5ed08fa9ab | ||
|
|
ddb557ebef | ||
|
|
b48615df3f | ||
|
|
86c7b92675 | ||
|
|
041f362cc4 | ||
|
|
65f491325e | ||
|
|
57aef5f5e5 | ||
|
|
e704e6fa04 | ||
|
|
4c99bbb58a | ||
|
|
c0647eef2c | ||
|
|
c619b9be93 | ||
|
|
abab3f9688 | ||
|
|
7b15111e8f | ||
|
|
2c7f38c503 | ||
|
|
ed80df1372 | ||
|
|
3614d1796c |
No files matched your search
@@ -75,3 +75,6 @@ Rails/SkipsModelValidations:
|
||||
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
Capybara/ClickLinkOrButtonStyle:
|
||||
Enabled: false
|
||||
+2
-1
@@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation
|
||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
@@ -49,6 +49,7 @@ COPY LICENSE README.md Rakefile config.ru ./
|
||||
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||
|
||||
WORKDIR /data/docuseal
|
||||
|
||||
+15
-17
@@ -98,7 +98,6 @@ GEM
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
better_html (2.0.2)
|
||||
actionview (>= 6.0)
|
||||
@@ -247,7 +246,7 @@ GEM
|
||||
hashdiff (1.0.1)
|
||||
hashery (2.1.2)
|
||||
hashie (5.0.0)
|
||||
hexapdf (0.33.0)
|
||||
hexapdf (0.34.1)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
@@ -294,7 +293,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.19.0)
|
||||
minitest (5.20.0)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
@@ -340,12 +339,12 @@ GEM
|
||||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
openssl (3.1.0)
|
||||
openssl (3.2.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
pagy (6.0.4)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.3)
|
||||
parser (3.2.2.4)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-reader (2.11.0)
|
||||
@@ -418,7 +417,7 @@ GEM
|
||||
rake (13.0.6)
|
||||
redis-client (0.16.0)
|
||||
connection_pool
|
||||
regexp_parser (2.8.1)
|
||||
regexp_parser (2.8.2)
|
||||
reline (0.3.8)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
@@ -455,33 +454,32 @@ GEM
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-support (3.12.1)
|
||||
rubocop (1.56.1)
|
||||
base64 (~> 0.1.1)
|
||||
rubocop (1.57.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.2.3)
|
||||
parser (>= 3.2.2.4)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.0)
|
||||
rubocop-ast (1.30.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.18.0)
|
||||
rubocop-capybara (2.19.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.23.1)
|
||||
rubocop-factory_bot (2.24.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-performance (1.19.0)
|
||||
rubocop-performance (1.19.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.20.2)
|
||||
rubocop-rails (2.22.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.23.2)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-rspec (2.25.0)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
ruby-progressbar (1.13.0)
|
||||
@@ -537,7 +535,7 @@ GEM
|
||||
tzinfo-data (1.2023.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.4.2)
|
||||
unicode-display_width (2.5.0)
|
||||
uniform_notifier (1.16.0)
|
||||
version_gem (1.1.3)
|
||||
warden (1.2.9)
|
||||
|
||||
@@ -34,10 +34,10 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
|
||||
## Features
|
||||
- [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] 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] PDF signature verification
|
||||
- [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
|
||||
```
|
||||
|
||||
## For Companies
|
||||
## For Businesses
|
||||
### 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.
|
||||
|
||||
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
|
||||
LOCALE_OPTIONS = {
|
||||
'en-US' => 'English (United States)',
|
||||
'en-GB' => 'English (United Kingdom)',
|
||||
'fr-FR' => 'French (France)',
|
||||
'es-ES' => 'Spanish (Spain)',
|
||||
'pt-PT' => 'Portuguese (Portugal)',
|
||||
'de-DE' => 'German (Germany)'
|
||||
|
||||
@@ -25,7 +25,7 @@ module Api
|
||||
|
||||
def paginate(relation)
|
||||
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[:before]) if params[:before].present?
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
class SubmissionsController < ApiBaseController
|
||||
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
|
||||
authorize!(:create, Submission)
|
||||
@@ -13,7 +13,8 @@ module Api
|
||||
submissions = Submissions.search(@submissions, params[:q])
|
||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||
|
||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
render json: {
|
||||
data: submissions.as_json(serialize_params),
|
||||
@@ -49,35 +50,14 @@ module Api
|
||||
end
|
||||
|
||||
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 =
|
||||
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_submissions(@template, params)
|
||||
|
||||
Submissions.create_from_submitters(
|
||||
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)
|
||||
|
||||
Submissions.send_signature_requests(submissions, send_email: is_send_email)
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
save_default_value_attachments!(attachments, submitters)
|
||||
|
||||
render json: submitters
|
||||
render json: submissions.flat_map(&:submitters)
|
||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
@@ -90,9 +70,39 @@ module Api
|
||||
|
||||
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
|
||||
{
|
||||
only: %i[id source submitters_order created_at updated_at],
|
||||
methods: %i[audit_log_url],
|
||||
include: {
|
||||
submitters: { only: %i[id slug uuid name email phone
|
||||
completed_at opened_at sent_at
|
||||
@@ -105,11 +115,19 @@ module Api
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: [{
|
||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key,
|
||||
{ values: {}, readonly_fields: [],
|
||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||
}])
|
||||
key = params.key?(:submission) ? :submission : :submissions
|
||||
|
||||
params.permit(
|
||||
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
|
||||
|
||||
def normalize_submissions_params!(submissions_params, template)
|
||||
|
||||
@@ -8,6 +8,7 @@ module Api
|
||||
templates = Templates.search(@templates, params[:q])
|
||||
|
||||
templates = params[:archived] ? templates.archived : templates.active
|
||||
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||
|
||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||
|
||||
@@ -51,11 +52,13 @@ module Api
|
||||
end
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(:name,
|
||||
schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
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
|
||||
@@ -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)
|
||||
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
|
||||
@@ -12,7 +12,7 @@ class NotificationsSettingsController < ApplicationController
|
||||
def index; end
|
||||
|
||||
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'
|
||||
else
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||
|
||||
@@ -6,12 +6,6 @@ class PreviewDocumentPageController < ActionController::API
|
||||
FORMAT = Templates::ProcessDocument::FORMAT
|
||||
|
||||
def show
|
||||
if Docuseal.multitenant?
|
||||
Rollbar.warning('load page')
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
||||
|
||||
return head :not_found unless attachment
|
||||
@@ -29,7 +23,7 @@ class PreviewDocumentPageController < ActionController::API
|
||||
|
||||
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
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SsoSettingsController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, only: :index
|
||||
authorize_resource :encrypted_config, parent: false, except: :index
|
||||
|
||||
def index; end
|
||||
|
||||
private
|
||||
|
||||
def load_encrypted_config
|
||||
@encrypted_config =
|
||||
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
|
||||
end
|
||||
end
|
||||
@@ -23,7 +23,8 @@ class StartFormController < ApplicationController
|
||||
@submitter.assign_attributes(
|
||||
uuid: @template.submitters.first['uuid'],
|
||||
ip: request.remote_ip,
|
||||
ua: request.user_agent
|
||||
ua: request.user_agent,
|
||||
preferences: { 'send_email' => true }
|
||||
)
|
||||
|
||||
@submitter.submission ||= Submission.new(template: @template,
|
||||
@@ -47,7 +48,7 @@ class StartFormController < ApplicationController
|
||||
private
|
||||
|
||||
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])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,23 +34,30 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
authorize!(:create, Submission)
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
end
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
Submissions.create_from_emails(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
emails: params[:emails])
|
||||
emails: params[:emails],
|
||||
params:)
|
||||
else
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||
params:)
|
||||
end
|
||||
|
||||
Submissions.send_signature_requests(submissions, params)
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||
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
|
||||
@@ -4,6 +4,13 @@ class SubmittersSendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||
|
||||
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!
|
||||
|
||||
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
||||
|
||||
@@ -57,9 +57,18 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@template.update!(deleted_at: Time.current)
|
||||
notice =
|
||||
if !Docuseal.multitenant? && params[:permanently].present?
|
||||
@template.destroy!
|
||||
|
||||
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
||||
'Template has been removed.'
|
||||
else
|
||||
@template.update!(deleted_at: Time.current)
|
||||
|
||||
'Template has been archived.'
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: root_path, notice:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,20 +3,55 @@
|
||||
class TemplatesUploadsController < ApplicationController
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
layout 'plain'
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
url_params = create_file_params_from_url if params[:url].present?
|
||||
|
||||
@template.account = current_account
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
@template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
@template.save!
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
|
||||
@template.update!(schema:)
|
||||
|
||||
redirect_to edit_template_path(@template)
|
||||
rescue StandardError => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
redirect_to root_path, alert: 'Unable to upload file'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||
tempfile.rewind
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
tempfile:,
|
||||
filename: File.basename(
|
||||
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
||||
),
|
||||
type: Marcel::MimeType.for(tempfile)
|
||||
)
|
||||
|
||||
{ files: [file] }
|
||||
end
|
||||
|
||||
def conn
|
||||
Faraday.new do |faraday|
|
||||
faraday.response :follow_redirects
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@ class VerifyPdfSignatureController < ApplicationController
|
||||
cert_data = if Docuseal.multitenant?
|
||||
Docuseal::CERTS
|
||||
else
|
||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
end
|
||||
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||
|
||||
@@ -18,6 +18,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
|
||||
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||
import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -49,6 +50,7 @@ window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||
window.customElements.define('signature-form', SignatureForm)
|
||||
window.customElements.define('submit-form', SubmitForm)
|
||||
|
||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||
document.addEventListener('turbo:submit-end', async (event) => {
|
||||
@@ -77,6 +79,8 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
this.appElem.classList.add('md:h-screen')
|
||||
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
backgroundColor: '#faf7f5',
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelector('form').requestSubmit()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex absolute text-[1.5vw] lg:text-base"
|
||||
class="flex absolute lg:text-base"
|
||||
: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
|
||||
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"
|
||||
>
|
||||
<span
|
||||
@@ -21,7 +21,7 @@
|
||||
</span>
|
||||
</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"
|
||||
>
|
||||
{{ 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 }"
|
||||
/>
|
||||
</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
|
||||
v-else-if="field.type === 'cells'"
|
||||
class="w-full flex items-center"
|
||||
@@ -102,6 +140,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
ref="textContainer"
|
||||
class="flex items-center px-0.5"
|
||||
>
|
||||
<span v-if="Array.isArray(modelValue)">
|
||||
@@ -173,6 +212,11 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value'],
|
||||
data () {
|
||||
return {
|
||||
textOverflowChars: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames () {
|
||||
return {
|
||||
@@ -251,6 +295,37 @@ export default {
|
||||
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>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="modelValue.length">
|
||||
<div v-if="value.length">
|
||||
<div
|
||||
v-for="(val, index) in modelValue"
|
||||
v-for="(val, index) in value"
|
||||
:key="index"
|
||||
class="flex mb-2"
|
||||
>
|
||||
@@ -26,10 +26,7 @@
|
||||
{{ attachmentsIndex[val].filename }}
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
v-if="modelValue"
|
||||
@click.prevent="removeAttachment(val)"
|
||||
>
|
||||
<button @click.prevent="removeAttachment(val)">
|
||||
<IconTrashX
|
||||
:width="18"
|
||||
:heigh="19"
|
||||
@@ -91,18 +88,28 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['attached', 'update:model-value'],
|
||||
computed: {
|
||||
value: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', this.modelValue || [])
|
||||
},
|
||||
get () {
|
||||
return this.modelValue || []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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) {
|
||||
attachments.forEach((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)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center w-full mb-2">
|
||||
<label
|
||||
:for="field.uuid"
|
||||
class="label text-2xl"
|
||||
>{{ field.name || t('date') }}
|
||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<button
|
||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||
@click.prevent="setCurrentDate"
|
||||
>
|
||||
<IconCalendarCheck :width="16" />
|
||||
{{ t('set_today') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<input
|
||||
ref="input"
|
||||
v-model="value"
|
||||
class="base-input !text-2xl text-center w-full"
|
||||
:required="field.required"
|
||||
type="date"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'DateStep',
|
||||
components: {
|
||||
IconCalendarCheck
|
||||
},
|
||||
inject: ['t'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value', 'focus'],
|
||||
computed: {
|
||||
value: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
},
|
||||
get () {
|
||||
return this.modelValue
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCurrentDate () {
|
||||
const inputEl = this.$refs.input
|
||||
|
||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -65,34 +65,13 @@
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="currentField.type === 'date'">
|
||||
<div class="flex justify-between items-center w-full mb-2">
|
||||
<label
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl"
|
||||
>{{ currentField.name || t('date') }}
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<button
|
||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||
@click.prevent="setCurrentDate"
|
||||
>
|
||||
<IconCalendarCheck :width="16" />
|
||||
{{ t('set_today') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<input
|
||||
:id="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
class="base-input !text-2xl text-center w-full"
|
||||
:required="currentField.required"
|
||||
type="date"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<DateStep
|
||||
v-else-if="currentField.type === 'date'"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
/>
|
||||
<div v-else-if="currentField.type === 'select'">
|
||||
<label
|
||||
v-if="currentField.name"
|
||||
@@ -120,12 +99,12 @@
|
||||
{{ t('select_your_option') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(option, index) in currentField.options"
|
||||
:key="index"
|
||||
:selected="values[currentField.uuid] == option"
|
||||
:value="option"
|
||||
v-for="option in currentField.options"
|
||||
:key="option.uuid"
|
||||
:selected="values[currentField.uuid] == option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option }}
|
||||
{{ option.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -140,24 +119,24 @@
|
||||
<div class="flex w-full">
|
||||
<div class="space-y-3.5 mx-auto">
|
||||
<div
|
||||
v-for="(option, index) in currentField.options"
|
||||
:key="index"
|
||||
v-for="option in currentField.options"
|
||||
:key="option.uuid"
|
||||
>
|
||||
<label
|
||||
:for="currentField.uuid + option"
|
||||
:for="option.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<input
|
||||
:id="currentField.uuid + option"
|
||||
:id="option.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
type="radio"
|
||||
class="base-radio !h-7 !w-7"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
:value="option"
|
||||
:value="option.value"
|
||||
:required="currentField.required"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ option }}
|
||||
{{ option.value }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -242,6 +221,7 @@
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:previous-value="previousSignatureValue"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:submitter-slug="submitterSlug"
|
||||
@@ -255,6 +235,7 @@
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:previous-value="previousInitialsValue"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:submitter-slug="submitterSlug"
|
||||
@@ -350,8 +331,9 @@ import AttachmentStep from './attachment_step'
|
||||
import MultiSelectStep from './multi_select_step'
|
||||
import PhoneStep from './phone_step'
|
||||
import TextStep from './text_step'
|
||||
import DateStep from './date_step'
|
||||
import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2, IconCalendarCheck } from '@tabler/icons-vue'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import { t } from './i18n'
|
||||
|
||||
export default {
|
||||
@@ -364,10 +346,10 @@ export default {
|
||||
InitialsStep,
|
||||
MultiSelectStep,
|
||||
IconInnerShadowTop,
|
||||
DateStep,
|
||||
IconArrowsDiagonal,
|
||||
TextStep,
|
||||
PhoneStep,
|
||||
IconCalendarCheck,
|
||||
IconArrowsDiagonalMinimize2,
|
||||
FormCompleted
|
||||
},
|
||||
@@ -392,6 +374,13 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
onComplete: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default () {
|
||||
return () => {}
|
||||
}
|
||||
},
|
||||
withConfetti: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -471,6 +460,16 @@ export default {
|
||||
submitterSlug () {
|
||||
return this.submitter.slug
|
||||
},
|
||||
previousSignatureValue () {
|
||||
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
||||
|
||||
return this.values[signatureField?.uuid]
|
||||
},
|
||||
previousInitialsValue () {
|
||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
|
||||
return this.values[initialsField?.uuid]
|
||||
},
|
||||
isAnonymousChecboxes () {
|
||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||
},
|
||||
@@ -514,9 +513,15 @@ export default {
|
||||
mounted () {
|
||||
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) {
|
||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.values[f.uuid])))
|
||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1
|
||||
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.submittedValues[f.uuid]))) + 1
|
||||
|
||||
const indexesList = [this.stepFields.length - 1]
|
||||
|
||||
@@ -635,20 +640,21 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
setCurrentDate () {
|
||||
const inputEl = document.getElementById(this.currentField.uuid)
|
||||
|
||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
},
|
||||
saveStep (formData) {
|
||||
const currentFieldUuid = this.currentField.uuid
|
||||
|
||||
if (this.isCompleted) {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
|
||||
}
|
||||
|
||||
return response
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -680,8 +686,6 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
||||
|
||||
if (isLastStep) {
|
||||
this.isSecondWalkthrough = true
|
||||
}
|
||||
@@ -696,6 +700,12 @@ export default {
|
||||
}
|
||||
} else {
|
||||
this.isCompleted = true
|
||||
|
||||
const respData = await response.text()
|
||||
|
||||
if (respData) {
|
||||
this.onComplete(JSON.parse(respData))
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error submitting form:', error)
|
||||
|
||||
@@ -32,6 +32,11 @@ const en = {
|
||||
please_fill_all_required_fields: 'Please fill all required fields',
|
||||
set_today: 'Set Today',
|
||||
toggle_multiline_text: 'Toggle Multiline Text',
|
||||
draw_signature: 'Draw signature',
|
||||
type_initial: 'Type initials',
|
||||
draw: 'Draw',
|
||||
type: 'Type',
|
||||
type_text: 'Type text',
|
||||
date: 'Date',
|
||||
email_has_been_sent: 'Email has been sent'
|
||||
}
|
||||
@@ -71,6 +76,11 @@ const es = {
|
||||
set_today: 'Establecer Hoy',
|
||||
date: 'Fecha',
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -108,6 +118,11 @@ const it = {
|
||||
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||
set_today: 'Imposta Oggi',
|
||||
date: 'Data',
|
||||
draw_signature: 'Disegna firma',
|
||||
type_initial: 'Inserisci iniziali',
|
||||
draw: 'Disegna',
|
||||
type: 'Inserisci',
|
||||
type_text: 'Inserisci testo',
|
||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||
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',
|
||||
set_today: 'Heute einstellen',
|
||||
date: 'Datum',
|
||||
draw_signature: 'Unterschrift zeichnen',
|
||||
type_initial: 'Initialen eingeben',
|
||||
draw: 'Zeichnen',
|
||||
type: 'Eingeben',
|
||||
type_text: 'Text eingeben',
|
||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||
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',
|
||||
set_today: "Définir Aujourd'hui",
|
||||
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',
|
||||
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',
|
||||
set_today: 'Ustaw Dziś',
|
||||
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',
|
||||
email_has_been_sent: 'E-mail został wysłany'
|
||||
}
|
||||
@@ -260,6 +290,11 @@ const uk = {
|
||||
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||
set_today: 'Задати Сьогодні',
|
||||
date: 'Дата',
|
||||
draw_signature: 'Намалюйте підпис',
|
||||
type_initial: 'Введіть ініціали',
|
||||
draw: 'Підпис',
|
||||
type: 'Текст',
|
||||
type_text: 'Введіть текст',
|
||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||
email_has_been_sent: 'Електронний лист був відправлений'
|
||||
}
|
||||
@@ -298,6 +333,11 @@ const cs = {
|
||||
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||
set_today: 'Nastavit Dnes',
|
||||
date: 'Datum',
|
||||
draw_signature: 'Nakreslit podpis',
|
||||
type_initial: 'Zadat iniciály',
|
||||
draw: 'Kreslit',
|
||||
type: 'Zadat',
|
||||
type_text: 'Zadat text',
|
||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||
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',
|
||||
set_today: 'Definir Hoje',
|
||||
date: 'Data',
|
||||
draw_signature: 'Desenhar assinatura',
|
||||
type_initial: 'Inserir iniciais',
|
||||
draw: 'Desenhar',
|
||||
type: 'Inserir',
|
||||
type_text: 'Inserir texto',
|
||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||
email_has_been_sent: 'Email enviado'
|
||||
}
|
||||
|
||||
@@ -6,23 +6,43 @@
|
||||
>{{ field.name || t('initials') }}</label>
|
||||
<div class="space-x-2 flex">
|
||||
<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"
|
||||
:data-tip="t('draw_initials')"
|
||||
>
|
||||
<a
|
||||
id="type_text_button"
|
||||
href="#"
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
||||
class="btn btn-outline font-medium btn-sm"
|
||||
@click.prevent="toggleTextInput"
|
||||
>
|
||||
<IconSignature :width="16" />
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('draw') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<a
|
||||
v-if="modelValue"
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn font-medium btn-outline btn-sm"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
@@ -31,7 +51,7 @@
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn font-medium btn-outline btn-sm"
|
||||
@click.prevent="clear"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
@@ -51,26 +71,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
:value="modelValue"
|
||||
:value="modelValue || computedPreviousValue"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue"
|
||||
:src="attachmentsIndex[modelValue].url"
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||
>
|
||||
<canvas
|
||||
v-show="!modelValue"
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
class="bg-white border border-base-300 rounded"
|
||||
class="bg-white border border-base-300 rounded-2xl"
|
||||
/>
|
||||
<input
|
||||
v-if="!isDrawInitials"
|
||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||
id="initials_text_input"
|
||||
ref="textInput"
|
||||
class="base-input !text-2xl w-full mt-6 text-center"
|
||||
:required="field.required && !isInitialsStarted && !modelValue"
|
||||
:required="field.required && !isInitialsStarted"
|
||||
:placeholder="`${t('type_initial_here')}...`"
|
||||
type="text"
|
||||
@focus="$emit('focus')"
|
||||
@@ -81,12 +101,14 @@
|
||||
|
||||
<script>
|
||||
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'
|
||||
|
||||
export default {
|
||||
name: 'InitialsStep',
|
||||
components: {
|
||||
IconReload,
|
||||
IconTextSize,
|
||||
IconSignature,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
@@ -110,6 +132,11 @@ export default {
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
previousValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -119,10 +146,20 @@ export default {
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||
data () {
|
||||
return {
|
||||
isInitialsStarted: false,
|
||||
isInitialsStarted: !!this.previousValue,
|
||||
isUsePreviousValue: true,
|
||||
isDrawInitials: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedPreviousValue () {
|
||||
if (this.isUsePreviousValue) {
|
||||
return this.previousValue
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.canvas) {
|
||||
@@ -137,8 +174,6 @@ export default {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
|
||||
const { default: SignaturePad } = await import('signature_pad')
|
||||
|
||||
if (this.$refs.canvas) {
|
||||
this.pad = new SignaturePad(this.$refs.canvas)
|
||||
|
||||
@@ -152,6 +187,9 @@ export default {
|
||||
methods: {
|
||||
remove () {
|
||||
this.$emit('update:model-value', '')
|
||||
|
||||
this.isUsePreviousValue = false
|
||||
this.isInitialsStarted = false
|
||||
},
|
||||
clear () {
|
||||
this.pad.clear()
|
||||
@@ -193,7 +231,11 @@ export default {
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
if (this.modelValue) {
|
||||
if (this.modelValue || this.computedPreviousValue) {
|
||||
if (this.computedPreviousValue) {
|
||||
this.$emit('update:model-value', this.computedPreviousValue)
|
||||
}
|
||||
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,25 +7,25 @@
|
||||
<div class="flex w-full">
|
||||
<div class="space-y-3.5 mx-auto">
|
||||
<div
|
||||
v-for="(option, index) in field.options"
|
||||
:key="index"
|
||||
v-for="option in field.options"
|
||||
:key="option.uuid"
|
||||
>
|
||||
<label
|
||||
:for="field.uuid + option"
|
||||
:for="option.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<input
|
||||
:id="field.uuid + option"
|
||||
:id="option.uuid"
|
||||
:ref="setInputRef"
|
||||
type="checkbox"
|
||||
:name="`values[${field.uuid}][]`"
|
||||
:value="option"
|
||||
:value="option.value"
|
||||
class="base-checkbox !h-7 !w-7"
|
||||
:checked="(modelValue || []).includes(option)"
|
||||
:checked="(modelValue || []).includes(option.value)"
|
||||
@change="onChange"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ option }}
|
||||
{{ option.value }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
>
|
||||
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||
<a
|
||||
v-if="!defaultValue"
|
||||
href="#"
|
||||
class="link"
|
||||
@click.prevent="isCodeSent = false"
|
||||
@@ -48,6 +49,7 @@
|
||||
:id="field.uuid"
|
||||
ref="phone"
|
||||
:value="modelValue || defaultValue"
|
||||
:readonly="!!defaultValue"
|
||||
class="base-input !text-2xl w-full"
|
||||
autocomplete="tel"
|
||||
pattern="^\+[0-9\s\-]+$"
|
||||
|
||||
@@ -6,24 +6,45 @@
|
||||
>{{ field.name || t('signature') }}</label>
|
||||
<div class="space-x-2 flex">
|
||||
<span
|
||||
v-if="isTextSignature"
|
||||
class="tooltip"
|
||||
data-tip="Type text"
|
||||
:data-tip="t('draw_signature')"
|
||||
>
|
||||
<button
|
||||
<a
|
||||
id="type_text_button"
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@click.prevent="toggleTextInput"
|
||||
>
|
||||
<IconSignature :width="16" />
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('draw') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="tooltip"
|
||||
:data-tip="t('type_text')"
|
||||
>
|
||||
<a
|
||||
id="type_text_button"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@click.prevent="toggleTextInput"
|
||||
>
|
||||
<IconTextSize :width="16" />
|
||||
</button>
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('type') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
class="tooltip"
|
||||
data-tip="Take photo"
|
||||
>
|
||||
<label
|
||||
class="btn btn-outline btn-sm btn-circle"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
>
|
||||
<IconCamera :width="16" />
|
||||
<input
|
||||
@@ -32,25 +53,31 @@
|
||||
accept="image/*"
|
||||
@change="drawImage"
|
||||
>
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('upload') }}
|
||||
</span>
|
||||
</label>
|
||||
</span>
|
||||
<button
|
||||
v-if="modelValue"
|
||||
class="btn btn-outline btn-sm"
|
||||
<a
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
{{ t('redraw') }}
|
||||
</button>
|
||||
<button
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="btn btn-outline btn-sm"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@click.prevent="clear"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
{{ t('clear') }}
|
||||
</button>
|
||||
<button
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
title="Minimize"
|
||||
class="py-1.5 inline md:hidden"
|
||||
@click.prevent="$emit('minimize')"
|
||||
@@ -59,23 +86,24 @@
|
||||
:width="20"
|
||||
:height="20"
|
||||
/>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
:value="modelValue"
|
||||
:value="modelValue || computedPreviousValue"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue"
|
||||
:src="attachmentsIndex[modelValue].url"
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||
>
|
||||
<canvas
|
||||
v-show="!modelValue"
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
class="bg-white border border-base-300 rounded"
|
||||
style="padding: 1px; 0"
|
||||
class="bg-white border border-base-300 rounded-2xl"
|
||||
/>
|
||||
<input
|
||||
v-if="isTextSignature"
|
||||
@@ -91,8 +119,11 @@
|
||||
</template>
|
||||
|
||||
<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 SignaturePad from 'signature_pad'
|
||||
|
||||
let isFontLoaded = false
|
||||
|
||||
export default {
|
||||
name: 'SignatureStep',
|
||||
@@ -100,6 +131,7 @@ export default {
|
||||
IconReload,
|
||||
IconCamera,
|
||||
IconTextSize,
|
||||
IconSignature,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
inject: ['baseUrl', 't'],
|
||||
@@ -122,6 +154,11 @@ export default {
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
previousValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -131,10 +168,20 @@ export default {
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
||||
data () {
|
||||
return {
|
||||
isSignatureStarted: false,
|
||||
isSignatureStarted: !!this.previousValue,
|
||||
isUsePreviousValue: true,
|
||||
isTextSignature: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedPreviousValue () {
|
||||
if (this.isUsePreviousValue) {
|
||||
return this.previousValue
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.canvas) {
|
||||
@@ -147,8 +194,6 @@ export default {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
|
||||
const { default: SignaturePad } = await import('signature_pad')
|
||||
|
||||
if (this.$refs.canvas) {
|
||||
this.pad = new SignaturePad(this.$refs.canvas)
|
||||
|
||||
@@ -162,6 +207,22 @@ export default {
|
||||
methods: {
|
||||
remove () {
|
||||
this.$emit('update:model-value', '')
|
||||
|
||||
this.isUsePreviousValue = false
|
||||
this.isSignatureStarted = false
|
||||
},
|
||||
loadFont () {
|
||||
if (!isFontLoaded) {
|
||||
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
|
||||
|
||||
font.load().then((loadedFont) => {
|
||||
document.fonts.add(loadedFont)
|
||||
|
||||
isFontLoaded = true
|
||||
}).catch((error) => {
|
||||
console.error('Font loading failed:', error)
|
||||
})
|
||||
}
|
||||
},
|
||||
clear () {
|
||||
this.pad.clear()
|
||||
@@ -178,8 +239,8 @@ export default {
|
||||
const canvas = this.$refs.canvas
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
const fontFamily = 'Arial'
|
||||
const fontSize = '44px'
|
||||
const fontFamily = 'Dancing Script'
|
||||
const fontSize = '38px'
|
||||
const fontStyle = 'italic'
|
||||
const fontWeight = ''
|
||||
|
||||
@@ -197,6 +258,8 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textInput.focus()
|
||||
|
||||
this.loadFont()
|
||||
|
||||
this.$emit('start')
|
||||
})
|
||||
}
|
||||
@@ -250,7 +313,11 @@ export default {
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
if (this.modelValue) {
|
||||
if (this.modelValue || this.computedPreviousValue) {
|
||||
if (this.computedPreviousValue) {
|
||||
this.$emit('update:model-value', this.computedPreviousValue)
|
||||
}
|
||||
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
v-if="!isTextArea"
|
||||
:id="field.uuid"
|
||||
v-model="text"
|
||||
:maxlength="cellsMaxLegth"
|
||||
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||
:required="field.required"
|
||||
:pattern="field.validation?.pattern"
|
||||
@@ -38,7 +39,7 @@
|
||||
@focus="$emit('focus')"
|
||||
/>
|
||||
<div
|
||||
v-if="!isTextArea"
|
||||
v-if="!isTextArea && field.type !== 'cells'"
|
||||
class="tooltip"
|
||||
:data-tip="t('toggle_multiline_text')"
|
||||
>
|
||||
@@ -80,6 +81,19 @@ export default {
|
||||
}
|
||||
},
|
||||
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: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||
>
|
||||
<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"
|
||||
style="left: -4px"
|
||||
@mousedown.stop="startResizeCell"
|
||||
@@ -42,6 +42,7 @@
|
||||
v-model="field.submitter_uuid"
|
||||
class="border-r"
|
||||
:compact="true"
|
||||
:editable="editable"
|
||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
||||
:submitters="template.submitters"
|
||||
@update:model-value="save"
|
||||
@@ -50,6 +51,7 @@
|
||||
<FieldType
|
||||
v-model="field.type"
|
||||
:button-width="27"
|
||||
:editable="editable"
|
||||
:button-classes="'px-1'"
|
||||
:menu-classes="'bg-white rounded-t-none'"
|
||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||
@@ -58,15 +60,15 @@
|
||||
<span
|
||||
v-if="field.type !== 'checkbox' || field.name"
|
||||
ref="name"
|
||||
contenteditable
|
||||
:contenteditable="editable"
|
||||
class="pr-1 cursor-text outline-none block"
|
||||
style="min-width: 2px"
|
||||
@keydown.enter.prevent="onNameEnter"
|
||||
@focus="onNameFocus"
|
||||
@blur="onNameBlur"
|
||||
>{{ field.name || defaultName }}</span>
|
||||
>{{ optionIndexText }} {{ field.name || defaultName }}</span>
|
||||
<div
|
||||
v-if="isNameFocus && field.type !== 'checkbox'"
|
||||
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
|
||||
class="flex items-center ml-1.5"
|
||||
>
|
||||
<input
|
||||
@@ -84,7 +86,7 @@
|
||||
>Required</label>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
v-else-if="editable"
|
||||
class="pr-1"
|
||||
title="Remove"
|
||||
@click.prevent="$emit('remove')"
|
||||
@@ -93,18 +95,30 @@
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="opacity-50 flex items-center justify-center h-full w-full"
|
||||
:class="bgColors[submitterIndex]"
|
||||
class="flex items-center h-full w-full"
|
||||
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
||||
>
|
||||
<span
|
||||
v-if="field"
|
||||
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
|
||||
:is="fieldIcons[field.type]"
|
||||
v-else
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="max-h-10"
|
||||
class="max-h-10 opacity-50"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -113,7 +127,7 @@
|
||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||
/>
|
||||
<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"
|
||||
@mousedown.stop="startResize"
|
||||
@touchstart="startTouchResize"
|
||||
@@ -126,6 +140,7 @@ import FieldSubmitter from './field_submitter'
|
||||
import FieldType from './field_type'
|
||||
import Field from './field'
|
||||
import { IconX } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
@@ -145,6 +160,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -157,6 +177,7 @@ export default {
|
||||
isResize: false,
|
||||
isDragged: false,
|
||||
isNameFocus: false,
|
||||
textOverflowChars: 0,
|
||||
dragFrom: { x: 0, y: 0 }
|
||||
}
|
||||
},
|
||||
@@ -164,6 +185,13 @@ export default {
|
||||
defaultName: Field.computed.defaultName,
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
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 () {
|
||||
const cells = []
|
||||
|
||||
@@ -185,30 +213,30 @@ export default {
|
||||
},
|
||||
borderColors () {
|
||||
return [
|
||||
'border-red-500',
|
||||
'border-sky-500',
|
||||
'border-emerald-500',
|
||||
'border-yellow-300',
|
||||
'border-purple-600',
|
||||
'border-pink-500',
|
||||
'border-cyan-500',
|
||||
'border-orange-500',
|
||||
'border-lime-500',
|
||||
'border-indigo-500'
|
||||
'border-red-500/50',
|
||||
'border-sky-500/50',
|
||||
'border-emerald-500/50',
|
||||
'border-yellow-300/50',
|
||||
'border-purple-600/50',
|
||||
'border-pink-500/50',
|
||||
'border-cyan-500/50',
|
||||
'border-orange-500/50',
|
||||
'border-lime-500/50',
|
||||
'border-indigo-500/50'
|
||||
]
|
||||
},
|
||||
bgColors () {
|
||||
return [
|
||||
'bg-red-100',
|
||||
'bg-sky-100',
|
||||
'bg-emerald-100',
|
||||
'bg-yellow-100',
|
||||
'bg-purple-100',
|
||||
'bg-pink-100',
|
||||
'bg-cyan-100',
|
||||
'bg-orange-100',
|
||||
'bg-lime-100',
|
||||
'bg-indigo-100'
|
||||
'bg-red-100/50',
|
||||
'bg-sky-100/50',
|
||||
'bg-emerald-100/50',
|
||||
'bg-yellow-100/50',
|
||||
'bg-purple-100/50',
|
||||
'bg-pink-100/50',
|
||||
'bg-cyan-100/50',
|
||||
'bg-orange-100/50',
|
||||
'bg-lime-100/50',
|
||||
'bg-indigo-100/50'
|
||||
]
|
||||
},
|
||||
isSelected () {
|
||||
@@ -225,6 +253,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: {
|
||||
onNameFocus (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
@@ -262,12 +304,14 @@ export default {
|
||||
}
|
||||
},
|
||||
maybeUpdateOptions () {
|
||||
delete this.field.default_value
|
||||
|
||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||
delete this.field.options
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||
this.field.options ||= ['']
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
@@ -313,6 +357,10 @@ export default {
|
||||
startDrag (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
class="mx-auto pl-3 md:pl-4 h-full"
|
||||
>
|
||||
<div
|
||||
v-if="$slots.buttons || withTitle"
|
||||
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
|
||||
:style="{ backgroundColor }"
|
||||
>
|
||||
@@ -15,7 +16,9 @@
|
||||
<Logo />
|
||||
</a>
|
||||
<Contenteditable
|
||||
v-if="withTitle"
|
||||
:model-value="template.name"
|
||||
:editable="editable"
|
||||
class="text-3xl font-semibold focus:text-clip"
|
||||
:icon-stroke-width="2.3"
|
||||
@update:model-value="updateName"
|
||||
@@ -31,6 +34,7 @@
|
||||
:href="`/templates/${template.id}/submissions/new`"
|
||||
data-turbo-frame="modal"
|
||||
class="btn btn-primary text-base"
|
||||
@click="maybeShowEmptyTemplateAlert"
|
||||
>
|
||||
<IconUsersPlus
|
||||
width="20"
|
||||
@@ -62,7 +66,10 @@
|
||||
</template>
|
||||
</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
|
||||
ref="previews"
|
||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||
@@ -75,6 +82,8 @@
|
||||
:item="item"
|
||||
:document="sortedDocuments[index]"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-replace-button="withUploadButton"
|
||||
:editable="editable"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@scroll-to="scrollIntoDocument(item)"
|
||||
@@ -86,10 +95,10 @@
|
||||
/>
|
||||
<div
|
||||
class="sticky bottom-0 py-2"
|
||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||
:style="withStickySubmitters ? { backgroundColor } : {}"
|
||||
>
|
||||
<Upload
|
||||
v-if="sortedDocuments.length"
|
||||
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:template-id="template.id"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -103,7 +112,7 @@
|
||||
class="pr-3.5 pl-0.5"
|
||||
>
|
||||
<Dropzone
|
||||
v-if="!sortedDocuments.length"
|
||||
v-if="!sortedDocuments.length && withUploadButton"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -119,16 +128,19 @@
|
||||
:areas-index="fieldAreasIndex[document.uuid]"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:document="document"
|
||||
:is-drag="!!dragFieldType"
|
||||
:is-drag="!!dragField"
|
||||
:draw-field="drawField"
|
||||
:editable="editable"
|
||||
:base-url="baseUrl"
|
||||
@draw="onDraw"
|
||||
@drop-field="onDropfield"
|
||||
@remove-area="removeArea"
|
||||
/>
|
||||
<DocumentControls
|
||||
v-if="isBreakpointLg"
|
||||
v-if="isBreakpointLg && editable"
|
||||
:with-arrows="template.schema.length > 1"
|
||||
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||
:with-replace-button="withUploadButton"
|
||||
:document="document"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -141,10 +153,11 @@
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-if="sortedDocuments.length && isBreakpointLg"
|
||||
v-if="sortedDocuments.length && isBreakpointLg && editable"
|
||||
class="pb-4"
|
||||
>
|
||||
<Upload
|
||||
v-if="withUploadButton"
|
||||
:template-id="template.id"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@success="updateFromUpload"
|
||||
@@ -152,34 +165,19 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span
|
||||
v-if="drawField"
|
||||
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
|
||||
>
|
||||
<span
|
||||
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"
|
||||
>
|
||||
<component
|
||||
:is="fieldIcons[drawField.type]"
|
||||
:width="20"
|
||||
: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>
|
||||
<MobileDrawField
|
||||
v-if="drawField && isBreakpointLg"
|
||||
:draw-field="drawField"
|
||||
:fields="template.fields"
|
||||
:submitters="template.submitters"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
class="md:hidden"
|
||||
:editable="editable"
|
||||
@cancel="[drawField = null, drawOption = null]"
|
||||
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||
/>
|
||||
<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"
|
||||
:model-value="''"
|
||||
@update:model-value="startFieldDraw($event)"
|
||||
@@ -207,7 +205,8 @@
|
||||
>
|
||||
<div
|
||||
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">
|
||||
<p>
|
||||
@@ -216,7 +215,7 @@
|
||||
<p>
|
||||
<button
|
||||
class="base-button"
|
||||
@click="drawField = null"
|
||||
@click="[drawField = null, drawOption = null]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -229,11 +228,13 @@
|
||||
:fields="template.fields"
|
||||
:submitters="template.submitters"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:default-fields="defaultFields"
|
||||
:with-sticky-submitters="withStickySubmitters"
|
||||
@set-draw="drawField = $event"
|
||||
@set-drag="dragFieldType = $event"
|
||||
:editable="editable"
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@set-drag="dragField = $event"
|
||||
@change-submitter="selectedSubmitter = $event"
|
||||
@drag-end="dragFieldType = null"
|
||||
@drag-end="dragField = null"
|
||||
@scroll-to-area="scrollToArea"
|
||||
/>
|
||||
</div>
|
||||
@@ -246,6 +247,7 @@
|
||||
import Upload from './upload'
|
||||
import Dropzone from './dropzone'
|
||||
import Fields from './fields'
|
||||
import MobileDrawField from './mobile_draw_field'
|
||||
import Document from './document'
|
||||
import Logo from './logo'
|
||||
import Contenteditable from './contenteditable'
|
||||
@@ -262,6 +264,7 @@ export default {
|
||||
Upload,
|
||||
Document,
|
||||
Fields,
|
||||
MobileDrawField,
|
||||
IconPlus,
|
||||
FieldType,
|
||||
IconX,
|
||||
@@ -299,6 +302,21 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
defaultFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
defaultSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -319,6 +337,16 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withUploadButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withTitle: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withPhone: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -337,7 +365,8 @@ export default {
|
||||
isSaving: false,
|
||||
selectedSubmitter: null,
|
||||
drawField: null,
|
||||
dragFieldType: null
|
||||
drawOption: null,
|
||||
dragField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -369,6 +398,13 @@ export default {
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.defaultSubmitters.forEach((name, index) => {
|
||||
const submitter = (this.template.submitters[index] ||= {})
|
||||
|
||||
submitter.name = name
|
||||
submitter.uuid ||= v4()
|
||||
})
|
||||
|
||||
this.selectedSubmitter = this.template.submitters[0]
|
||||
},
|
||||
mounted () {
|
||||
@@ -405,10 +441,11 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = ['']
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
this.drawField = field
|
||||
this.drawOption = null
|
||||
},
|
||||
undo () {
|
||||
if (this.undoStack.length > 1) {
|
||||
@@ -458,6 +495,7 @@ export default {
|
||||
onKeyUp (e) {
|
||||
if (e.code === 'Escape') {
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
this.selectedAreaRef.value = null
|
||||
}
|
||||
|
||||
@@ -504,6 +542,16 @@ export default {
|
||||
},
|
||||
onDraw (area) {
|
||||
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.push(area)
|
||||
|
||||
@@ -512,6 +560,7 @@ export default {
|
||||
}
|
||||
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
|
||||
@@ -561,14 +610,14 @@ export default {
|
||||
onDropfield (area) {
|
||||
const field = {
|
||||
name: '',
|
||||
type: this.dragFieldType,
|
||||
uuid: v4(),
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
required: this.dragFieldType !== 'checkbox'
|
||||
required: this.dragField.type !== 'checkbox',
|
||||
...this.dragField
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
||||
field.options = ['']
|
||||
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
const fieldArea = {
|
||||
@@ -582,27 +631,27 @@ export default {
|
||||
|
||||
let baseArea
|
||||
|
||||
if (this.selectedField?.type === this.dragFieldType) {
|
||||
if (this.selectedField?.type === field.type) {
|
||||
baseArea = this.selectedAreaRef.value
|
||||
} else if (previousField?.areas?.length) {
|
||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||
} else {
|
||||
if (['checkbox'].includes(this.dragFieldType)) {
|
||||
if (['checkbox'].includes(field.type)) {
|
||||
baseArea = {
|
||||
w: area.maskW / 30 / area.maskW,
|
||||
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||
}
|
||||
} else if (this.dragFieldType === 'image') {
|
||||
} else if (field.type === 'image') {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||
}
|
||||
} else if (this.dragFieldType === 'signature') {
|
||||
} else if (field.type === 'signature') {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||
}
|
||||
} else if (this.dragFieldType === 'initials') {
|
||||
} else if (field.type === 'initials') {
|
||||
baseArea = {
|
||||
w: area.maskW / 10 / area.maskW,
|
||||
h: area.maskW / 35 / area.maskW
|
||||
@@ -619,7 +668,7 @@ export default {
|
||||
fieldArea.h = baseArea.h
|
||||
fieldArea.y = fieldArea.y - baseArea.h / 2
|
||||
|
||||
if (this.dragFieldType === 'cells') {
|
||||
if (field.type === 'cells') {
|
||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||
}
|
||||
|
||||
@@ -691,14 +740,25 @@ export default {
|
||||
|
||||
this.save()
|
||||
},
|
||||
onSaveClick () {
|
||||
this.isSaving = true
|
||||
maybeShowEmptyTemplateAlert (e) {
|
||||
if (!this.template.fields.length) {
|
||||
e.preventDefault()
|
||||
|
||||
this.save().then(() => {
|
||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
})
|
||||
alert('Please draw fields to prepare the document.')
|
||||
}
|
||||
},
|
||||
onSaveClick () {
|
||||
if (this.template.fields.length) {
|
||||
this.isSaving = true
|
||||
|
||||
this.save().then(() => {
|
||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
})
|
||||
} else {
|
||||
alert('Please draw fields to prepare the document.')
|
||||
}
|
||||
},
|
||||
scrollToArea (area) {
|
||||
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<span
|
||||
ref="contenteditable"
|
||||
contenteditable
|
||||
:contenteditable="editable"
|
||||
style="min-width: 2px"
|
||||
:class="iconInline ? 'inline' : 'block'"
|
||||
class="peer outline-none focus:block"
|
||||
@@ -24,10 +24,10 @@
|
||||
*
|
||||
</span>
|
||||
<IconPencil
|
||||
class="cursor-pointer flex-none opacity-0 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' }"
|
||||
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"
|
||||
:stroke-width="iconStrokeWidth"
|
||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||
@@ -69,6 +69,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
iconStrokeWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@update:model-value="onUpdateName"
|
||||
/>
|
||||
<ReplaceButton
|
||||
v-if="withReplaceButton"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
@click.stop
|
||||
@@ -67,6 +68,11 @@ export default {
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
withReplaceButton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: true
|
||||
},
|
||||
withArrows: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:key="image.id"
|
||||
:ref="setPageRefs"
|
||||
:number="index"
|
||||
:editable="editable"
|
||||
:areas="areasIndex[index]"
|
||||
:is-drag="isDrag"
|
||||
:draw-field="drawField"
|
||||
@@ -38,11 +39,21 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
drawField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -56,6 +67,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
basePreviewUrl () {
|
||||
if (this.baseUrl) {
|
||||
return new URL(this.baseUrl).origin
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
numberOfPages () {
|
||||
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||
},
|
||||
@@ -66,7 +84,7 @@ export default {
|
||||
return this.previewImagesIndex[i] || {
|
||||
metadata: lazyloadMetadata,
|
||||
id: Math.random().toString(),
|
||||
url: `/preview/${this.document.uuid}/${i}.jpg`
|
||||
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
>
|
||||
<div
|
||||
class="border border-base-300 rounded rounded-tr-none relative group"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
>
|
||||
<div class="flex items-center justify-between relative">
|
||||
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||
@click="scrollToFirstArea"
|
||||
@@ -13,6 +14,7 @@
|
||||
<div class="flex items-center p-1 space-x-1">
|
||||
<FieldType
|
||||
v-model="field.type"
|
||||
:editable="editable"
|
||||
:button-width="20"
|
||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||
@click="scrollToFirstArea"
|
||||
@@ -20,6 +22,7 @@
|
||||
<Contenteditable
|
||||
ref="name"
|
||||
:model-value="field.name || defaultName"
|
||||
:editable="editable"
|
||||
:icon-inline="true"
|
||||
:icon-width="18"
|
||||
:icon-stroke-width="1.6"
|
||||
@@ -31,34 +34,46 @@
|
||||
v-if="isNameFocus"
|
||||
class="flex items-center relative"
|
||||
>
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
<template v-if="field.type != 'phone'">
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-else-if="editable"
|
||||
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
|
||||
v-if="field.areas?.length"
|
||||
class="dropdown dropdown-end"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
title="Areas"
|
||||
title="Settings"
|
||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||
>
|
||||
<IconShape
|
||||
<IconSettings
|
||||
:width="18"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
@@ -66,8 +81,60 @@
|
||||
<ul
|
||||
tabindex="0"
|
||||
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"
|
||||
>
|
||||
<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
|
||||
v-for="(area, index) in field.areas || []"
|
||||
:key="index"
|
||||
@@ -84,11 +151,11 @@
|
||||
Page {{ area.page + 1 }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@click.prevent="$emit('set-draw', field)"
|
||||
@click.prevent="$emit('set-draw', { field })"
|
||||
>
|
||||
<IconNewSection
|
||||
:width="20"
|
||||
@@ -113,18 +180,7 @@
|
||||
</ul>
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
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"
|
||||
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||
title="Remove"
|
||||
@click="$emit('remove', field)"
|
||||
>
|
||||
@@ -133,48 +189,58 @@
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
</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
|
||||
v-if="field.options"
|
||||
ref="options"
|
||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||
draggable="true"
|
||||
@dragstart.prevent.stop
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in field.options"
|
||||
:key="index"
|
||||
:key="option.uuid"
|
||||
class="flex space-x-1.5 items-center"
|
||||
>
|
||||
<span class="text-sm w-3.5">
|
||||
{{ index + 1 }}.
|
||||
</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
|
||||
v-model="field.options[index]"
|
||||
v-else
|
||||
v-model="option.value"
|
||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||
type="text"
|
||||
required
|
||||
@focus="maybeFocusOnOptionArea(option)"
|
||||
@blur="save"
|
||||
>
|
||||
<button
|
||||
class="text-sm w-3.5"
|
||||
@click="[field.options.splice(index, 1), save()]"
|
||||
tabindex="-1"
|
||||
@click="removeOption(option)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -182,7 +248,7 @@
|
||||
<button
|
||||
v-if="field.options"
|
||||
class="text-center text-sm w-full pb-1"
|
||||
@click="[field.options.push(''), save()]"
|
||||
@click="addOption"
|
||||
>
|
||||
+ Add option
|
||||
</button>
|
||||
@@ -194,26 +260,33 @@
|
||||
<script>
|
||||
import Contenteditable from './contenteditable'
|
||||
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 {
|
||||
name: 'TemplateField',
|
||||
components: {
|
||||
Contenteditable,
|
||||
IconSettings,
|
||||
IconShape,
|
||||
IconNewSection,
|
||||
IconTrashX,
|
||||
IconCopy,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'save'],
|
||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||
data () {
|
||||
return {
|
||||
isNameFocus: false
|
||||
@@ -259,19 +332,45 @@ export default {
|
||||
}, 1)
|
||||
}
|
||||
},
|
||||
maybeFocusOnOptionArea (option) {
|
||||
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||
|
||||
if (area) {
|
||||
this.selectedAreaRef.value = area
|
||||
}
|
||||
},
|
||||
scrollToFirstArea () {
|
||||
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
||||
},
|
||||
closeDropdown () {
|
||||
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 () {
|
||||
delete this.field.default_value
|
||||
|
||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||
delete this.field.options
|
||||
}
|
||||
|
||||
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||
this.field.options ||= ['']
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
|
||||
@@ -1,5 +1,91 @@
|
||||
<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
|
||||
v-if="compact"
|
||||
tabindex="0"
|
||||
@@ -14,7 +100,7 @@
|
||||
<label
|
||||
v-else
|
||||
tabindex="0"
|
||||
class="cursor-pointer 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">
|
||||
<span
|
||||
@@ -25,12 +111,13 @@
|
||||
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>
|
||||
<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
|
||||
width="18"
|
||||
height="18"
|
||||
@@ -38,6 +125,7 @@
|
||||
</span>
|
||||
</label>
|
||||
<ul
|
||||
v-if="editable || !compact"
|
||||
tabindex="0"
|
||||
:class="menuClasses"
|
||||
@click="closeDropdown"
|
||||
@@ -62,7 +150,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
v-if="!compact && submitters.length > 1"
|
||||
v-if="!compact && submitters.length > 1 && editable"
|
||||
class="hidden group-hover:block px-2"
|
||||
@click.stop="remove(submitter)"
|
||||
>
|
||||
@@ -70,7 +158,7 @@
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="submitters.length < 10">
|
||||
<li v-if="submitters.length < 10 && editable">
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-2"
|
||||
@@ -81,7 +169,7 @@
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span class="py-1">
|
||||
Add Submitter
|
||||
Add {{ names[submitters.length] }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -90,7 +178,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
||||
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
|
||||
import Contenteditable from './contenteditable'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
@@ -100,18 +188,29 @@ export default {
|
||||
IconUserPlus,
|
||||
Contenteditable,
|
||||
IconPlus,
|
||||
IconTrashX
|
||||
IconTrashX,
|
||||
IconChevronUp
|
||||
},
|
||||
props: {
|
||||
submitters: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
mobileView: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -140,16 +239,16 @@ export default {
|
||||
},
|
||||
names () {
|
||||
return [
|
||||
'First Submitter',
|
||||
'Second Submitter',
|
||||
'Third Submitter',
|
||||
'Fourth Submitter',
|
||||
'Fifth Submitter',
|
||||
'Sixth Submitter',
|
||||
'Seventh Submitter',
|
||||
'Eighth Submitter',
|
||||
'Ninth Submitter',
|
||||
'Tenth Submitter'
|
||||
'First Party',
|
||||
'Second Party',
|
||||
'Third Party',
|
||||
'Fourth Party',
|
||||
'Fifth Party',
|
||||
'Sixth Party',
|
||||
'Seventh Party',
|
||||
'Eighth Party',
|
||||
'Ninth Party',
|
||||
'Tenth Party'
|
||||
]
|
||||
},
|
||||
selectedSubmitter () {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
</label>
|
||||
</slot>
|
||||
<ul
|
||||
v-if="editable"
|
||||
tabindex="0"
|
||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||
:class="menuClasses"
|
||||
@@ -65,6 +66,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
buttonWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
|
||||
@@ -5,26 +5,66 @@
|
||||
class="w-full rounded-lg"
|
||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||
:submitters="submitters"
|
||||
:editable="editable"
|
||||
@new-submitter="save"
|
||||
@remove="removeSubmitter"
|
||||
@name-change="save"
|
||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-1 mt-2">
|
||||
<div
|
||||
class="mb-1 mt-2"
|
||||
@dragover.prevent="onFieldDragover"
|
||||
@drop="save"
|
||||
>
|
||||
<Field
|
||||
v-for="field in submitterFields"
|
||||
:key="field.uuid"
|
||||
:data-uuid="field.uuid"
|
||||
:field="field"
|
||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||
:editable="editable && !dragField"
|
||||
:draggable="editable"
|
||||
@dragstart="dragField = field"
|
||||
@dragend="dragField = null"
|
||||
@remove="removeField"
|
||||
@move-up="move(field, -1)"
|
||||
@move-down="move(field, 1)"
|
||||
@scroll-to="$emit('scroll-to-area', $event)"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
/>
|
||||
</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
|
||||
v-for="(icon, type) in fieldIcons"
|
||||
:key="type"
|
||||
@@ -32,37 +72,14 @@
|
||||
<button
|
||||
v-if="withPhone || type != 'phone'"
|
||||
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 }"
|
||||
@dragstart="onDragstart(type)"
|
||||
@dragstart="onDragstart({ type: type })"
|
||||
@dragend="$emit('drag-end')"
|
||||
@click="addField(type)"
|
||||
>
|
||||
<div class="w-0 absolute left-0">
|
||||
<svg
|
||||
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 class="flex items-console group-hover:bg-base-200/50 transition-all cursor-grab h-full absolute left-0">
|
||||
<IconDrag class=" my-auto" />
|
||||
</div>
|
||||
<div class="flex items-center flex-col px-2 py-2">
|
||||
<component :is="icon" />
|
||||
@@ -100,7 +117,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="fields.length < 4"
|
||||
v-if="fields.length < 4 && editable"
|
||||
class="text-xs p-2 border border-base-200 rounded"
|
||||
>
|
||||
<ul class="list-disc list-outside ml-3">
|
||||
@@ -123,12 +140,15 @@ import { v4 } from 'uuid'
|
||||
import FieldType from './field_type'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconLock } from '@tabler/icons-vue'
|
||||
import IconDrag from './icon_drag'
|
||||
|
||||
export default {
|
||||
name: 'TemplateFields',
|
||||
components: {
|
||||
Field,
|
||||
FieldType,
|
||||
FieldSubmitter,
|
||||
IconDrag,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||
@@ -137,6 +157,16 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
defaultFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withStickySubmitters: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -152,16 +182,44 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||
data () {
|
||||
return {
|
||||
dragField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
submitterFields () {
|
||||
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: {
|
||||
onDragstart (fieldType) {
|
||||
this.$emit('set-drag', fieldType)
|
||||
onDragstart (field) {
|
||||
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) {
|
||||
[...this.fields].forEach((field) => {
|
||||
@@ -178,26 +236,6 @@ export default {
|
||||
|
||||
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) {
|
||||
this.fields.splice(this.fields.indexOf(field), 1)
|
||||
|
||||
@@ -214,11 +252,15 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = ['']
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
this.fields.push(field)
|
||||
|
||||
if (['signature', 'initials', 'cells'].includes(type)) {
|
||||
this.$emit('set-draw', { field })
|
||||
}
|
||||
|
||||
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,7 @@
|
||||
:ref="setAreaRefs"
|
||||
:area="item.area"
|
||||
:field="item.field"
|
||||
:editable="editable"
|
||||
@start-resize="resizeDirection = $event"
|
||||
@stop-resize="resizeDirection = null"
|
||||
@start-drag="isMove = true"
|
||||
@@ -39,8 +40,8 @@
|
||||
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
||||
id="mask"
|
||||
ref="mask"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
@pointermove="onPointermove"
|
||||
@pointerdown="onStartDraw"
|
||||
@dragover.prevent
|
||||
@@ -77,6 +78,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -141,6 +147,10 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
this.showMask = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
|
||||
@@ -12,10 +12,14 @@
|
||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
||||
@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 class="">
|
||||
<ReplaceButton
|
||||
v-if="withReplaceButton"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
@@ -95,11 +99,21 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
withReplaceButton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: true
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -10,19 +10,18 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
|
||||
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||
|
||||
enqueue_completed_emails(submitter)
|
||||
end
|
||||
|
||||
return unless is_all_completed
|
||||
return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at)
|
||||
return if Accounts.load_webhook_configs(submitter.account).blank?
|
||||
|
||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||
|
||||
enqueue_emails(submitter)
|
||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||
end
|
||||
|
||||
def enqueue_emails(submitter)
|
||||
def enqueue_completed_emails(submitter)
|
||||
user = submitter.submission.created_by_user || submitter.template.author
|
||||
|
||||
if submitter.template.account.users.exists?(id: user.id)
|
||||
@@ -32,7 +31,8 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
||||
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
|
||||
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?
|
||||
end
|
||||
@@ -49,6 +49,6 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
||||
|
||||
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
|
||||
@@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
|
||||
def perform(params = {})
|
||||
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')
|
||||
|
||||
|
||||
@@ -3,18 +3,26 @@
|
||||
class SubmitterMailer < ApplicationMailer
|
||||
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
|
||||
@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 =
|
||||
if @email_config || subject.present?
|
||||
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
||||
if @email_config || @subject
|
||||
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
|
||||
else
|
||||
'You are invited to submit a form'
|
||||
DEFAULT_INVITATION_SUBJECT
|
||||
end
|
||||
|
||||
mail(to: @submitter.friendly_name,
|
||||
@@ -31,7 +39,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
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)
|
||||
|
||||
@@ -58,7 +66,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
@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 =
|
||||
if @email_config
|
||||
@@ -87,12 +95,11 @@ class SubmitterMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
documents.each do |attachment|
|
||||
data = attachment.download
|
||||
total_size += data.size
|
||||
total_size += attachment.byte_size
|
||||
|
||||
break if total_size >= MAX_ATTACHMENTS_SIZE
|
||||
|
||||
attachments[attachment.filename.to_s] = data
|
||||
attachments[attachment.filename.to_s] = attachment.download
|
||||
end
|
||||
|
||||
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
class AccessToken < ApplicationRecord
|
||||
TOKEN_LENGTH = 22
|
||||
TOKEN_LENGTH = 43
|
||||
|
||||
belongs_to :user
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class Account < ApplicationRecord
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :encrypted_configs, dependent: :destroy
|
||||
has_many :account_configs, dependent: :destroy
|
||||
has_many :email_messages, dependent: :destroy
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :template_folders, dependent: :destroy
|
||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||
|
||||
@@ -49,7 +49,7 @@ class AccountConfig < ApplicationRecord
|
||||
'body' => "Hi there,\n\n" \
|
||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||
"Alternatively, you can download your copy using:\n\n" \
|
||||
"{{documents.links}}\n\n" \
|
||||
"{{documents.link}}\n\n" \
|
||||
"Thanks,\n" \
|
||||
'{{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
|
||||
@@ -6,6 +6,8 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# preferences :text not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters_order :string not null
|
||||
# template_fields :text
|
||||
@@ -19,6 +21,7 @@
|
||||
# Indexes
|
||||
#
|
||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||
# index_submissions_on_slug (slug) UNIQUE
|
||||
# index_submissions_on_template_id (template_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
@@ -34,13 +37,18 @@ class Submission < ApplicationRecord
|
||||
has_many :submitters, dependent: :destroy
|
||||
has_many :submission_events, dependent: :destroy
|
||||
|
||||
attribute :preferences, :string, default: -> { {} }
|
||||
|
||||
serialize :template_fields, JSON
|
||||
serialize :template_schema, JSON
|
||||
serialize :template_submitters, JSON
|
||||
serialize :preferences, JSON
|
||||
|
||||
attribute :source, :string, default: 'link'
|
||||
attribute :submitters_order, :string, default: 'random'
|
||||
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
|
||||
has_one_attached :audit_trail
|
||||
|
||||
has_many :template_schema_documents,
|
||||
@@ -49,7 +57,7 @@ class Submission < ApplicationRecord
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
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, {
|
||||
invite: 'invite',
|
||||
@@ -62,4 +70,9 @@ class Submission < ApplicationRecord
|
||||
random: 'random',
|
||||
preserved: 'preserved'
|
||||
}, scope: false, prefix: true
|
||||
|
||||
def audit_trail_url
|
||||
audit_trail&.url
|
||||
end
|
||||
alias audit_log_url audit_trail_url
|
||||
end
|
||||
@@ -12,6 +12,7 @@
|
||||
# name :string
|
||||
# opened_at :datetime
|
||||
# phone :string
|
||||
# preferences :text not null
|
||||
# sent_at :datetime
|
||||
# slug :string not null
|
||||
# ua :string
|
||||
@@ -37,9 +38,11 @@ class Submitter < ApplicationRecord
|
||||
has_one :account, through: :template
|
||||
|
||||
attribute :values, :string, default: -> { {} }
|
||||
attribute :preferences, :string, default: -> { {} }
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
|
||||
serialize :values, JSON
|
||||
serialize :preferences, JSON
|
||||
|
||||
has_many_attached :documents
|
||||
has_many_attached :attachments
|
||||
@@ -61,7 +64,7 @@ class Submitter < ApplicationRecord
|
||||
|
||||
def friendly_name
|
||||
if name.present? && email.present? && email.exclude?(',')
|
||||
"#{name} <#{email}>"
|
||||
%("#{name.delete('"')}" <#{email}>)
|
||||
else
|
||||
email
|
||||
end
|
||||
|
||||
+15
-14
@@ -4,19 +4,20 @@
|
||||
#
|
||||
# Table name: templates
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# fields :text not null
|
||||
# name :string not null
|
||||
# schema :text not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# author_id :bigint not null
|
||||
# folder_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# application_key :string
|
||||
# deleted_at :datetime
|
||||
# fields :text not null
|
||||
# name :string not null
|
||||
# schema :text not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# author_id :bigint not null
|
||||
# folder_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -32,7 +33,7 @@
|
||||
# fk_rails_... (folder_id => template_folders.id)
|
||||
#
|
||||
class Template < ApplicationRecord
|
||||
DEFAULT_SUBMITTER_NAME = 'First Submitter'
|
||||
DEFAULT_SUBMITTER_NAME = 'First Party'
|
||||
|
||||
belongs_to :author, class_name: 'User'
|
||||
belongs_to :account
|
||||
|
||||
+3
-1
@@ -52,8 +52,10 @@ class User < ApplicationRecord
|
||||
belongs_to :account
|
||||
has_one :access_token, dependent: :destroy
|
||||
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 :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
||||
has_many :email_messages, dependent: :destroy
|
||||
|
||||
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||
@@ -94,7 +96,7 @@ class User < ApplicationRecord
|
||||
|
||||
def friendly_name
|
||||
if full_name.present?
|
||||
"#{full_name} <#{email}>"
|
||||
%("#{full_name.delete('"')}" <#{email}>)
|
||||
else
|
||||
email
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"submitters": [
|
||||
{
|
||||
"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@') %>",
|
||||
"values": {
|
||||
"Form Text Field Name": "Default Value"
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% 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 %>
|
||||
<%= 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 %>
|
||||
<% end %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
@@ -20,7 +20,11 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% 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 %>
|
||||
<%= 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 %>
|
||||
<% end %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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>
|
||||
@@ -3,7 +3,9 @@
|
||||
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
||||
<%= render 'shared/title' %>
|
||||
</a>
|
||||
<%= render 'shared/github' %>
|
||||
<span class="hidden sm:inline">
|
||||
<%= render 'shared/github' %>
|
||||
</span>
|
||||
</div>
|
||||
<% if signed_in? %>
|
||||
<div class="space-x-4 flex items-center">
|
||||
@@ -16,13 +18,7 @@
|
||||
</span>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||
<% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
|
||||
<%= 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 %>
|
||||
<%= render 'shared/navbar_buttons' %>
|
||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -75,7 +71,8 @@
|
||||
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
|
||||
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
|
||||
<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>
|
||||
<% 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">
|
||||
<menu-active>
|
||||
<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>
|
||||
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
@@ -41,12 +42,12 @@
|
||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||
<li>
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
Plans
|
||||
<% end %>
|
||||
</li>
|
||||
<% unless Docuseal.multitenant? %>
|
||||
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) %>
|
||||
<li>
|
||||
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||
<% if can?(:read, AccessToken) %>
|
||||
@@ -68,12 +69,26 @@
|
||||
<%= render 'shared/settings_nav_extra' %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||
<li>
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
Console
|
||||
<%= 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
|
||||
<span class="badge badge-warning">New</span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||
<li>
|
||||
<%= 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 %>
|
||||
<% if Docuseal.multitenant? %> API <% else %> Console <% end %>
|
||||
<% end %>
|
||||
</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 %>
|
||||
</ul>
|
||||
</menu-active>
|
||||
<% if !can?(:manage, :tenants) %>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="alert">
|
||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||
<div>
|
||||
<p class="font-bold">Single Sign On with SAML 2.0</p>
|
||||
<p class="text-gray-700">
|
||||
Unlock with DocuSeal Enterprise
|
||||
<br>
|
||||
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold mb-4">SMS</h1>
|
||||
<%= render 'placeholder' %>
|
||||
</div>
|
||||
<div class="w-0 md:w-52"></div>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
<%= 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">
|
||||
<%= 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], required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= render 'shared/logo', width: 40, height: 40 %>
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="form-control">
|
||||
<%= 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]) %>
|
||||
<span class="label">Preserve submitters 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="label">Preserve order</span>
|
||||
<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') %>
|
||||
</span>
|
||||
<% 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']) %>
|
||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||
<% elsif field['type'] == 'file' %>
|
||||
<autosize-field></autosize-field>
|
||||
<div class="px-0.5 flex flex-col justify-center">
|
||||
<% Array.wrap(value).each do |val| %>
|
||||
<a target="_blank" href="<%= attachments_index[val].url %>">
|
||||
@@ -11,9 +12,16 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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'}") %>
|
||||
</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' %>
|
||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||
<div class="w-full flex items-center">
|
||||
@@ -24,10 +32,12 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif field['type'] == 'date' %>
|
||||
<autosize-field></autosize-field>
|
||||
<div class="flex items-center px-0.5">
|
||||
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
||||
</div>
|
||||
<% else %>
|
||||
<autosize-field></autosize-field>
|
||||
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</field-value>
|
||||
@@ -1,25 +1,27 @@
|
||||
<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">
|
||||
<a href="<%= template_path(@submission.template) %>" class="flex space-x-3 py-1">
|
||||
<span><%= render 'shared/logo', width: 40, height: 40 %></span>
|
||||
<span class="text-3xl font-semibold focus:text-clip"><%= @submission.template.name %></span>
|
||||
<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 'submissions/logo' %></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>
|
||||
<div class="space-x-3 flex items-center">
|
||||
<% if @submission.audit_trail.present? %>
|
||||
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
|
||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||
Audit Log
|
||||
<span class="hidden md:inline">
|
||||
Audit Log
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
<% 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">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||
<span>Download</span>
|
||||
<span class="hidden md:inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span>Downloading</span>
|
||||
<span class="hidden md:inline">Downloading</span>
|
||||
</span>
|
||||
</download-button>
|
||||
<% elsif @submission.submitters.to_a.size == 1 %>
|
||||
@@ -61,7 +63,7 @@
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% 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 %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,18 +111,18 @@
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||
<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>
|
||||
</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">
|
||||
<%= 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>
|
||||
<% 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: %>
|
||||
<% 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">
|
||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||
Submit Form
|
||||
@@ -160,7 +162,7 @@
|
||||
<% elsif field['type'] == 'checkbox' %>
|
||||
<%= svg_icon('check', class: 'w-6 h-6') %>
|
||||
<% 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 %>
|
||||
<%= Array.wrap(value).join(', ') %>
|
||||
<% end %>
|
||||
@@ -172,3 +174,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'scripts/autosize_field' %>
|
||||
@@ -1 +1,3 @@
|
||||
<%= render 'docuseal_logo' %>
|
||||
<div class="flex mt-4">
|
||||
<%= render 'docuseal_logo' %>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<% if @submitter.submission.template.submitters.size == 1 %>
|
||||
<div class="divider">OR</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>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
<div id="scrollbox">
|
||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||
<div class="mt-4 flex">
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<%# 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' %>
|
||||
<% (@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_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||
@@ -22,7 +21,7 @@
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% 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 !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
||||
@@ -42,3 +41,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'scripts/autosize_field' %>
|
||||
@@ -3,14 +3,10 @@
|
||||
<% else %>
|
||||
<p>Hi there,</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>
|
||||
<% @documents.each do |document| %>
|
||||
<ul>
|
||||
<li>
|
||||
<%= link_to document.filename.to_s, rails_blob_url(document) %>
|
||||
</li>
|
||||
</ul>
|
||||
<% end %>
|
||||
<p>Alternatively, you can review and download your copy using:</p>
|
||||
<p>
|
||||
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %>
|
||||
</p>
|
||||
<p>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
</a>
|
||||
</span>
|
||||
<% end %>
|
||||
<% if !template.deleted_at? && can?(:destroy, template) %>
|
||||
<span class="tooltip tooltip-left" data-tip="Archive">
|
||||
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
||||
<% if (!template.deleted_at? || !Docuseal.multitenant?) && can?(:destroy, template) %>
|
||||
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Remove' : '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 %>
|
||||
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
||||
<% end %>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
||||
<% end %>
|
||||
<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 Template Name', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="mt-3 mb-4 flex items-center justify-between">
|
||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
|
||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||
</a>
|
||||
<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">
|
||||
Change Folder
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="h-screen">
|
||||
<div
|
||||
class="text-center p-8 h-full flex items-center justify-center">
|
||||
<div>
|
||||
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %>
|
||||
<span>
|
||||
Processing...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<submit-form>
|
||||
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %>
|
||||
<button type="submit"></button>
|
||||
<input name="url" value="<%= params[:url] %>">
|
||||
<input name="filename" value="<%= params[:filename] %>">
|
||||
<% end %>
|
||||
</submit-form>
|
||||
@@ -22,9 +22,11 @@ module DocuSeal
|
||||
|
||||
config.active_storage.routes_prefix = ''
|
||||
|
||||
config.i18n.available_locales = %i[en en-US en-GB es-ES pt-PT de-DE]
|
||||
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE]
|
||||
config.i18n.fallbacks = [:en]
|
||||
|
||||
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
|
||||
|
||||
config.action_view.frozen_string_literal = true
|
||||
|
||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||
|
||||
@@ -24,5 +24,19 @@ if ENV['RAILS_ENV'] == 'production' && ENV['SECRET_KEY_BASE'].to_s.empty?
|
||||
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
|
||||
end
|
||||
|
||||
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
|
||||
require 'addressable'
|
||||
|
||||
url = Addressable::URI.parse(ENV.fetch('DATABASE_URL', ''))
|
||||
|
||||
ENV['DATABASE_HOST'] = url.host
|
||||
ENV['DATABASE_PORT'] = (url.port || 5432).to_s
|
||||
ENV['DATABASE_USER'] = url.user
|
||||
ENV['DATABASE_PASSWORD'] = url.password
|
||||
ENV['DATABASE_NAME'] = url.path.to_s.delete_prefix('/')
|
||||
|
||||
ENV.delete('DATABASE_URL')
|
||||
end
|
||||
|
||||
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
||||
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
|
||||
+8
-1
@@ -12,7 +12,14 @@ test:
|
||||
database: docuseal_test
|
||||
|
||||
production:
|
||||
<% if ENV['DATABASE_URL'].to_s.empty? %>
|
||||
<% if !ENV['DATABASE_HOST'].to_s.empty? %>
|
||||
<<: *default
|
||||
host: <%= ENV['DATABASE_HOST'] %>
|
||||
port: <%= ENV['DATABASE_PORT'] %>
|
||||
username: <%= ENV['DATABASE_USER'] %>
|
||||
password: <%= ENV['DATABASE_PASSWORD'] %>
|
||||
database: <%= ENV['DATABASE_NAME'] %>
|
||||
<% elsif ENV['DATABASE_URL'].to_s.empty? %>
|
||||
adapter: sqlite3
|
||||
database: <%= ENV['WORKDIR'] || '.' %>/db.sqlite3
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
|
||||
|
||||
@@ -59,7 +59,7 @@ Rails.application.configure do
|
||||
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
|
||||
|
||||
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
||||
config.force_ssl = ENV['FORCE_SSL'] == 'true'
|
||||
config.force_ssl = ENV['FORCE_SSL'].present?
|
||||
|
||||
# Include generic and useful information about system operation, but avoid logging too much
|
||||
# information to avoid inadvertent exposure of personally identifiable information (PII).
|
||||
|
||||
@@ -66,4 +66,16 @@ Rails.application.configure do
|
||||
deterministic_key: 'test deterministic key',
|
||||
key_derivation_salt: 'test key derivation salt'
|
||||
}
|
||||
|
||||
config.middleware.use(Class.new do
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
return [200, {}, []] if env['PATH_INFO'].starts_with?('/fonts')
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
|
||||
module HexaPDF
|
||||
module Type
|
||||
module AcroForm
|
||||
class Field
|
||||
def field_value
|
||||
''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2 +1,9 @@
|
||||
en:
|
||||
hello: "Hello world"
|
||||
en-US:
|
||||
date:
|
||||
formats:
|
||||
default: "%m/%d/%Y"
|
||||
|
||||
en-GB:
|
||||
date:
|
||||
formats:
|
||||
default: "%d/%m/%Y"
|
||||
+1
-1
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
|
||||
environment ENV.fetch('RAILS_ENV', 'development')
|
||||
|
||||
# Specifies the `pidfile` that Puma will use.
|
||||
# pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
|
||||
pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
|
||||
|
||||
# Specifies the number of `workers` to boot in clustered mode.
|
||||
# Workers are forked web server processes. If using threads and workers together
|
||||
|
||||
@@ -58,6 +58,9 @@ Rails.application.routes.draw do
|
||||
resources :submissions, only: %i[show destroy]
|
||||
resources :console_redirect, only: %i[index]
|
||||
resource :templates_upload, only: %i[create]
|
||||
authenticated do
|
||||
resource :templates_upload, only: %i[show], path: 'new'
|
||||
end
|
||||
resources :templates_archived, only: %i[index], path: 'archived'
|
||||
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
|
||||
resources :templates, only: %i[new create edit show destroy] do
|
||||
@@ -77,6 +80,8 @@ Rails.application.routes.draw do
|
||||
get :completed
|
||||
end
|
||||
|
||||
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug'
|
||||
|
||||
resources :send_submission_email, only: %i[create] do
|
||||
get :success, on: :collection
|
||||
end
|
||||
@@ -92,6 +97,7 @@ Rails.application.routes.draw do
|
||||
resources :storage, only: %i[index create], controller: 'storage_settings'
|
||||
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
||||
resources :sms, only: %i[index], controller: 'sms_settings'
|
||||
resources :sso, only: %i[index], controller: 'sso_settings'
|
||||
end
|
||||
resources :notifications, only: %i[index create], controller: 'notifications_settings'
|
||||
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
|
||||
|
||||
@@ -20,7 +20,7 @@ class AddFolderIdToTemplates < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
add_reference :templates, :folder, foreign_key: { to_table: :template_folders }, index: true, null: true
|
||||
|
||||
MigrationAccount.all.pluck(:id).each do |account_id|
|
||||
MigrationAccount.pluck(:id).each do |account_id|
|
||||
author_id = MigrationUser.where(account_id:).minimum(:id)
|
||||
|
||||
next if author_id.blank?
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddApplicationIdToSubmitters < ActiveRecord::Migration[7.0]
|
||||
class AddApplicationKeyToSubmitters < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :submitters, :application_key, :string
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddApplicationKeyToTemplates < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :templates, :application_key, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UpdateFieldOptions < ActiveRecord::Migration[7.0]
|
||||
class MigrationTemplate < ApplicationRecord
|
||||
self.table_name = 'templates'
|
||||
end
|
||||
|
||||
class MigrationSubmission < ApplicationRecord
|
||||
self.table_name = 'submissions'
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def up
|
||||
MigrationTemplate.find_each do |template|
|
||||
next if template.fields.blank?
|
||||
|
||||
template_fields = JSON.parse(template.fields)
|
||||
|
||||
new_fields = template_fields.deep_dup
|
||||
|
||||
new_fields.each do |field|
|
||||
if field['options'].present? && !field['options'].first.is_a?(Hash)
|
||||
field['options'] = field['options'].map { |o| { value: o || '', uuid: SecureRandom.uuid } }
|
||||
end
|
||||
end
|
||||
|
||||
template.update_columns(fields: new_fields.to_json) if template_fields != new_fields
|
||||
end
|
||||
|
||||
MigrationSubmission.find_each do |submission|
|
||||
next if submission.template_fields.blank?
|
||||
|
||||
template_fields = JSON.parse(submission.template_fields)
|
||||
|
||||
new_fields = template_fields.deep_dup
|
||||
|
||||
new_fields.each do |field|
|
||||
if field['options'].present? && !field['options'].first.is_a?(Hash)
|
||||
field['options'] = field['options'].map { |o| { value: o || '', uuid: SecureRandom.uuid } }
|
||||
end
|
||||
end
|
||||
|
||||
submission.update_columns(template_fields: new_fields.to_json) if template_fields != new_fields
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def down
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddSlugToSubmissions < ActiveRecord::Migration[7.0]
|
||||
class MigrationSubmission < ApplicationRecord
|
||||
self.table_name = 'submissions'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :submissions, :slug, :string
|
||||
|
||||
MigrationSubmission.where(slug: nil).find_each do |submission|
|
||||
submission.update_columns(slug: SecureRandom.base58(14))
|
||||
end
|
||||
|
||||
change_column_null :submissions, :slug, false
|
||||
|
||||
add_index :submissions, :slug, unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :submissions, :slug
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddPreferencesToSubmitters < ActiveRecord::Migration[7.0]
|
||||
class MigrationSubmitter < ApplicationRecord
|
||||
self.table_name = 'submitters'
|
||||
end
|
||||
|
||||
def change
|
||||
add_column :submitters, :preferences, :text
|
||||
|
||||
MigrationSubmitter.where(preferences: nil).update_all(preferences: '{}')
|
||||
|
||||
change_column_null :submitters, :preferences, false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddPreferencesToSubmissions < ActiveRecord::Migration[7.0]
|
||||
class MigrationSubmission < ApplicationRecord
|
||||
self.table_name = 'submissions'
|
||||
end
|
||||
|
||||
def change
|
||||
add_column :submissions, :preferences, :text
|
||||
|
||||
MigrationSubmission.where(preferences: nil).update_all(preferences: '{}')
|
||||
|
||||
change_column_null :submissions, :preferences, false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateEmailMessages < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :email_messages do |t|
|
||||
t.string :uuid, null: false, index: true
|
||||
t.references :author, null: false, foreign_key: { to_table: :users }, index: false
|
||||
t.references :account, null: false, foreign_key: true, index: true
|
||||
t.text :subject, null: false
|
||||
t.text :body, null: false
|
||||
t.string :sha1, null: false, index: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
+22
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -81,6 +81,20 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
|
||||
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
|
||||
end
|
||||
|
||||
create_table "email_messages", force: :cascade do |t|
|
||||
t.string "uuid", null: false
|
||||
t.bigint "author_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.text "subject", null: false
|
||||
t.text "body", null: false
|
||||
t.string "sha1", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_email_messages_on_account_id"
|
||||
t.index ["sha1"], name: "index_email_messages_on_sha1"
|
||||
t.index ["uuid"], name: "index_email_messages_on_uuid"
|
||||
end
|
||||
|
||||
create_table "encrypted_configs", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.string "key", null: false
|
||||
@@ -124,7 +138,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
|
||||
t.text "template_submitters"
|
||||
t.text "source", null: false
|
||||
t.string "submitters_order", null: false
|
||||
t.string "slug", null: false
|
||||
t.text "preferences", null: false
|
||||
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||
t.index ["slug"], name: "index_submissions_on_slug", unique: true
|
||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||
end
|
||||
|
||||
@@ -144,6 +161,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
|
||||
t.string "name"
|
||||
t.string "phone"
|
||||
t.string "application_key"
|
||||
t.text "preferences", null: false
|
||||
t.index ["email"], name: "index_submitters_on_email"
|
||||
t.index ["slug"], name: "index_submitters_on_slug", unique: true
|
||||
t.index ["submission_id"], name: "index_submitters_on_submission_id"
|
||||
@@ -173,6 +191,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "source", null: false
|
||||
t.bigint "folder_id", null: false
|
||||
t.string "application_key"
|
||||
t.index ["account_id"], name: "index_templates_on_account_id"
|
||||
t.index ["author_id"], name: "index_templates_on_author_id"
|
||||
t.index ["folder_id"], name: "index_templates_on_folder_id"
|
||||
@@ -226,6 +245,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "document_generation_events", "submitters"
|
||||
add_foreign_key "email_messages", "accounts"
|
||||
add_foreign_key "email_messages", "users", column: "author_id"
|
||||
add_foreign_key "encrypted_configs", "accounts"
|
||||
add_foreign_key "encrypted_user_configs", "users"
|
||||
add_foreign_key "submission_events", "submissions"
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ services:
|
||||
volumes:
|
||||
- .:/data
|
||||
environment:
|
||||
FORCE_SSL: 'true'
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/docuseal
|
||||
- FORCE_SSL=${HOST}
|
||||
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user