Compare commits

...
70 Commits
Author SHA1 Message Date
DocuSeal df561a6211 update rubocop 2023-10-30 00:56:44 +02:00
DocuSeal 50d3404f3b force ssl only if host passed 2023-10-29 21:36:30 +02:00
DocuSeal 607fc8d338 allow to permanently remove templates 2023-10-29 21:36:30 +02:00
DocuSeal 8c366dfc38 use account timezone in audit trail 2023-10-29 21:36:30 +02:00
DocuSeal 5ed08fa9ab add sso settings placeholder 2023-10-29 21:36:30 +02:00
DocuSeal ddb557ebef upload template from URL 2023-10-29 21:36:30 +02:00
DocuSeal b48615df3f use handwritten font for typed signature 2023-10-29 21:36:29 +02:00
DocuSeal 86c7b92675 apply previous signature and initials on the next field 2023-10-28 01:56:22 +03:00
DocuSeal 041f362cc4 do not allow to edit phone if already specified 2023-10-28 00:53:16 +03:00
DocuSeal 65f491325e adjust configs loading 2023-10-28 00:42:33 +03:00
DocuSeal 57aef5f5e5 refactor date step 2023-10-26 21:26:08 +03:00
Alex Turchyn e704e6fa04 show edit icon on hover the outer box 2023-10-25 20:09:34 +03:00
DocuSeal 4c99bbb58a add onComplete form callback 2023-10-25 02:06:47 +03:00
DocuSeal c0647eef2c process 15 pages after pdf upload 2023-10-24 23:48:26 +03:00
DocuSeal c619b9be93 move plans link 2023-10-24 23:16:02 +03:00
DocuSeal abab3f9688 do not use libvips pdf on large files 2023-10-24 22:31:09 +03:00
DocuSeal 7b15111e8f use temp file for puma pid 2023-10-24 13:36:50 +03:00
DocuSeal 2c7f38c503 optimize mailer attachments 2023-10-24 12:41:12 +03:00
Alex Turchyn ed80df1372 fix audit trail images generation 2023-10-24 00:46:47 +03:00
DocuSeal 3614d1796c do not split signature pad js in form 2023-10-22 23:26:43 +03:00
DocuSeal ce39494f84 optimize for large files 2023-10-22 22:33:54 +03:00
DocuSeal c639a3f733 do not sign in after reset password if 2fa
https://github.com/devise-two-factor/devise-two-factor#disabling-automatic-login-after-password-resets
thanks Greg Molnar
2023-10-22 13:19:47 +03:00
DocuSeal 74bef133b3 add enqueries checkbox 2023-10-21 15:33:10 +03:00
DocuSeal 33142966dc allow to resubmit form via shared link 2023-10-21 13:06:59 +03:00
DocuSeal 42ec8d5fc9 do not allow to skip required fields 2023-10-21 12:09:50 +03:00
DocuSeal 01d752e811 fix webhook serializer 2023-10-21 12:09:37 +03:00
DocuSeal 99b33d4165 allow to resubmit form via shared link 2023-10-20 22:55:25 +03:00
DocuSeal 11a90d797a add user encrypted configs 2023-10-20 13:16:39 +03:00
DocuSeal 9da7463657 add submission status filters 2023-10-20 00:13:17 +03:00
Alex Turchyn b6d350ebbd fix no error messages when sign in with wrong credentials 2023-10-18 22:56:28 +03:00
DocuSeal 9e87a0373f add application key to submitters 2023-10-18 02:14:44 +03:00
DocuSeal aee55165c5 use start page 2023-10-16 21:21:04 +03:00
DocuSeal 9ddbcae153 fix plans link 2023-10-16 20:19:39 +03:00
DocuSeal 10cd3aa72a smtp ssl verify mode none 2023-10-16 20:19:28 +03:00
DocuSeal 6684a27e2f preserve textarea in the form 2023-10-16 02:04:52 +03:00
DocuSeal b6f7a7dd15 fix initials step 2023-10-16 01:40:17 +03:00
DocuSeal 309e43e18e fix set today date timezone 2023-10-16 01:28:04 +03:00
DocuSeal d5b8640a0e force redraw form steps 2023-10-16 00:51:08 +03:00
DocuSeal 49b9064d9f add ability to toggle textarea 2023-10-16 00:41:14 +03:00
DocuSeal 698031cfc0 add current date button 2023-10-15 22:59:32 +03:00
DocuSeal 6dc21e66e1 return to required fields 2023-10-15 21:41:47 +03:00
DocuSeal 60b32fec94 adjust sign up header buttons 2023-10-15 01:03:29 +03:00
DocuSeal c621166b45 use full name form company name if missing 2023-10-14 22:31:32 +03:00
DocuSeal 8e2da83f9e adjust demo 2023-10-14 21:19:21 +03:00
DocuSeal a39ba5b275 add plausible 2023-10-14 16:00:56 +03:00
DocuSeal edf466a6de add plans link 2023-10-14 15:42:50 +03:00
DocuSeal 3b8bb8c5bb fix api when fields without name 2023-10-13 14:17:28 +03:00
DocuSeal 79f1061353 improve signup form links 2023-10-12 22:59:15 +03:00
Alex Turchyn 745f2160c5 update the wording of emails 2023-10-12 22:42:14 +03:00
DocuSeal 2f0c79444b add fields configs param to submissions api 2023-10-12 22:13:46 +03:00
DocuSeal 69a3782903 add readonly fields via api 2023-10-12 17:23:59 +03:00
DocuSeal 3dd7c47558 refactor result generation 2023-10-11 21:05:42 +03:00
DocuSeal 761bb07ee6 fix migration for mysql 2023-10-11 21:05:29 +03:00
Alex Turchyn 92d7bc65f5 replace 'log in' with 'sign in' 2023-10-11 01:11:36 +03:00
DocuSeal 79e158be43 add more icons 2023-10-08 23:22:27 +03:00
Alex Turchyn 41ee2dba6a increase the maximum number of submitters to 10 2023-10-08 17:05:35 +00:00
DocuSeal 8e1354175f add extra settings 2023-10-08 19:33:01 +03:00
DocuSeal 2474adffc2 fix builder color 2023-10-08 19:33:01 +03:00
DocuSeal b8fd5a77d4 adjust cancan permissions 2023-10-08 19:33:01 +03:00
DocuSeal 18bb57aa99 ensure user role is in the list 2023-10-08 19:29:09 +03:00
Alex Turchyn 17ff193e34 use company name 2023-10-08 18:07:16 +03:00
Alex Turchyn 20ac63e524 remove 'Tell about us' 2023-10-08 16:49:09 +03:00
DocuSeal 518bbe3d1c do not allow to submit archived forms 2023-10-07 14:30:12 +03:00
DocuSeal fa5e3e7163 add allow to skip form prop 2023-10-07 10:23:11 +03:00
DocuSeal 2b8280168e move custom completted button to the top 2023-10-07 09:06:09 +03:00
DocuSeal 82854b8ce8 allow to make checkboxes required 2023-10-07 09:04:02 +03:00
Alex Turchyn 276b4f4a5f update the meeting link 2023-10-07 01:56:32 +03:00
DocuSeal b73a730390 update builder hints 2023-10-07 01:55:41 +03:00
DocuSeal 2e98885f41 make builder usable on mobile 2023-10-06 10:46:25 +03:00
DocuSeal bfece574b9 fix audit trail with document images 2023-10-03 02:14:27 +03:00
141 changed files with 1753 additions and 493 deletions

No files matched your search

+6
View File
@@ -67,8 +67,14 @@ RSpec/MultipleMemoizedHelpers:
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
Rails/FindEach:
Enabled: false
Rails/SkipsModelValidations: Rails/SkipsModelValidations:
Enabled: false Enabled: false
Rails/ApplicationController: Rails/ApplicationController:
Enabled: false Enabled: false
Capybara/ClickLinkOrButtonStyle:
Enabled: false
+2 -1
View File
@@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test"
WORKDIR /app WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler 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 ./ COPY ./Gemfile ./Gemfile.lock ./
@@ -49,6 +49,7 @@ COPY LICENSE README.md Rakefile config.ru ./
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
RUN bundle exec bootsnap precompile --gemfile app/ lib/ RUN bundle exec bootsnap precompile --gemfile app/ lib/
WORKDIR /data/docuseal WORKDIR /data/docuseal
+13 -15
View File
@@ -98,7 +98,6 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1) faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1)
bcrypt (3.1.19) bcrypt (3.1.19)
better_html (2.0.2) better_html (2.0.2)
actionview (>= 6.0) actionview (>= 6.0)
@@ -294,7 +293,7 @@ GEM
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.4)
minitest (5.19.0) minitest (5.20.0)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
@@ -345,7 +344,7 @@ GEM
os (1.1.4) os (1.1.4)
pagy (6.0.4) pagy (6.0.4)
parallel (1.23.0) parallel (1.23.0)
parser (3.2.2.3) parser (3.2.2.4)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.11.0) pdf-reader (2.11.0)
@@ -418,7 +417,7 @@ GEM
rake (13.0.6) rake (13.0.6)
redis-client (0.16.0) redis-client (0.16.0)
connection_pool connection_pool
regexp_parser (2.8.1) regexp_parser (2.8.2)
reline (0.3.8) reline (0.3.8)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
@@ -455,33 +454,32 @@ GEM
rspec-mocks (~> 3.12) rspec-mocks (~> 3.12)
rspec-support (~> 3.12) rspec-support (~> 3.12)
rspec-support (3.12.1) rspec-support (3.12.1)
rubocop (1.56.1) rubocop (1.57.2)
base64 (~> 0.1.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.3) parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0) rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.30.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.18.0) rubocop-capybara (2.19.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.23.1) rubocop-factory_bot (2.24.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.19.0) rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2) rubocop-rails (2.22.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.23.2) rubocop-rspec (2.25.0)
rubocop (~> 1.33) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
@@ -537,7 +535,7 @@ GEM
tzinfo-data (1.2023.3) tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.4.2) unicode-display_width (2.5.0)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
version_gem (1.1.3) version_gem (1.1.3)
warden (1.2.9) warden (1.2.9)
+2 -2
View File
@@ -81,9 +81,9 @@ HOST=your-domain-name.com docker-compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[![Book an Integration Demo](https://cal.com/book-with-cal-dark.svg)](https://cal.com/docuseal) [Book a Meeting](https://calendly.com/kriti-docuseal/30min)
## License ## License
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information. Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 Oleksandr Turchyn. Unless otherwise noted, all files © 2023 DocuSeal LLC.
@@ -106,7 +106,9 @@ module Api
def submissions_params def submissions_params
params.permit(submission: [{ params.permit(submission: [{
submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]] submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key,
{ values: {}, readonly_fields: [],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
}]) }])
end end
@@ -115,11 +117,15 @@ module Api
Array.wrap(submissions_params).each do |submission| Array.wrap(submissions_params).each do |submission|
submission[:submitters].each_with_index do |submitter, index| submission[:submitters].each_with_index do |submitter, index|
next if submitter[:values].blank? default_values = submitter[:values] || {}
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
next if default_values.blank?
values, new_attachments = values, new_attachments =
Submitters::NormalizeValues.call(template, Submitters::NormalizeValues.call(template,
submitter[:values], default_values,
submitter[:role] || template.submitters[index]['name']) submitter[:role] || template.submitters[index]['name'])
attachments.push(*new_attachments) attachments.push(*new_attachments)
@@ -4,6 +4,27 @@ module Api
class SubmittersController < ApiBaseController class SubmittersController < ApiBaseController
load_and_authorize_resource :submitter load_and_authorize_resource :submitter
def index
submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = paginate(
submitters.preload(:template, :submission, :submission_events,
documents_attachments: :blob, attachments_attachments: :blob)
)
render json: {
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
pagination: {
count: submitters.size,
next: submitters.last&.id,
prev: submitters.first&.id
}
}
end
def show def show
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at? Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
@@ -16,6 +16,7 @@ module Api
render json: { render json: {
schema:, schema:,
documents: documents.as_json( documents: documents.as_json(
methods: [:metadata],
include: { include: {
preview_images: { methods: %i[url metadata filename] } preview_images: { methods: %i[url metadata filename] }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base
private private
def sign_in_for_demo def sign_in_for_demo
sign_in(User.order('random()').take) unless signed_in? sign_in(User.active.order('random()').take) unless signed_in?
end end
def current_account def current_account
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class EnquiriesController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
if params[:talk_to_sales] == 'on'
Faraday.post(Docuseal::ENQUIRIES_URL,
enquiry_params.merge(type: :talk_to_sales).to_json,
'Content-Type' => 'application/json')
end
head :ok
end
private
def enquiry_params
params.require(:user).permit(:email)
end
end
+2 -2
View File
@@ -10,7 +10,7 @@ class MfaSetupController < ApplicationController
current_user.save! current_user.save!
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME) @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
end end
def edit; end def edit; end
@@ -22,7 +22,7 @@ class MfaSetupController < ApplicationController
redirect_to settings_profile_index_path, notice: '2FA has been configured' redirect_to settings_profile_index_path, notice: '2FA has been configured'
else else
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME) @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = 'Code is invalid' @error_message = 'Code is invalid'
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class PasswordsController < Devise::PasswordsController
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
def update
super do |resource|
Current.user = resource
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
class PreviewDocumentPageController < ActionController::API
include ActiveStorage::SetCurrent
FORMAT = Templates::ProcessDocument::FORMAT
def show
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
return head :not_found unless attachment
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
return redirect_to preview_image.url, allow_other_host: true if preview_image
file_path =
if attachment.service.name == :disk
ActiveStorage::Blob.service.path_for(attachment.key)
else
find_or_create_document_tempfile_path(attachment)
end
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
render plain: io.tap(&:rewind).read
end
def find_or_create_document_tempfile_path(attachment)
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
f.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
if f.size.zero?
f.binmode
f.write(attachment.download)
end
# rubocop:enable Style/ZeroLengthPredicate
end
file_path
end
end
+4 -4
View File
@@ -31,10 +31,10 @@ class RegistrationsController < Devise::RegistrationsController
redirect_to after_sign_up_path_for(current_user), allow_other_host: true redirect_to after_sign_up_path_for(current_user), allow_other_host: true
end end
def require_no_authentication def set_flash_message(key, kind, options = {})
super return if key == :alert && kind == 'already_authenticated'
flash.clear super
end end
def build_resource(_hash = {}) def build_resource(_hash = {})
@@ -43,7 +43,7 @@ class RegistrationsController < Devise::RegistrationsController
self.resource = account.users.new(user_params) self.resource = account.users.new(user_params)
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create' account.name ||= resource.full_name if params[:action] == 'create'
end end
def user_params def user_params
+3 -3
View File
@@ -32,9 +32,9 @@ class SessionsController < Devise::SessionsController
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end end
def require_no_authentication def set_flash_message(key, kind, options = {})
super return if key == :alert && kind == 'already_authenticated'
flash.clear super
end end
end end
@@ -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
+3 -2
View File
@@ -14,7 +14,8 @@ class StartFormController < ApplicationController
def update def update
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil)) @submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
.find_or_initialize_by(email: submitter_params[:email]) .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
@@ -46,7 +47,7 @@ class StartFormController < ApplicationController
private private
def submitter_params def submitter_params
params.require(:submitter).permit(:email).tap do |attrs| params.require(:submitter).permit(:email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
end end
end end
+13 -1
View File
@@ -6,12 +6,24 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
PRELOAD_ALL_PAGES_AMOUNT = 200
def show def show
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@submission], records: [@submission],
associations: [:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }] associations: [:template, { template_schema_documents: :blob }]
).call ).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 :show, layout: 'plain' render :show, layout: 'plain'
end end
+20 -5
View File
@@ -6,18 +6,33 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
PRELOAD_ALL_PAGES_AMOUNT = 200
def show def show
@submitter = @submitter = Submitter.find_by!(slug: params[:slug])
Submitter.preload(submission: [
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
])
.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
ActiveRecord::Associations::Preloader.new(
records: [@submitter],
associations: [submission: [:template, { template_schema_documents: :blob }]]
).call
total_pages =
@submitter.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: @submitter.submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
cookies[:submitter_sid] = @submitter.signed_id cookies[:submitter_sid] = @submitter.signed_id
render @submitter.submission.template.deleted_at? ? :archived : :show
end end
def update def update
+24 -2
View File
@@ -10,6 +10,11 @@ class TemplatesController < ApplicationController
submissions = submissions.active if @template.deleted_at.blank? submissions = submissions.active if @template.deleted_at.blank?
submissions = Submissions.search(submissions, params[:q]) submissions = Submissions.search(submissions, params[:q])
@base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc)) @pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
@@ -25,6 +30,14 @@ class TemplatesController < ApplicationController
associations: [schema_documents: { preview_images_attachments: :blob }] associations: [schema_documents: { preview_images_attachments: :blob }]
).call ).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: [:metadata],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :edit, layout: 'plain' render :edit, layout: 'plain'
end end
@@ -44,9 +57,18 @@ class TemplatesController < ApplicationController
end end
def destroy 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 end
private private
@@ -3,20 +3,55 @@
class TemplatesUploadsController < ApplicationController class TemplatesUploadsController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
layout 'plain'
def show; end
def create def create
url_params = create_file_params_from_url if params[:url].present?
@template.account = current_account @template.account = current_account
@template.author = current_user @template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) @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! @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 } } schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
@template.update!(schema:) @template.update!(schema:)
redirect_to edit_template_path(@template) 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(params[:url]).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
end end
+4 -1
View File
@@ -27,7 +27,10 @@ class UsersController < ApplicationController
def update def update
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo? return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
if @user.update(user_params.compact_blank.except(current_user == @user ? :role : nil)) attrs = user_params.compact_blank
attrs.delete(:role) if User::ROLES.exclude?(attrs[:role])
if @user.update(attrs.except(current_user == @user ? :role : nil))
redirect_to settings_users_path, notice: 'User has been updated' redirect_to settings_users_path, notice: 'User has been updated'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -12,7 +12,7 @@ class VerifyPdfSignatureController < ApplicationController
cert_data = if Docuseal.multitenant? cert_data = if Docuseal.multitenant?
Docuseal::CERTS Docuseal::CERTS
else else
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {} EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end end
default_pkcs = GenerateCertificate.load_pkcs(cert_data) default_pkcs = GenerateCertificate.load_pkcs(cert_data)
+3 -1
View File
@@ -18,6 +18,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
import SubmittersAutocomplete from './elements/submitter_autocomplete' import SubmittersAutocomplete from './elements/submitter_autocomplete'
import FolderAutocomplete from './elements/folder_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form' import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form'
import * as TurboInstantClick from './lib/turbo_instant_click' 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('submitters-autocomplete', SubmittersAutocomplete)
window.customElements.define('folder-autocomplete', FolderAutocomplete) window.customElements.define('folder-autocomplete', FolderAutocomplete)
window.customElements.define('signature-form', SignatureForm) window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => { document.addEventListener('turbo:submit-end', async (event) => {
@@ -76,12 +78,12 @@ document.addEventListener('turbo:submit-end', async (event) => {
window.customElements.define('template-builder', class extends HTMLElement { window.customElements.define('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
this.appElem.classList.add('max-h-screen')
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template: reactive(JSON.parse(this.dataset.template)),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false',
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true' isDirectUpload: this.dataset.isDirectUpload === 'true'
}) })
+5
View File
@@ -0,0 +1,5 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelector('form').requestSubmit()
}
}
+4
View File
@@ -47,6 +47,10 @@ select:required:invalid {
@apply border-base-content/20; @apply border-base-content/20;
} }
.base-textarea {
@apply textarea textarea-bordered bg-white rounded-3xl;
}
.btn { .btn {
@apply no-animation; @apply no-animation;
} }
+4 -3
View File
@@ -110,9 +110,10 @@
<span v-else-if="field.type === 'date'"> <span v-else-if="field.type === 'date'">
{{ formattedDate }} {{ formattedDate }}
</span> </span>
<span v-else> <span
{{ modelValue }} v-else
</span> class="whitespace-pre-wrap"
>{{ modelValue }}</span>
</div> </div>
</div> </div>
</template> </template>
+10 -10
View File
@@ -11,6 +11,15 @@
</span> </span>
</p> </p>
<div class="space-y-3 mt-5"> <div class="space-y-3 mt-5">
<a
v-if="completedButton.url"
:href="completedButton.url"
class="white-button flex items-center w-full"
>
<span>
{{ completedButton.title || 'Back to Website' }}
</span>
</a>
<button <button
v-if="canSendEmail && !isDemo" v-if="canSendEmail && !isDemo"
class="white-button !h-auto flex items-center space-x-1 w-full" class="white-button !h-auto flex items-center space-x-1 w-full"
@@ -52,15 +61,6 @@
Star on Github Star on Github
</span> </span>
</a> </a>
<a
v-if="completedButton.url"
:href="completedButton.url"
class="white-button flex items-center space-x-1 w-full"
>
<span>
{{ completedButton.title || 'Back to Website' }}
</span>
</a>
<a <a
v-if="isDemo" v-if="isDemo"
href="https://docuseal.co/sign_up" href="https://docuseal.co/sign_up"
@@ -78,7 +78,7 @@
> >
{{ t('signed_with') }} {{ t('signed_with') }}
<a <a
href="https://www.docuseal.co" href="https://www.docuseal.co/start"
target="_blank" target="_blank"
class="underline" class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }} >DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -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>
+92 -55
View File
@@ -58,54 +58,20 @@
> >
<div class="md:mt-4"> <div class="md:mt-4">
<div v-if="['cells', 'text'].includes(currentField.type)"> <div v-if="['cells', 'text'].includes(currentField.type)">
<label <TextStep
v-if="currentField.name" :key="currentField.uuid"
:for="currentField.uuid" v-model="values[currentField.uuid]"
class="label text-2xl mb-2" :field="currentField"
>{{ currentField.name }} @focus="$refs.areas.scrollIntoField(currentField)"
<template v-if="!currentField.required">({{ t('optional') }})</template>
</label>
<div
v-else
class="py-1"
/> />
<div>
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl w-full"
:required="currentField.required"
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
type="text"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
<div v-else-if="currentField.type === 'date'">
<label
v-if="currentField.name"
:for="currentField.uuid"
class="label text-2xl mb-2"
>{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</label>
<div
v-else
class="py-1"
/>
<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> </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'"> <div v-else-if="currentField.type === 'select'">
<label <label
v-if="currentField.name" v-if="currentField.name"
@@ -179,6 +145,7 @@
</div> </div>
<MultiSelectStep <MultiSelectStep
v-else-if="currentField.type === 'multiple'" v-else-if="currentField.type === 'multiple'"
:key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
/> />
@@ -224,6 +191,9 @@
:id="field.uuid" :id="field.uuid"
type="checkbox" type="checkbox"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required"
:checked="!!values[field.uuid]" :checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]" @click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
> >
@@ -237,6 +207,7 @@
</div> </div>
<ImageStep <ImageStep
v-else-if="currentField.type === 'image'" v-else-if="currentField.type === 'image'"
:key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -247,8 +218,10 @@
<SignatureStep <SignatureStep
v-else-if="currentField.type === 'signature'" v-else-if="currentField.type === 'signature'"
ref="currentStep" ref="currentStep"
:key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:previous-value="previousSignatureValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -259,8 +232,10 @@
<InitialsStep <InitialsStep
v-else-if="currentField.type === 'initials'" v-else-if="currentField.type === 'initials'"
ref="currentStep" ref="currentStep"
:key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:previous-value="previousInitialsValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -271,6 +246,7 @@
/> />
<AttachmentStep <AttachmentStep
v-else-if="currentField.type === 'file'" v-else-if="currentField.type === 'file'"
:key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:field="currentField" :field="currentField"
@@ -281,6 +257,7 @@
<PhoneStep <PhoneStep
v-else-if="currentField.type === 'phone'" v-else-if="currentField.type === 'phone'"
ref="currentStep" ref="currentStep"
:key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:default-value="submitter.phone" :default-value="submitter.phone"
@@ -291,6 +268,7 @@
</div> </div>
<div class="mt-6 md:mt-8"> <div class="mt-6 md:mt-8">
<button <button
ref="submitButton"
type="submit" type="submit"
class="base-button w-full flex justify-center" class="base-button w-full flex justify-center"
:disabled="isButtonDisabled" :disabled="isButtonDisabled"
@@ -311,6 +289,12 @@
><span>...</span></span> ><span>...</span></span>
</span> </span>
</button> </button>
<div
v-if="showFillAllRequiredFields"
class="text-center mt-1"
>
{{ t('please_fill_all_required_fields') }}
</div>
</div> </div>
</form> </form>
<FormCompleted <FormCompleted
@@ -329,7 +313,7 @@
:key="step[0].uuid" :key="step[0].uuid"
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1" class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }" :class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]" @click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
/> />
</div> </div>
@@ -346,6 +330,8 @@ import InitialsStep from './initials_step'
import AttachmentStep from './attachment_step' import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
import TextStep from './text_step'
import DateStep from './date_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { t } from './i18n' import { t } from './i18n'
@@ -360,7 +346,9 @@ export default {
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
DateStep,
IconArrowsDiagonal, IconArrowsDiagonal,
TextStep,
PhoneStep, PhoneStep,
IconArrowsDiagonalMinimize2, IconArrowsDiagonalMinimize2,
FormCompleted FormCompleted
@@ -386,6 +374,13 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
onComplete: {
type: Function,
required: false,
default () {
return () => {}
}
},
withConfetti: { withConfetti: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -416,6 +411,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
allowToSkip: {
type: Boolean,
required: false,
default: false
},
goToLast: { goToLast: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -446,6 +446,7 @@ export default {
return { return {
isCompleted: false, isCompleted: false,
isFormVisible: true, isFormVisible: true,
showFillAllRequiredFields: false,
currentStep: 0, currentStep: 0,
isSubmitting: false, isSubmitting: false,
submittedValues: {}, submittedValues: {},
@@ -459,6 +460,16 @@ export default {
submitterSlug () { submitterSlug () {
return this.submitter.slug 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 () { isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4 return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
}, },
@@ -476,7 +487,7 @@ export default {
return this.currentStepFields[0] return this.currentStepFields[0]
}, },
stepFields () { stepFields () {
return this.fields.reduce((acc, f) => { return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') { if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
@@ -503,10 +514,20 @@ export default {
this.submittedValues = JSON.parse(JSON.stringify(this.values)) this.submittedValues = JSON.parse(JSON.stringify(this.values))
if (this.goToLast) { if (this.goToLast) {
this.currentStep = Math.min( const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.values[f.uuid])))
this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1, const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1
this.stepFields.length - 1
) const indexesList = [this.stepFields.length - 1]
if (requiredEmptyStepIndex !== -1) {
indexesList.push(requiredEmptyStepIndex)
}
if (lastFilledStepIndex !== -1) {
indexesList.push(lastFilledStepIndex)
}
this.currentStep = Math.min(...indexesList)
} }
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
@@ -597,6 +618,7 @@ export default {
}, },
goToStep (step, scrollToArea = false, clickUpload = false) { goToStep (step, scrollToArea = false, clickUpload = false) {
this.currentStep = this.stepFields.indexOf(step) this.currentStep = this.stepFields.indexOf(step)
this.showFillAllRequiredFields = false
this.$nextTick(() => { this.$nextTick(() => {
this.recalculateButtonDisabledKey = Math.random() this.recalculateButtonDisabledKey = Math.random()
@@ -631,12 +653,13 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
return index < this.currentStep && fields[0].required && fields[0].type === 'phone' && !this.submittedValues[fields[0].uuid] return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid]
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = this.currentStep === this.stepFields.length - 1
if (this.currentStep === this.stepFields.length - 1 && !emptyRequiredField) { if (isLastStep && !emptyRequiredField) {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
@@ -651,12 +674,26 @@ export default {
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid] this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
const nextStep = emptyRequiredField || this.stepFields[this.currentStep + 1] if (isLastStep) {
this.isSecondWalkthrough = true
}
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
if (nextStep) { if (nextStep) {
this.goToStep(nextStep, true) this.goToStep(nextStep, true)
if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true
}
} else { } else {
this.isCompleted = true this.isCompleted = true
const respData = await response.text()
if (respData) {
this.onComplete(JSON.parse(respData))
}
} }
}).catch(error => { }).catch(error => {
console.error('Error submitting form:', error) console.error('Error submitting form:', error)
+45
View File
@@ -20,6 +20,7 @@ const en = {
form_has_been_completed: 'Form has been completed!', form_has_been_completed: 'Form has been completed!',
create_a_free_account: 'Create a Free Account', create_a_free_account: 'Create a Free Account',
signed_with: 'Signed with', signed_with: 'Signed with',
please_check_the_box_to_continue: 'Please check the box to continue',
open_source_documents_software: 'open source documents software', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use internatioanl format: +1xxx', use_international_format: 'Use internatioanl format: +1xxx',
@@ -28,6 +29,10 @@ const en = {
sending: 'Sending...', sending: 'Sending...',
resend_code: 'Re-send code', resend_code: 'Re-send code',
verification_code_has_been_resent: 'Verification code has been re-sent via SMS', verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
please_fill_all_required_fields: 'Please fill all required fields',
set_today: 'Set Today',
toggle_multiline_text: 'Toggle Multiline Text',
date: 'Date',
email_has_been_sent: 'Email has been sent' email_has_been_sent: 'Email has been sent'
} }
@@ -53,6 +58,7 @@ const es = {
form_has_been_completed: '¡El formulario ha sido completado!', form_has_been_completed: '¡El formulario ha sido completado!',
create_a_free_account: 'Crear una Cuenta Gratuita', create_a_free_account: 'Crear una Cuenta Gratuita',
signed_with: 'Firmado con', signed_with: 'Firmado con',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
open_source_documents_software: 'software de documentos de código abierto', open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono', verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx', use_international_format: 'Usar formato internacional: +1xxx',
@@ -61,6 +67,10 @@ const es = {
sending: 'Enviando...', sending: 'Enviando...',
resend_code: 'Reenviar código', resend_code: 'Reenviar código',
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS', verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
set_today: 'Establecer Hoy',
date: 'Fecha',
toggle_multiline_text: 'Alternar Texto Multilínea',
email_has_been_sent: 'El correo electrónico ha sido enviado' email_has_been_sent: 'El correo electrónico ha sido enviado'
} }
@@ -86,6 +96,7 @@ const it = {
form_has_been_completed: 'Il modulo è stato completato!', form_has_been_completed: 'Il modulo è stato completato!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
signed_with: 'Firmato con', signed_with: 'Firmato con',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
open_source_documents_software: 'software di documenti open source', open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono', verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx', use_international_format: 'Usa formato internazionale: +1xxx',
@@ -94,6 +105,10 @@ const it = {
sending: 'Invio in corso...', sending: 'Invio in corso...',
resend_code: 'Rinvia codice', resend_code: 'Rinvia codice',
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS', verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
set_today: 'Imposta Oggi',
date: 'Data',
toggle_multiline_text: 'Attiva Testo Multilinea',
email_has_been_sent: "L'email è stata inviata" email_has_been_sent: "L'email è stata inviata"
} }
@@ -119,6 +134,7 @@ const de = {
form_has_been_completed: 'Formular wurde ausgefüllt!', form_has_been_completed: 'Formular wurde ausgefüllt!',
create_a_free_account: 'Kostenloses Konto erstellen', create_a_free_account: 'Kostenloses Konto erstellen',
signed_with: 'Unterschrieben mit', signed_with: 'Unterschrieben mit',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
open_source_documents_software: 'Open-Source-Dokumentensoftware', open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen', verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx', use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -127,6 +143,10 @@ const de = {
sending: 'Senden...', sending: 'Senden...',
resend_code: 'Code erneut senden', resend_code: 'Code erneut senden',
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet', verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
set_today: 'Heute einstellen',
date: 'Datum',
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
email_has_been_sent: 'Die E-Mail wurde gesendet' email_has_been_sent: 'Die E-Mail wurde gesendet'
} }
@@ -152,6 +172,7 @@ const fr = {
form_has_been_completed: 'Le formulaire a été complété !', form_has_been_completed: 'Le formulaire a été complété !',
create_a_free_account: 'Créer un Compte Gratuit', create_a_free_account: 'Créer un Compte Gratuit',
signed_with: 'Signé avec', signed_with: 'Signé avec',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
open_source_documents_software: 'logiciel de documents open source', open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone', verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx', use_international_format: 'Utiliser le format international : +1xxx',
@@ -160,6 +181,10 @@ const fr = {
sending: 'Envoi en cours...', sending: 'Envoi en cours...',
resend_code: 'Renvoyer le code', resend_code: 'Renvoyer le code',
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS', verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
set_today: "Définir Aujourd'hui",
date: 'Date',
toggle_multiline_text: 'Basculer le Texte Multiligne',
email_has_been_sent: "L'email a été envoyé" email_has_been_sent: "L'email a été envoyé"
} }
@@ -185,6 +210,7 @@ const pl = {
form_has_been_completed: 'Formularz został wypełniony!', form_has_been_completed: 'Formularz został wypełniony!',
create_a_free_account: 'Utwórz darmowe konto', create_a_free_account: 'Utwórz darmowe konto',
signed_with: 'Podpisane za pomocą', signed_with: 'Podpisane za pomocą',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
open_source_documents_software: 'oprogramowanie do dokumentów open source', open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu', verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx', use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -193,6 +219,10 @@ const pl = {
sending: 'Wysyłanie...', sending: 'Wysyłanie...',
resend_code: 'Ponownie wyślij kod', resend_code: 'Ponownie wyślij kod',
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany', verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
set_today: 'Ustaw Dziś',
date: 'Data',
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany' email_has_been_sent: 'E-mail został wysłany'
} }
@@ -218,6 +248,7 @@ const uk = {
form_has_been_completed: 'Форму заповнено!', form_has_been_completed: 'Форму заповнено!',
create_a_free_account: 'Створити безкоштовний обліковий запис', create_a_free_account: 'Створити безкоштовний обліковий запис',
signed_with: 'Підписано за допомогою', signed_with: 'Підписано за допомогою',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
open_source_documents_software: 'відкритий програмний засіб для документів', open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону', verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx', use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -226,6 +257,10 @@ const uk = {
sending: 'Надсилаю...', sending: 'Надсилаю...',
resend_code: 'Повторно відправити код', resend_code: 'Повторно відправити код',
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий', verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
set_today: 'Задати Сьогодні',
date: 'Дата',
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений' email_has_been_sent: 'Електронний лист був відправлений'
} }
@@ -251,6 +286,7 @@ const cs = {
form_has_been_completed: 'Formulář byl dokončen!', form_has_been_completed: 'Formulář byl dokončen!',
create_a_free_account: 'Vytvořit bezplatný účet', create_a_free_account: 'Vytvořit bezplatný účet',
signed_with: 'Podepsáno pomocí', signed_with: 'Podepsáno pomocí',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
open_source_documents_software: 'open source software pro dokumenty', open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo', verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx', use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -259,6 +295,10 @@ const cs = {
sending: 'Odesílání...', sending: 'Odesílání...',
resend_code: 'Znovu odeslat kód', resend_code: 'Znovu odeslat kód',
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán', verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
set_today: 'Nastavit Dnes',
date: 'Datum',
toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán' email_has_been_sent: 'E-mail byl odeslán'
} }
@@ -284,6 +324,7 @@ const pt = {
form_has_been_completed: 'O formulário foi concluído!', form_has_been_completed: 'O formulário foi concluído!',
create_a_free_account: 'Criar uma Conta Gratuita', create_a_free_account: 'Criar uma Conta Gratuita',
signed_with: 'Assinado com', signed_with: 'Assinado com',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
open_source_documents_software: 'software de documentos de código aberto', open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone', verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx', use_international_format: 'Use formato internacional: +1xxx',
@@ -292,6 +333,10 @@ const pt = {
sending: 'Enviando...', sending: 'Enviando...',
resend_code: 'Reenviar código', resend_code: 'Reenviar código',
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS', verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
set_today: 'Definir Hoje',
date: 'Data',
toggle_multiline_text: 'Alternar Texto Multilinha',
email_has_been_sent: 'Email enviado' email_has_been_sent: 'Email enviado'
} }
@@ -9,33 +9,37 @@
class="tooltip" class="tooltip"
:data-tip="t('draw_initials')" :data-tip="t('draw_initials')"
> >
<button <a
id="type_text_button" id="type_text_button"
href="#"
class="btn btn-sm btn-circle" class="btn btn-sm btn-circle"
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }" :class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconSignature :width="16" /> <IconSignature :width="16" />
</button> </a>
</span> </span>
<button <a
v-if="modelValue" v-if="modelValue || computedPreviousValue"
href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('clear') }} {{ t('clear') }}
</button> </a>
<button <a
v-else v-else
href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="clear" @click.prevent="clear"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('clear') }} {{ t('clear') }}
</button> </a>
<button <a
title="Minimize" title="Minimize"
href="#"
class="py-1.5 inline md:hidden" class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')" @click.prevent="$emit('minimize')"
> >
@@ -43,26 +47,26 @@
:width="20" :width="20"
:height="20" :height="20"
/> />
</button> </a>
</div> </div>
</div> </div>
<input <input
:value="modelValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
> >
<img <img
v-if="modelValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72" class="mx-auto bg-white border border-base-300 rounded max-h-72"
> >
<canvas <canvas
v-show="!modelValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded" class="bg-white border border-base-300 rounded"
/> />
<input <input
v-if="!isDrawInitials" v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input" id="initials_text_input"
ref="textInput" ref="textInput"
class="base-input !text-2xl w-full mt-6 text-center" class="base-input !text-2xl w-full mt-6 text-center"
@@ -78,6 +82,7 @@
<script> <script>
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad'
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
@@ -106,6 +111,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
previousValue: {
type: String,
required: false,
default: ''
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -115,10 +125,20 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
data () { data () {
return { return {
isInitialsStarted: false, isInitialsStarted: !!this.previousValue,
isUsePreviousValue: true,
isDrawInitials: false isDrawInitials: false
} }
}, },
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
} else {
return null
}
}
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -133,8 +153,6 @@ export default {
import('@rails/activestorage') import('@rails/activestorage')
} }
const { default: SignaturePad } = await import('signature_pad')
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -148,6 +166,9 @@ export default {
methods: { methods: {
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
this.isUsePreviousValue = false
this.isInitialsStarted = false
}, },
clear () { clear () {
this.pad.clear() this.pad.clear()
@@ -189,7 +210,11 @@ export default {
} }
}, },
async submit () { async submit () {
if (this.modelValue) { if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue)
}
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -21,7 +21,7 @@
:name="`values[${field.uuid}][]`" :name="`values[${field.uuid}][]`"
:value="option" :value="option"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="modelValue.includes(option)" :checked="(modelValue || []).includes(option)"
@change="onChange" @change="onChange"
> >
<span class="text-xl"> <span class="text-xl">
@@ -28,6 +28,7 @@
> >
<div class="flex justify-between mt-2 -mb-2 md:-mb-4"> <div class="flex justify-between mt-2 -mb-2 md:-mb-4">
<a <a
v-if="!defaultValue"
href="#" href="#"
class="link" class="link"
@click.prevent="isCodeSent = false" @click.prevent="isCodeSent = false"
@@ -48,6 +49,7 @@
:id="field.uuid" :id="field.uuid"
ref="phone" ref="phone"
:value="modelValue || defaultValue" :value="modelValue || defaultValue"
:readonly="!!defaultValue"
class="base-input !text-2xl w-full" class="base-input !text-2xl w-full"
autocomplete="tel" autocomplete="tel"
pattern="^\+[0-9\s\-]+$" pattern="^\+[0-9\s\-]+$"
@@ -9,14 +9,15 @@
class="tooltip" class="tooltip"
data-tip="Type text" data-tip="Type text"
> >
<button <a
id="type_text_button" id="type_text_button"
href="#"
class="btn btn-sm btn-circle" class="btn btn-sm btn-circle"
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }" :class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
</button> </a>
</span> </span>
<span <span
class="tooltip" class="tooltip"
@@ -34,23 +35,26 @@
> >
</label> </label>
</span> </span>
<button <a
v-if="modelValue" v-if="modelValue || computedPreviousValue"
href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('redraw') }} {{ t('redraw') }}
</button> </a>
<button <a
v-else v-else
href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="clear" @click.prevent="clear"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('clear') }} {{ t('clear') }}
</button> </a>
<button <a
href="#"
title="Minimize" title="Minimize"
class="py-1.5 inline md:hidden" class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')" @click.prevent="$emit('minimize')"
@@ -59,21 +63,21 @@
:width="20" :width="20"
:height="20" :height="20"
/> />
</button> </a>
</div> </div>
</div> </div>
<input <input
:value="modelValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
> >
<img <img
v-if="modelValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72" class="mx-auto bg-white border border-base-300 rounded max-h-72"
> >
<canvas <canvas
v-show="!modelValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded" class="bg-white border border-base-300 rounded"
/> />
@@ -93,6 +97,9 @@
<script> <script>
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad'
let isFontLoaded = false
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
@@ -122,6 +129,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
previousValue: {
type: String,
required: false,
default: ''
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -131,10 +143,20 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize'], emits: ['attached', 'update:model-value', 'start', 'minimize'],
data () { data () {
return { return {
isSignatureStarted: false, isSignatureStarted: !!this.previousValue,
isUsePreviousValue: true,
isTextSignature: false isTextSignature: false
} }
}, },
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
} else {
return null
}
}
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -147,8 +169,6 @@ export default {
import('@rails/activestorage') import('@rails/activestorage')
} }
const { default: SignaturePad } = await import('signature_pad')
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -162,6 +182,22 @@ export default {
methods: { methods: {
remove () { remove () {
this.$emit('update:model-value', '') 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 () { clear () {
this.pad.clear() this.pad.clear()
@@ -178,8 +214,8 @@ export default {
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
const context = canvas.getContext('2d') const context = canvas.getContext('2d')
const fontFamily = 'Arial' const fontFamily = 'Dancing Script'
const fontSize = '44px' const fontSize = '38px'
const fontStyle = 'italic' const fontStyle = 'italic'
const fontWeight = '' const fontWeight = ''
@@ -197,6 +233,8 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.textInput.focus() this.$refs.textInput.focus()
this.loadFont()
this.$emit('start') this.$emit('start')
}) })
} }
@@ -250,7 +288,11 @@ export default {
} }
}, },
async submit () { async submit () {
if (this.modelValue) { if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue)
}
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -0,0 +1,120 @@
<template>
<label
v-if="field.name"
:for="field.uuid"
class="label text-2xl mb-2"
>{{ field.name }}
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
<div
v-else
class="py-1"
/>
<div class="items-center flex">
<input
v-if="!isTextArea"
:id="field.uuid"
v-model="text"
class="base-input !text-2xl w-full !pr-11 -mr-10"
:required="field.required"
:pattern="field.validation?.pattern"
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
type="text"
:name="`values[${field.uuid}]`"
@focus="$emit('focus')"
>
<textarea
v-if="isTextArea"
:id="field.uuid"
ref="textarea"
v-model="text"
class="base-textarea !text-2xl w-full"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
:required="field.required"
:name="`values[${field.uuid}]`"
@input="resizeTextarea"
@focus="$emit('focus')"
/>
<div
v-if="!isTextArea"
class="tooltip"
:data-tip="t('toggle_multiline_text')"
>
<a
href="#"
class="btn btn-ghost btn-circle btn-sm"
@click.prevent="toggleTextArea"
>
<IconAlignBoxLeftTop />
</a>
</div>
</div>
</template>
<script>
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
export default {
name: 'TextStep',
components: {
IconAlignBoxLeftTop
},
inject: ['t'],
props: {
field: {
type: Object,
required: true
},
modelValue: {
type: String,
required: false,
default: ''
}
},
emits: ['update:model-value', 'focus'],
data () {
return {
isTextArea: false
}
},
computed: {
text: {
set (value) {
this.$emit('update:model-value', value)
},
get () {
return this.modelValue
}
}
},
mounted () {
this.isTextArea = this.modelValue?.includes('\n')
if (this.isTextArea) {
this.$nextTick(() => {
this.resizeTextarea()
})
}
},
methods: {
resizeTextarea () {
const textarea = this.$refs.textarea
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
},
toggleTextArea () {
this.isTextArea = true
this.$nextTick(() => {
this.$refs.textarea.focus()
this.$refs.textarea.setSelectionRange(this.$refs.textarea.value.length, this.$refs.textarea.value.length)
this.resizeTextarea()
})
}
}
}
</script>
+85 -4
View File
@@ -4,6 +4,7 @@
:style="positionStyle" :style="positionStyle"
@pointerdown.stop @pointerdown.stop
@mousedown.stop="startDrag" @mousedown.stop="startDrag"
@touchstart="startTouchDrag"
> >
<div <div
v-if="isSelected || isDraw" v-if="isSelected || isDraw"
@@ -32,7 +33,7 @@
<div <div
v-if="field?.type" v-if="field?.type"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10" class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
:class="{ 'flex z-10': isNameFocus || isSelected, hidden: !isNameFocus && !isSelected }" :class="{ 'flex z-10': isNameFocus || isSelected, invisible: !isNameFocus && !isSelected }"
style="top: -25px; height: 25px" style="top: -25px; height: 25px"
@mousedown.stop @mousedown.stop
@pointerdown.stop @pointerdown.stop
@@ -108,12 +109,14 @@
</span> </span>
</div> </div>
<div <div
ref="touchTarget"
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
/> />
<span <span
v-if="field?.type" v-if="field?.type"
class="h-2.5 w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize" class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
@mousedown.stop="startResize" @mousedown.stop="startResize"
@touchstart="startTouchResize"
/> />
</div> </div>
</template> </template>
@@ -186,7 +189,12 @@ export default {
'border-sky-500', 'border-sky-500',
'border-emerald-500', 'border-emerald-500',
'border-yellow-300', 'border-yellow-300',
'border-purple-600' 'border-purple-600',
'border-pink-500',
'border-cyan-500',
'border-orange-500',
'border-lime-500',
'border-indigo-500'
] ]
}, },
bgColors () { bgColors () {
@@ -195,7 +203,12 @@ export default {
'bg-sky-100', 'bg-sky-100',
'bg-emerald-100', 'bg-emerald-100',
'bg-yellow-100', 'bg-yellow-100',
'bg-purple-100' 'bg-purple-100',
'bg-pink-100',
'bg-cyan-100',
'bg-orange-100',
'bg-lime-100',
'bg-indigo-100'
] ]
}, },
isSelected () { isSelected () {
@@ -309,6 +322,47 @@ export default {
this.$emit('start-drag') this.$emit('start-drag')
}, },
startTouchDrag (e) {
if (e.target !== this.$refs.touchTarget) {
return
}
this.$refs?.name?.blur()
e.preventDefault()
this.isDragged = true
const rect = e.target.getBoundingClientRect()
this.selectedAreaRef.value = this.area
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
this.$el.getRootNode().addEventListener('touchmove', this.touchDrag)
this.$el.getRootNode().addEventListener('touchend', this.stopTouchDrag)
this.$emit('start-drag')
},
touchDrag (e) {
const page = this.$parent.$refs.mask.previousSibling
const rect = page.getBoundingClientRect()
this.area.x = (this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
},
stopTouchDrag () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
if (this.isDragged) {
this.save()
}
this.isDragged = false
this.$emit('stop-drag')
},
stopDrag () { stopDrag () {
this.$el.getRootNode().removeEventListener('mousemove', this.drag) this.$el.getRootNode().removeEventListener('mousemove', this.drag)
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag) this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
@@ -335,6 +389,33 @@ export default {
this.$emit('stop-resize') this.$emit('stop-resize')
this.save()
},
startTouchResize (e) {
this.selectedAreaRef.value = this.area
this.$refs?.name?.blur()
e.preventDefault()
this.$el.getRootNode().addEventListener('touchmove', this.touchResize)
this.$el.getRootNode().addEventListener('touchend', this.stopTouchResize)
this.$emit('start-resize', 'nwse')
},
touchResize (e) {
const page = this.$parent.$refs.mask.previousSibling
const rect = page.getBoundingClientRect()
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
},
stopTouchResize () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
this.$emit('stop-resize')
this.save() this.save()
} }
} }
+99 -24
View File
@@ -1,9 +1,12 @@
<template> <template>
<div <div
style="max-width: 1600px" style="max-width: 1600px"
class="mx-auto pl-4 h-full" class="mx-auto pl-3 md:pl-4 h-full"
> >
<div class="flex justify-between py-1.5 items-center pr-4"> <div
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
:style="{ backgroundColor }"
>
<div class="flex space-x-3"> <div class="flex space-x-3">
<a <a
v-if="withLogo" v-if="withLogo"
@@ -28,6 +31,7 @@
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="btn btn-primary text-base" class="btn btn-primary text-base"
@click="maybeShowEmptyTemplateAlert"
> >
<IconUsersPlus <IconUsersPlus
width="20" width="20"
@@ -59,10 +63,7 @@
</template> </template>
</div> </div>
</div> </div>
<div <div class="flex md:max-h-[calc(100vh-60px)]">
class="flex"
style="max-height: calc(100% - 60px)"
>
<div <div
ref="previews" ref="previews"
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }" :style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
@@ -97,7 +98,7 @@
/> />
</div> </div>
</div> </div>
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"> <div class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div <div
ref="documents" ref="documents"
class="pr-3.5 pl-0.5" class="pr-3.5 pl-0.5"
@@ -152,17 +153,54 @@
</div> </div>
</template> </template>
</div> </div>
<div <span
v-if="sortedDocuments.length" v-if="drawField"
class="sticky md:hidden" class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
style="bottom: 100px"
> >
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between ml-4 mr-6"> <span
<span class="w-full text-center text-lg"> 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"
You need a larger screen to use builder tools. >
<component
:is="fieldIcons[drawField.type]"
:width="20"
:height="20"
class="inline"
:stroke-width="1.6"
/>
<span>
Draw {{ fieldNames[drawField.type] }} Field
</span> </span>
</div> <a
</div> href="#"
class="link block text-center"
@click.prevent="drawField = null"
>
Cancel
</a>
</span>
</span>
<FieldType
v-if="sortedDocuments.length && !drawField"
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
:model-value="''"
@update:model-value="startFieldDraw($event)"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
>
<IconPlus
class="group-focus:hidden"
width="28"
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
/>
</label>
</FieldType>
</div> </div>
<div <div
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block" class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
@@ -214,7 +252,8 @@ import Logo from './logo'
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import DocumentPreview from './preview' import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue' import FieldType from './field_type'
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -224,6 +263,9 @@ export default {
Upload, Upload,
Document, Document,
Fields, Fields,
IconPlus,
FieldType,
IconX,
Logo, Logo,
Dropzone, Dropzone,
DocumentPreview, DocumentPreview,
@@ -300,6 +342,8 @@ export default {
} }
}, },
computed: { computed: {
fieldIcons: FieldType.computed.fieldIcons,
fieldNames: FieldType.computed.fieldNames,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldAreasIndex () { fieldAreasIndex () {
const areas = {} const areas = {}
@@ -351,6 +395,22 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
startFieldDraw (type) {
const field = {
name: '',
uuid: v4(),
required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid,
type
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = ['']
}
this.drawField = field
},
undo () { undo () {
if (this.undoStack.length > 1) { if (this.undoStack.length > 1) {
this.undoStack.pop() this.undoStack.pop()
@@ -448,6 +508,10 @@ export default {
this.drawField.areas ||= [] this.drawField.areas ||= []
this.drawField.areas.push(area) this.drawField.areas.push(area)
if (this.template.fields.indexOf(this.drawField) === -1) {
this.template.fields.push(this.drawField)
}
this.drawField = null this.drawField = null
this.selectedAreaRef.value = area this.selectedAreaRef.value = area
@@ -628,14 +692,25 @@ export default {
this.save() this.save()
}, },
onSaveClick () { maybeShowEmptyTemplateAlert (e) {
this.isSaving = true if (!this.template.fields.length) {
e.preventDefault()
this.save().then(() => { alert('Please draw fields to prepare the document.')
window.Turbo.visit(`/templates/${this.template.id}`) }
}).finally(() => { },
this.isSaving = false 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) { scrollToArea (area) {
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid) const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
@@ -24,7 +24,7 @@
* *
</span> </span>
<IconPencil <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' }" :style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
title="Edit" title="Edit"
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }" :class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
+19 -1
View File
@@ -56,8 +56,26 @@ export default {
} }
}, },
computed: { computed: {
numberOfPages () {
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
},
sortedPreviewImages () { sortedPreviewImages () {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename)) const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
return [...Array(this.numberOfPages).keys()].map((i) => {
return this.previewImagesIndex[i] || {
metadata: lazyloadMetadata,
id: Math.random().toString(),
url: `/preview/${this.document.uuid}/${i}.jpg`
}
})
},
previewImagesIndex () {
return this.document.preview_images.reduce((acc, e) => {
acc[parseInt(e.filename)] = e
return acc
}, {})
} }
}, },
beforeUpdate () { beforeUpdate () {
+14 -16
View File
@@ -5,7 +5,7 @@
<div <div
class="border border-base-300 rounded rounded-tr-none relative group" class="border border-base-300 rounded rounded-tr-none relative group"
> >
<div class="flex items-center justify-between relative"> <div class="flex items-center justify-between relative group/contenteditable-container">
<div <div
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
@click="scrollToFirstArea" @click="scrollToFirstArea"
@@ -31,21 +31,19 @@
v-if="isNameFocus" v-if="isNameFocus"
class="flex items-center relative" class="flex items-center relative"
> >
<template v-if="field.type !== 'checkbox'"> <input
<input :id="`required-checkbox-${field.uuid}`"
:id="`required-checkbox-${field.uuid}`" v-model="field.required"
v-model="field.required" type="checkbox"
type="checkbox" class="checkbox checkbox-xs no-animation rounded"
class="checkbox checkbox-xs no-animation rounded" @mousedown.prevent
@mousedown.prevent >
> <label
<label :for="`required-checkbox-${field.uuid}`"
:for="`required-checkbox-${field.uuid}`" class="label text-xs"
class="label text-xs" @click.prevent="field.required = !field.required"
@click.prevent="field.required = !field.required" @mousedown.prevent
@mousedown.prevent >Required</label>
>Required</label>
</template>
</div> </div>
<div <div
v-else v-else
@@ -14,7 +14,7 @@
<label <label
v-else v-else
tabindex="0" tabindex="0"
class="cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-between" class="cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 w-full flex justify-between"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
@@ -70,7 +70,7 @@
</button> </button>
</a> </a>
</li> </li>
<li v-if="submitters.length < 5"> <li v-if="submitters.length < 10">
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2"
@@ -130,7 +130,12 @@ export default {
'bg-sky-500', 'bg-sky-500',
'bg-emerald-500', 'bg-emerald-500',
'bg-yellow-300', 'bg-yellow-300',
'bg-purple-600' 'bg-purple-600',
'bg-pink-500',
'bg-cyan-500',
'bg-orange-500',
'bg-lime-500',
'bg-indigo-500'
] ]
}, },
names () { names () {
@@ -139,7 +144,12 @@ export default {
'Second Submitter', 'Second Submitter',
'Third Submitter', 'Third Submitter',
'Fourth Submitter', 'Fourth Submitter',
'Fifth Submitter' 'Fifth Submitter',
'Sixth Submitter',
'Seventh Submitter',
'Eighth Submitter',
'Ninth Submitter',
'Tenth Submitter'
] ]
}, },
selectedSubmitter () { selectedSubmitter () {
+15 -13
View File
@@ -1,20 +1,22 @@
<template> <template>
<span class="dropdown"> <span class="dropdown">
<label <slot>
tabindex="0" <label
:title="fieldNames[modelValue]" tabindex="0"
class="cursor-pointer" :title="fieldNames[modelValue]"
> class="cursor-pointer"
<component >
:is="fieldIcons[modelValue]" <component
:width="buttonWidth" :is="fieldIcons[modelValue]"
:class="buttonClasses" :width="buttonWidth"
:stroke-width="1.6" :class="buttonClasses"
/> :stroke-width="1.6"
</label> />
</label>
</slot>
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10" class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
:class="menuClasses" :class="menuClasses"
@click="closeDropdown" @click="closeDropdown"
> >
@@ -107,9 +107,6 @@
<li> <li>
Draw a text field on the page with a mouse Draw a text field on the page with a mouse
</li> </li>
<li>
Single click on the page to add a checkbox
</li>
<li> <li>
Drag &amp; drop any other field type on the page Drag &amp; drop any other field type on the page
</li> </li>
+20 -4
View File
@@ -1,12 +1,16 @@
<template> <template>
<div class="relative cursor-crosshair select-none"> <div
class="relative cursor-crosshair select-none"
:style="drawField ? 'touch-action: none' : ''"
>
<img <img
ref="image" ref="image"
loading="lazy"
:src="image.url" :src="image.url"
:width="width" :width="width"
class="border rounded mb-4"
:height="height" :height="height"
loading="lazy" class="border rounded mb-4"
@load="onImageLoad"
> >
<div <div
class="top-0 bottom-0 left-0 right-0 absolute" class="top-0 bottom-0 left-0 right-0 absolute"
@@ -32,12 +36,13 @@
/> />
</div> </div>
<div <div
v-show="resizeDirection || isMove || isDrag || showMask" v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
id="mask" id="mask"
ref="mask" ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute z-10" class="top-0 bottom-0 left-0 right-0 absolute z-10"
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }" :class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove" @pointermove="onPointermove"
@pointerdown="onStartDraw"
@dragover.prevent @dragover.prevent
@drop="onDrop" @drop="onDrop"
@pointerup="onPointerup" @pointerup="onPointerup"
@@ -93,6 +98,9 @@ export default {
} }
}, },
computed: { computed: {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
resizeDirectionClasses () { resizeDirectionClasses () {
return { return {
nwse: 'cursor-nwse-resize', nwse: 'cursor-nwse-resize',
@@ -110,6 +118,10 @@ export default {
this.areaRefs = [] this.areaRefs = []
}, },
methods: { methods: {
onImageLoad (e) {
e.target.setAttribute('width', e.target.naturalWidth)
e.target.setAttribute('height', e.target.naturalHeight)
},
setAreaRefs (el) { setAreaRefs (el) {
if (el) { if (el) {
this.areaRefs.push(el) this.areaRefs.push(el)
@@ -125,6 +137,10 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (this.isMobile && !this.drawField) {
return
}
this.showMask = true this.showMask = true
this.$nextTick(() => { this.$nextTick(() => {
+1 -1
View File
@@ -114,7 +114,7 @@ export default {
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'], emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
computed: { computed: {
previewImage () { previewImage () {
return this.document.preview_images[0] return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
} }
}, },
mounted () { mounted () {
@@ -4,7 +4,7 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.co Webhook'
def perform(submitter) def perform(submitter)
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) config = Accounts.load_webhook_configs(submitter.submission.account)
return if config.blank? || config.value.blank? return if config.blank? || config.value.blank?
@@ -15,7 +15,7 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
Faraday.post(config.value, Faraday.post(config.value,
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current.iso8601, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
@@ -4,7 +4,7 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.co Webhook'
def perform(submitter) def perform(submitter)
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) config = Accounts.load_webhook_configs(submitter.submission.account)
return if config.blank? || config.value.blank? return if config.blank? || config.value.blank?
@@ -13,7 +13,7 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
Faraday.post(config.value, Faraday.post(config.value,
{ {
event_type: 'form.started', event_type: 'form.started',
timestamp: Time.current.iso8601, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
@@ -4,7 +4,7 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.co Webhook'
def perform(submitter) def perform(submitter)
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) config = Accounts.load_webhook_configs(submitter.submission.account)
return if config.blank? || config.value.blank? return if config.blank? || config.value.blank?
@@ -13,7 +13,7 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
Faraday.post(config.value, Faraday.post(config.value,
{ {
event_type: 'form.viewed', event_type: 'form.viewed',
timestamp: Time.current.iso8601, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
+18 -5
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class SubmitterMailer < ApplicationMailer class SubmitterMailer < ApplicationMailer
MAX_ATTACHMENTS_SIZE = 10.megabytes
def invitation_email(submitter, body: nil, subject: nil) def invitation_email(submitter, body: nil, subject: nil)
@current_account = submitter.submission.template.account @current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
@@ -12,7 +14,7 @@ class SubmitterMailer < ApplicationMailer
if @email_config || subject.present? if @email_config || subject.present?
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:) ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
else else
'You have been invited to submit a form' 'You are invited to submit a form'
end end
mail(to: @submitter.friendly_name, mail(to: @submitter.friendly_name,
@@ -62,7 +64,7 @@ class SubmitterMailer < ApplicationMailer
if @email_config if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:) ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
else else
'Your copy of documents' 'Your document copy'
end end
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
@@ -75,13 +77,24 @@ class SubmitterMailer < ApplicationMailer
def add_completed_email_attachments!(submitter) def add_completed_email_attachments!(submitter)
documents = Submitters.select_attachments_for_download(submitter) documents = Submitters.select_attachments_for_download(submitter)
total_size = 0
audit_trail_data = nil
if submitter.submission.audit_trail.present?
audit_trail_data = submitter.submission.audit_trail.download
total_size = audit_trail_data.size
end
documents.each do |attachment| documents.each do |attachment|
total_size += attachment.byte_size
break if total_size >= MAX_ATTACHMENTS_SIZE
attachments[attachment.filename.to_s] = attachment.download attachments[attachment.filename.to_s] = attachment.download
end end
if submitter.submission.audit_trail.present? attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
attachments[submitter.submission.audit_trail.filename.to_s] = submitter.submission.audit_trail.download
end
documents documents
end end
+3 -3
View File
@@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class UserMailer < ApplicationMailer class UserMailer < ApplicationMailer
def invitation_email(user) def invitation_email(user, invited_by: nil)
@current_account = user.account @current_account = invited_by&.account || user.account
@user = user @user = user
@token = @user.send(:set_reset_password_token) @token = @user.send(:set_reset_password_token)
mail(to: @user.friendly_name, mail(to: @user.friendly_name,
subject: 'You have been invited to Docuseal') subject: 'You are invited to DocuSeal')
end end
end end
+4 -4
View File
@@ -30,9 +30,9 @@ class AccountConfig < ApplicationRecord
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
'subject' => 'You have been invited to submit a form', 'subject' => 'You are invited to submit a form',
'body' => "Hi there,\n\n" \ 'body' => "Hi there,\n\n" \
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \ "You have been invited to submit the \"{{template.name}}\" form.\n\n" \
"{{submitter.link}}\n\n" \ "{{submitter.link}}\n\n" \
"Please contact us by replying to this email if you didn't request this.\n\n" \ "Please contact us by replying to this email if you didn't request this.\n\n" \
"Thanks,\n" \ "Thanks,\n" \
@@ -45,10 +45,10 @@ class AccountConfig < ApplicationRecord
'{{submission.link}}' '{{submission.link}}'
}, },
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => { SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
'subject' => 'Your copy of documents', 'subject' => 'Your document copy',
'body' => "Hi there,\n\n" \ 'body' => "Hi there,\n\n" \
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \ "Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
"Alternatively, you can download the copy using:\n\n" \ "Alternatively, you can download your copy using:\n\n" \
"{{documents.links}}\n\n" \ "{{documents.links}}\n\n" \
"Thanks,\n" \ "Thanks,\n" \
'{{account.name}}' '{{account.name}}'
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: encrypted_user_configs
#
# id :bigint not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_encrypted_user_configs_on_user_id (user_id)
# index_encrypted_user_configs_on_user_id_and_key (user_id,key) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
class EncryptedUserConfig < ApplicationRecord
belongs_to :user
encrypts :value
serialize :value, JSON
end
+2
View File
@@ -48,6 +48,8 @@ class Submission < ApplicationRecord
through: :template, source: :documents_attachments through: :template, source: :documents_attachments
scope :active, -> { where(deleted_at: nil) } 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 }
enum :source, { enum :source, {
invite: 'invite', invite: 'invite',
+16 -15
View File
@@ -4,21 +4,22 @@
# #
# Table name: submitters # Table name: submitters
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# completed_at :datetime # application_key :string
# email :string # completed_at :datetime
# ip :string # email :string
# name :string # ip :string
# opened_at :datetime # name :string
# phone :string # opened_at :datetime
# sent_at :datetime # phone :string
# slug :string not null # sent_at :datetime
# ua :string # slug :string not null
# uuid :string not null # ua :string
# values :text not null # uuid :string not null
# created_at :datetime not null # values :text not null
# updated_at :datetime not null # created_at :datetime not null
# submission_id :bigint not null # updated_at :datetime not null
# submission_id :bigint not null
# #
# Indexes # Indexes
# #
+9
View File
@@ -53,6 +53,7 @@ class User < ApplicationRecord
has_one :access_token, dependent: :destroy has_one :access_token, dependent: :destroy
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
has_many :user_configs, dependent: :destroy has_many :user_configs, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant? devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
@@ -75,6 +76,14 @@ class User < ApplicationRecord
true true
end end
def self.sign_in_after_reset_password
if PasswordsController::Current.user.present?
!PasswordsController::Current.user.otp_required_for_login
else
true
end
end
def initials def initials
[first_name&.first, last_name&.first].compact_blank.join.upcase [first_name&.first, last_name&.first].compact_blank.join.upcase
end end
+3 -2
View File
@@ -21,8 +21,9 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<% unless Docuseal.multitenant? %> <% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<%= f.fields_for @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) do |ff| %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
<%= f.fields_for encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %> <%= ff.label :value, 'App URL', class: 'label' %>
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %> <%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
+1 -1
View File
@@ -84,7 +84,7 @@
<div class="flex items-center h-full"> <div class="flex items-center h-full">
<div class="mx-auto"> <div class="mx-auto">
<div class="max-w-xl mx-auto"> <div class="max-w-xl mx-auto">
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to DocuSeal</h1> <h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
</div> </div>
<div class="max-w-lg mx-auto"> <div class="max-w-lg mx-auto">
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p> <p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
-1
View File
@@ -21,6 +21,5 @@
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %> <%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %>
</div> </div>
<%= render 'devise/shared/links' %>
<% end %> <% end %>
</div> </div>
-1
View File
@@ -12,5 +12,4 @@
<%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %> <%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<%= render 'devise/shared/links' %>
</div> </div>
@@ -41,6 +41,13 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %> <%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
<% if Docuseal.multitenant? %>
<button id="talk_to_sales_button" class="hidden" type="submit" formaction="<%= enquiries_path %>" formnovalidate="true" formmethod="post"></button>
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="talk_to_sales" class="base-checkbox" onchange="window.talk_to_sales_button.click()">
<span class="label">I would like to talk to sales</span>
</label>
<% end %>
</div> </div>
<% end %> <% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
+3 -3
View File
@@ -1,6 +1,6 @@
<div class="max-w-lg mx-auto px-2"> <div class="max-w-lg mx-auto px-2">
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %> <%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1> <h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %> <%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<% if params[:redir].present? %> <% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %> <%= hidden_field_tag :redir, params[:redir] %>
@@ -16,11 +16,11 @@
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %> <%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<%= button_to button_title(title: 'Log 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 %> <%= 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 %>
<% end %> <% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
</div> </div>
+2 -2
View File
@@ -1,5 +1,5 @@
<div class="max-w-lg mx-auto px-2"> <div class="max-w-lg mx-auto px-2">
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1> <h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %> <%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<%= f.hidden_field :email %> <%= f.hidden_field :email %>
<%= f.hidden_field :password %> <%= f.hidden_field :password %>
@@ -13,7 +13,7 @@
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %> <%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
</div> </div>
+1 -7
View File
@@ -1,6 +1,6 @@
<div class="flex justify-between mt-4"> <div class="flex justify-between mt-4">
<%- if controller_name != 'sessions' %> <%- if controller_name != 'sessions' %>
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %> <%= link_to 'Already have an account?', new_session_path(resource_name), class: 'link link-hover mx-auto' %>
<% end %> <% end %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %> <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %> <%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
@@ -8,10 +8,4 @@
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %> <%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
<% end %> <% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: 'link link-hover' %>
<% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
<% end %>
</div> </div>
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 21l18 0"></path>
<path d="M9 8l1 0"></path>
<path d="M9 12l1 0"></path>
<path d="M9 16l1 0"></path>
<path d="M14 8l1 0"></path>
<path d="M14 12l1 0"></path>
<path d="M14 16l1 0"></path>
<path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16"></path>
</svg>

After

Width:  |  Height:  |  Size: 565 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 7v5l3 3" />
</svg>

After

Width:  |  Height:  |  Size: 362 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3"></path>
<path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3"></path>
<path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7"></path>
<path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1"></path>
<path d="M17 12h.01"></path>
<path d="M13 12h.01"></path>
</svg>

After

Width:  |  Height:  |  Size: 588 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 6l11 0" />
<path d="M9 12l11 0" />
<path d="M9 18l11 0" />
<path d="M5 6l0 .01" />
<path d="M5 12l0 .01" />
<path d="M5 18l0 .01" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
<path d="M20 4v5h-5"></path>
</svg>

After

Width:  |  Height:  |  Size: 409 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path>
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path>
</svg>

After

Width:  |  Height:  |  Size: 504 B

+1 -1
View File
@@ -1,5 +1,5 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to Docuseal</h1> <h1 class="text-4xl font-bold text-center my-8">👋 Welcome to <%= Docuseal.product_name %></h1>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %> <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %> <%= render 'devise/shared/error_messages', resource: %>
+4
View File
@@ -0,0 +1,4 @@
<title>
DocuSeal | Open Source Document Filling and Signing
</title>
<%= render 'shared/meta' %>
+2 -4
View File
@@ -1,16 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-theme="docuseal" lang="en"> <html data-theme="docuseal" lang="en">
<head> <head>
<title> <%= render 'layouts/head_tags' %>
DocuSeal | Open Source Document Filling and Signing
</title>
<%= render 'shared/meta' %>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<%= javascript_pack_tag 'application', defer: true %> <%= javascript_pack_tag 'application', defer: true %>
<%= stylesheet_pack_tag 'application', media: 'all' %> <%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
</head> </head>
<body> <body>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>
+1 -4
View File
@@ -1,10 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-theme="docuseal" lang="en"> <html data-theme="docuseal" lang="en">
<head> <head>
<title> <%= render 'layouts/head_tags' %>
DocuSeal | Open Source Document Filling and Signing
</title>
<%= render 'shared/meta' %>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
+1 -4
View File
@@ -1,10 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-theme="docuseal" lang="en"> <html data-theme="docuseal" lang="en">
<head> <head>
<title> <%= render 'layouts/head_tags' %>
DocuSeal | Open Source Document Filling and Signing
</title>
<%= render 'shared/meta' %>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
+2 -2
View File
@@ -22,7 +22,7 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3> <h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>. Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
</p> </p>
</div> </div>
</div> </div>
@@ -70,4 +70,4 @@
</div> </div>
</div> </div>
</section> </section>
<%= render 'shared/attribution' %> <%= render 'shared/attribution', with_counter: true %>
Whitespace-only changes.
+1
View File
@@ -21,6 +21,7 @@
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %> <%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<%= render 'email_configs' %>
<p class="text-2xl font-bold mt-8 mb-4">Signature</p> <p class="text-2xl font-bold mt-8 mb-4">Signature</p>
<% signature = UserConfigs.load_signature(current_user) %> <% signature = UserConfigs.load_signature(current_user) %>
<% if signature %> <% if signature %>
@@ -10,4 +10,4 @@
</div> </div>
</div> </div>
</div> </div>
<%= render 'shared/attribution' %> <%= render 'shared/attribution', link_path: '/start' %>
+1 -1
View File
@@ -1 +1 @@
<%= render 'shared/powered_by' %> <%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path] %>
+1
View File
@@ -0,0 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent' %>
+1 -2
View File
@@ -3,8 +3,7 @@
<div class="w-full text-center"> <div class="w-full text-center">
<span class="font-bold">Demo Environment</span> <span class="font-bold">Demo Environment</span>
<br> <br>
Feel free to <a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊 <a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
</div> </div>
</div> </div>
+1 -1
View File
@@ -2,5 +2,5 @@
--- ---
</p> </p>
<p> <p>
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing. Sent using <a href="<%= Docuseal::PRODUCT_URL %>/start"><%= Docuseal.product_name %></a> free document signing.
</p> </p>
+3
View File
@@ -0,0 +1,3 @@
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
</a>
+35 -30
View File
@@ -1,26 +1,28 @@
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between"> <div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2"> <a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/logo' %> <%= render 'shared/title' %>
<span>DocuSeal</span>
</a> </a>
<% unless Docuseal.demo? %> <%= render 'shared/github' %>
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
</a>
<% end %>
</div> </div>
<% if signed_in? %> <% if signed_in? %>
<div class="space-x-4 flex items-center"> <div class="space-x-4 flex items-center">
<% if Docuseal.demo? %> <% if Docuseal.demo? %>
<%= render 'shared/github_button' %> <a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
Sign Up
</a>
<span class="hidden sm:inline">
<%= render 'shared/github_button' %>
</span>
<% else %> <% else %>
<div class="flex items-center justify-center space-x-4 mr-1"> <div class="flex items-center justify-center space-x-4 mr-1">
<%= 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 %> <% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
Console <%= 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 %>
<span class="badge badge-warning ml-1">New</span> 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 %> <% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %> <%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
</div> </div>
<% end %> <% end %>
@@ -35,7 +37,7 @@
<span class="mr-1">Profile</span> <span class="mr-1">Profile</span>
<% end %> <% end %>
</li> </li>
<% unless Docuseal.demo? %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %>
<%= svg_icon('terminal', class: 'w-5 h-5 stroke-2') %> <%= svg_icon('terminal', class: 'w-5 h-5 stroke-2') %>
@@ -43,12 +45,14 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<li> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<%= link_to settings_esign_path, class: 'flex items-center' do %> <li>
<%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %> <%= link_to settings_esign_path, class: 'flex items-center' do %>
<span class="mr-1">Verify PDF</span> <%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %>
<% end %> <span class="mr-1">Verify PDF</span>
</li> <% end %>
</li>
<% end %>
<li> <li>
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %> <%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 mr-2 inline') %> <%= svg_icon('logout', class: 'w-5 h-5 stroke-2 mr-2 inline') %>
@@ -59,18 +63,19 @@
</div> </div>
</div> </div>
<% else %> <% else %>
<div class="flex space-x-6"> <div class="flex space-x-2">
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %> <% if request.path != new_user_session_path %>
<span class="flex items-center justify-center space-x-1"> <%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
<%= svg_icon('login', class: 'w-6 h-6') %>
<span>Sign in</span>
</span>
<% end %>
<% if Docuseal.multitenant? %>
<%= link_to registration_path, class: 'font-medium text-lg hidden md:inline' do %>
<span class="flex items-center justify-center space-x-1"> <span class="flex items-center justify-center space-x-1">
<%= svg_icon('user_plus', class: 'w-6 h-6') %> <%= svg_icon('login', class: 'w-6 h-6') %>
<span>Try for Free</span> <span>Sign in</span>
</span>
<% end %>
<% end %>
<% 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> </span>
<% end %> <% end %>
<% end %> <% end %>
+9
View File
@@ -0,0 +1,9 @@
<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>
+11 -2
View File
@@ -1,4 +1,13 @@
<div class="text-center px-2"> <div class="text-center px-2">
Powered by <% if local_assigns[:with_counter] %>
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software <% count = Submitter.where.not(completed_at: nil).distinct.count(:submission_id) %>
<% if count > 1 %>
<b><%= count %></b> documents signed with
<% else %>
Powered by
<% end %>
<% else %>
Powered by
<% end %>
<a href="<%= Docuseal::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Docuseal.product_name %></a> - open source documents software
</div> </div>
+9 -6
View File
@@ -1,11 +1,14 @@
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex"> <form action="<%= url_for %>" method="get" class="items-center hidden md:flex">
<div class="relative"> <% if params[:status].present? %>
<% if params[:q].present? %> <input name="status" value="<%= params[:status] %>" class="hidden">
<a href="<%= request.path %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight"> <% end %>
<% if params[:q].present? %>
<div class="relative">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
&times; &times;
</a> </a>
<% end %> </div>
</div> <% end %>
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>"> <input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>">
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]"> <button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
<span class="enabled"> <span class="enabled">
+41 -22
View File
@@ -41,6 +41,13 @@
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% 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 Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %> <% if can?(:read, AccessToken) %>
<li> <li>
@@ -58,7 +65,15 @@
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% unless Docuseal.demo? %> <%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Console Console
@@ -68,27 +83,31 @@
<% end %> <% end %>
</ul> </ul>
</menu-active> </menu-active>
<div class="mx-4 border-t border-base-300 hidden md:block"> <% if !can?(:manage, :tenants) %>
<div class="text-sm mt-3"> <div class="mx-4 border-t border-base-300 hidden md:block">
Need help? Ask a question: <div class="text-sm mt-3">
</div> Need help? Ask a question:
<div class="flex mt-3 space-x-3"> </div>
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <div class="flex mt-3 space-x-3">
<%= svg_icon('brand_github', class: 'w-8 h-8') %> <div class="tooltip" data-tip="GitHub">
</a> <a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <%= svg_icon('brand_github', class: 'w-8 h-8') %>
<%= svg_icon('brand_discord', class: 'w-8 h-8') %> </a>
</a> </div>
<a href="<%= Docuseal::TWITTER_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <div class="tooltip" data-tip="Discord Community">
<%= svg_icon('brand_twitter', class: 'w-8 h-8') %> <a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
</a>
</div>
<div class="tooltip" data-tip="Twitter">
<a href="<%= Docuseal::TWITTER_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_twitter', class: 'w-8 h-8') %>
</a>
</div>
</div>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %>
</a> </a>
</div> </div>
<a href="https://twitter.com/intent/tweet?<%= { text: "Open source DocuSign alternative\n#{Docuseal::GITHUB_URL} #{Docuseal::TWITTER_HANDLE}" }.to_query %>" target="_blank" class="btn btn-neutral btn-outline w-full mt-5"> <% end %>
<%= svg_icon('send', class: 'h-5 w-5') %>
Tell about us
</a>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %>
</a>
</div>
</div> </div>
Whitespace-only changes.
+2
View File
@@ -0,0 +1,2 @@
<%= render 'shared/logo' %>
<span>DocuSeal</span>
@@ -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>
+8
View File
@@ -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>
+4 -1
View File
@@ -24,6 +24,9 @@
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> <%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div> </div>
<% end %> <% end %>
<div>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div>
</div> </div>
</div> </div>
<%= render 'shared/attribution' %> <%= render 'shared/attribution', link_path: '/start' %>
+19 -11
View File
@@ -3,7 +3,9 @@
<div class="space-y-6"> <div class="space-y-6">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'banner' %>
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p> <% unless @template.deleted_at? %>
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
<% end %>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center"> <div class="flex items-center">
@@ -12,20 +14,26 @@
</div> </div>
<div> <div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p> <p class="text-lg font-bold mb-1"><%= @template.name %></p>
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p> <% if @template.deleted_at? %>
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @template.account.name %></span>.</p>
<% else %>
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
<% end %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %> <% unless @template.deleted_at? %>
<div class="form-control !mt-0"> <%= 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| %>
<%= f.label :email, class: 'label' %> <div class="form-control !mt-0">
<%= f.email_field :email, value: current_user&.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %> <%= f.label :email, class: 'label' %>
</div> <%= f.email_field :email, value: current_user&.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
<div class="form-control"> </div>
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %> <div class="form-control">
</div> <%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
</div>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>
<%= render 'shared/attribution' %> <%= render 'shared/attribution', link_path: '/start' %>
Whitespace-only changes.
@@ -41,6 +41,7 @@
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %> <%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<%= render 'message_fields' %>
</div> </div>
</div> </div>
</div> </div>
+1 -3
View File
@@ -28,8 +28,6 @@
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %> <%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
</div> </div>
<% else %> <% else %>
<div class="flex items-center px-0.5"> <div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<%= Array.wrap(value).join(', ') %>
</div>
<% end %> <% end %>
</div> </div>
+1
View File
@@ -23,4 +23,5 @@
<%= render 'detailed_form', template: @template %> <%= render 'detailed_form', template: @template %>
</div> </div>
</div> </div>
<%= content_for(:modal_extra) %>
<% end %> <% end %>
+7 -3
View File
@@ -44,12 +44,16 @@
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %> <% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %> <% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% (@submission.template_schema || @submission.template.schema).each do |item| %> <% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %> <% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.first.metadata %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>" loading="lazy"> <img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>">
<div class="top-0 bottom-0 left-0 right-0 absolute"> <div class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %> <% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %> <%= render 'submissions/annotation', annot: %>
@@ -66,7 +70,7 @@
</div> </div>
</div> </div>
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space"> <div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %> <% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %> <% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %> <% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %> <% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
@@ -1,4 +1,4 @@
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3"> <a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %> <%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span><%= Docuseal::PRODUCT_NAME %></span> <span><%= Docuseal.product_name %></span>
</a> </a>
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more