Compare commits

...
85 Commits
Author SHA1 Message Date
Pete Matsyburka 95f734e4b8 adjust cloud logs 2024-02-11 15:25:10 +02:00
Pete Matsyburka ff74574e84 adjust api completion 2024-02-10 21:03:12 +02:00
Pete Matsyburka 843f6c405f max 20 signer parties 2024-02-10 11:31:20 +02:00
Pete Matsyburka f8b24ebfce retry webhooks on non 200 status 2024-02-10 11:31:19 +02:00
Pete Matsyburka d1d26b5424 increase canvas resolution 2024-02-09 22:33:18 +02:00
Pete Matsyburka 8aa6f45da8 fix signature image reupload 2024-02-09 21:24:30 +02:00
Pete Matsyburka 48d5bee95b parse unix date 2024-02-09 15:03:20 +02:00
Pete Matsyburka 98c79d2dec skip anotation if nil 2024-02-09 00:43:13 +02:00
Pete Matsyburka 0f51238f62 fix default date 2024-02-08 23:29:18 +02:00
Pete Matsyburka b7323b929f set signing date option 2024-02-08 22:55:13 +02:00
Pete Matsyburka 05eb1537f7 replace checkmark emoji 2024-02-08 21:44:27 +02:00
Pete Matsyburka bc9f27cfed allow to aunarchive template 2024-02-08 20:49:10 +02:00
Pete Matsyburka 8f457be719 fix default values 2024-02-08 20:12:08 +02:00
Pete Matsyburka 6d590e13c9 flatten pdf form when signing 2024-02-08 18:06:55 +02:00
Pete Matsyburka c987ec172a fix canvas intersection observer 2024-02-08 12:11:10 +02:00
Pete Matsyburka 49f1637c47 add more submitter parties 2024-02-08 11:54:54 +02:00
Pete Matsyburka 13379bd699 resize canvas on visibility change 2024-02-07 01:47:57 +02:00
Pete Matsyburka 5ee762c08c adjust update tempalte api 2024-02-06 20:07:15 +02:00
Pete Matsyburka edb6d561f1 allow to add checked checkboxes in builder 2024-02-05 23:03:45 +02:00
Pete Matsyburka 1b90f77e82 skip errors in pdf processor 2024-02-05 13:54:55 +02:00
Pete Matsyburka c37785ecbc update on-prem links 2024-02-03 22:52:41 +02:00
Pete Matsyburka c67188edf8 ability to share template with testing account 2024-02-03 22:37:24 +02:00
Pete Matsyburka 8630c68631 add account_id to submissions 2024-02-03 18:56:45 +02:00
Pete Matsyburka ddcb5c5d40 adjust params validator 2024-02-03 11:37:05 +02:00
Pete Matsyburka 07f0fbb50f add submitter metadata 2024-02-03 10:29:45 +02:00
Pete Matsyburka 3781b0aa72 application_key -> external_id 2024-02-03 10:29:45 +02:00
Pete Matsyburka 42a5cea521 remove verify link from api source 2024-02-02 19:29:01 +02:00
Pete Matsyburka fb6000ca2c add builder title container id 2024-02-01 20:59:03 +02:00
Pete Matsyburka 59f98db754 fix NL i18n 2024-02-01 15:14:52 +02:00
Pete Matsyburka 6add842a27 api match lowercase field names 2024-02-01 13:34:30 +02:00
Pete Matsyburka 43ca4a6365 update readme contact link 2024-02-01 12:48:20 +02:00
Pete Matsyburka 68196d6af0 better on prem console sign up 2024-01-31 02:52:43 +02:00
Pete Matsyburka a582d39bb9 add preview image url to templates 2024-01-30 17:53:25 +02:00
Pete Matsyburka 7a3e89c4d8 allow to disable resubmit button 2024-01-30 17:03:58 +02:00
Alex Turchyn 87f036ad88 optimize template page for mobile devices 2024-01-30 11:45:52 +02:00
Pete Matsyburka 5738b8dd58 fix submissions params validator 2024-01-30 01:46:30 +02:00
Pete Matsyburka 8d73371e0f do not add email signing link automatically with API 2024-01-29 23:37:22 +02:00
Pete Matsyburka 3d870b2305 remove i18n duplicate keys 2024-01-29 21:08:57 +02:00
Pete Matsyburka 27d8cd57bd add dutch form translations 2024-01-29 19:53:27 +02:00
Pete Matsyburka 353b2e7a94 fix typed font 2024-01-29 17:44:55 +02:00
Pete Matsyburka d77bd87acb add account removal button 2024-01-29 01:30:06 +02:00
Pete Matsyburka bae7b924ae adjust testing audit log 2024-01-29 00:07:52 +02:00
Pete Matsyburka 89867fdde3 simplify submissions api 2024-01-28 14:43:13 +02:00
Pete Matsyburka c9a1f067ac make API fields match case insensitive 2024-01-28 13:47:17 +02:00
Pete Matsyburka eb26835d57 remove resubmit button from API submissions 2024-01-28 13:27:50 +02:00
Pete Matsyburka 7301661468 fix api params validator 2024-01-27 14:24:24 +02:00
Pete Matsyburka e4fe984a70 fix validator path 2024-01-26 23:03:39 +02:00
Pete Matsyburka 12ccf622c1 enable API errors 2024-01-26 22:26:55 +02:00
Pete Matsyburka 6c35ff6d1c make confetti configurable 2024-01-26 17:43:52 +02:00
Pete Matsyburka 1d7c84302e fix form area style 2024-01-26 17:24:01 +02:00
Pete Matsyburka f3e8889710 clear canvas on signature toggle 2024-01-26 17:12:08 +02:00
Pete Matsyburka 4b2f9ff306 improve form i18n 2024-01-26 16:45:32 +02:00
Pete Matsyburka b4ba642f8e add builder currencies prop 2024-01-26 10:43:37 +02:00
Pete Matsyburka 5c69b965c0 fix submission default values with 2+ submitters 2024-01-25 23:41:44 +02:00
Pete Matsyburka cba196385c do now show textarea toggle with validation pattern 2024-01-25 22:58:17 +02:00
Alex Turchyn 00abad56cb add the 'more_then' validator to the params validator 2024-01-25 01:36:50 +02:00
Pete Matsyburka d1070c4955 fix focus field border 2024-01-24 23:53:47 +02:00
Pete Matsyburka e84be523c6 add show field names prop 2024-01-24 23:20:53 +02:00
Pete Matsyburka 5d598e8268 improve radio checkboxes form 2024-01-24 22:28:45 +02:00
Pete Matsyburka 0e9d922204 fix normalize values when nil 2024-01-24 10:05:14 +02:00
Pete Matsyburka 80c1b04e83 fix mobile builder style 2024-01-24 01:11:04 +02:00
Pete Matsyburka b2fca3edf3 do not allow add new if parties predefined 2024-01-23 23:58:25 +02:00
Pete Matsyburka 2a292c8e80 add phone validation alert 2024-01-23 21:58:19 +02:00
Pete Matsyburka e6052ad0d8 validate email/phone submission format 2024-01-23 21:47:01 +02:00
Pete Matsyburka 8646e17d5a fix typo 2024-01-22 22:45:24 +02:00
Pete Matsyburka e6785b0afd fix sign in form without oauth 2024-01-22 09:32:19 +02:00
Pete Matsyburka 009033a411 fix params validation 2024-01-22 09:32:18 +02:00
Alex Turchyn fdc3d7742a fix recipients switch 2024-01-21 18:07:29 +02:00
Alex Turchyn f9ccca1376 add base API error messages 2024-01-21 15:19:16 +02:00
Alex Turchyn 0a00fe6ab8 fix minor mobile UI issues 2024-01-21 13:36:58 +02:00
Pete Matsyburka 6253e80a70 add testing environment 2024-01-21 11:20:34 +02:00
Pete Matsyburka 3dcb7c813f fix rspec 2024-01-19 12:45:43 +02:00
Pete Matsyburka 39cdc41a67 fix erblint 2024-01-19 12:41:36 +02:00
Pete Matsyburka d3000487f4 prevent builder submitter click # nav 2024-01-19 10:15:21 +02:00
Pete Matsyburka 80254da1c1 adjust consider json middleware 2024-01-19 09:54:49 +02:00
Pete Matsyburka 73fc2d92f7 fix annotations 2024-01-19 01:15:13 +02:00
Pete Matsyburka f69aa33f3d i18n builder confirm 2024-01-18 21:12:08 +02:00
Pete Matsyburka e16202c9f7 ajust 2fa message 2024-01-18 10:33:25 +02:00
Pete Matsyburka 7453d0d274 reduce image size 2024-01-17 22:22:50 +02:00
Pete Matsyburka c102828c93 fix manual send button 2024-01-17 13:52:38 +02:00
Alex Turchyn afeb6dd318 optimize submissions show page for mobile devices 2024-01-17 01:45:44 +02:00
Pete Matsyburka 6156623f0e remove welcome message 2024-01-17 01:45:44 +02:00
Pete Matsyburka ccc06fcc3d allow to remove template permanently 2024-01-17 01:45:40 +02:00
Pete Matsyburka bbe3942056 validate sender email 2024-01-16 23:46:41 +02:00
Pete Matsyburka 49cea59b94 do not authenticate archived users 2024-01-16 00:18:58 +02:00
140 changed files with 2301 additions and 769 deletions

No files matched your search

+2 -2
View File
@@ -30,7 +30,7 @@ Metrics/ParameterLists:
Max: 10 Max: 10
Metrics/MethodLength: Metrics/MethodLength:
Max: 25 Max: 30
Exclude: Exclude:
- 'db/migrate/**' - 'db/migrate/**'
@@ -44,7 +44,7 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#'] AllowedPatterns: ['\A\s*#']
Metrics/AbcSize: Metrics/AbcSize:
Max: 35 Max: 40
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 500
+1 -1
View File
@@ -43,7 +43,7 @@ RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && find /usr/local/bundle -type d \( -name "rdoc" -o -name "spec" \) -exec rm -rf {} + RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
COPY ./bin ./bin COPY ./bin ./bin
COPY ./app ./app COPY ./app ./app
+1
View File
@@ -26,6 +26,7 @@ gem 'pagy'
gem 'pdf-reader' gem 'pdf-reader'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'pretender'
gem 'puma' gem 'puma'
gem 'rack' gem 'rack'
gem 'rails' gem 'rails'
+3
View File
@@ -354,6 +354,8 @@ GEM
actionmailer (>= 3) actionmailer (>= 3)
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pry (0.14.2) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
@@ -596,6 +598,7 @@ DEPENDENCIES
pdf-reader pdf-reader
pg pg
premailer-rails premailer-rails
pretender
pry-rails pry-rails
puma puma
rack rack
+1 -1
View File
@@ -81,7 +81,7 @@ sudo 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 a Meeting](https://calendly.com/kriti-docuseal/30min) [Book a Meeting](https://www.docuseal.co/contact)
## License ## License
@@ -7,6 +7,7 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS = [ ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY AccountConfig::ESIGNING_PREFERENCE_KEY
].freeze ].freeze
+12
View File
@@ -37,6 +37,18 @@ class AccountsController < ApplicationController
render :show, status: :unprocessable_entity render :show, status: :unprocessable_entity
end end
def destroy
authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current)
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
)
end
private private
def load_account def load_account
+7 -1
View File
@@ -8,11 +8,17 @@ module Api
DEFAULT_LIMIT = 10 DEFAULT_LIMIT = 10
MAX_LIMIT = 100 MAX_LIMIT = 100
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
wrap_parameters false wrap_parameters false
before_action :authenticate_user! before_action :authenticate_user!
check_authorization check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
end
if Rails.env.production? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -44,7 +50,7 @@ module Api
if request.headers['X-Auth-Token'].present? if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token']) sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).find_by(access_token: { sha256: }) User.joins(:access_token).active.find_by(access_token: { sha256: })
end end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
+49 -22
View File
@@ -33,12 +33,18 @@ module Api
def show def show
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob) submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
serialized_submitters = submitters.map do |submitter| submitters.each do |submitter|
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at? if submitter.completed_at? && submitter.documents_attachments.blank?
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
Submitters::SerializeForApi.call(submitter) end
end end
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
end
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
json = @submission.as_json( json = @submission.as_json(
serialize_params.deep_merge( serialize_params.deep_merge(
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } } include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
@@ -63,6 +69,8 @@ module Api
end end
def create def create
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank? if @template.fields.blank?
@@ -78,6 +86,12 @@ module Api
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
end
end
render json: submissions.flat_map(&:submitters) render json: submissions.flat_map(&:submitters)
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
@@ -94,7 +108,8 @@ module Api
def create_submissions(template, params) def create_submissions(template, params)
is_send_email = !params[:send_email].in?(['false', false]) is_send_email = !params[:send_email].in?(['false', false])
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank? if (emails = (params[:emails] || params[:email]).presence) &&
(params[:submission].blank? && params[:submitters].blank?)
Submissions.create_from_emails(template:, Submissions.create_from_emails(template:,
user: current_user, user: current_user,
source: :api, source: :api,
@@ -115,8 +130,13 @@ module Api
params: params:
) )
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters = submissions.flat_map(&:submitters)
submissions.flat_map(&:submitters))
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
submitters.each do |submitter|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
end
submissions submissions
end end
@@ -129,8 +149,8 @@ module Api
include: { include: {
submitters: { only: %i[id slug uuid name email phone submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at completed_at opened_at sent_at
created_at updated_at], created_at updated_at external_id metadata],
methods: %i[status] }, methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] }, template: { only: %i[id name created_at updated_at] },
created_by_user: { only: %i[id email first_name last_name] } created_by_user: { only: %i[id email first_name last_name] }
} }
@@ -138,20 +158,27 @@ module Api
end end
def submissions_params def submissions_params
key = params.key?(:submission) ? :submission : :submissions permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}
]
params.permit( if params.key?(:submitters)
key => [ params.permit(*permitted_attrs)
[:send_email, :send_sms, :bcc_completed, :completed_redirect_url, { else
message: %i[subject body], key = params.key?(:submission) ? :submission : :submissions
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, params.permit(
{ values: {}, readonly_fields: [], message: %i[subject body], key => [permitted_attrs]
fields: [%i[name default_value title description ).fetch(key, [])
readonly validation_pattern invalid_message]] }]] end
}]
]
).fetch(key, [])
end end
end end
end end
+11 -5
View File
@@ -7,7 +7,8 @@ module Api
def index def index
submitters = Submitters.search(@submitters, params[:q]) submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present? submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = paginate( submitters = paginate(
@@ -44,7 +45,7 @@ module Api
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission, Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
[normalized_params], [normalized_params],
submitter_uuid: @submitter.uuid) default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params) assign_submitter_attrs(@submitter, normalized_params)
@@ -54,6 +55,8 @@ module Api
@submitter.save! @submitter.save!
@submitter.submission.save! @submitter.submission.save!
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
end end
if @submitter.completed_at? if @submitter.completed_at?
@@ -69,8 +72,9 @@ module Api
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role, :completed, :phone, :application_key, :send_email, :send_sms, :uuid, :name, :email, :role,
{ values: {}, readonly_fields: [], message: %i[subject body], :completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] } fields: [%i[name default_value readonly validation_pattern invalid_message]] }
) )
end end
@@ -82,7 +86,9 @@ module Api
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone) submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present? submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.application_key = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
assign_submission_fields(submitter.submission) assign_submission_fields(submitter.submission)
assign_preferences(submitter, attrs) assign_preferences(submitter, attrs)
@@ -7,11 +7,13 @@ module Api
def create def create
authorize!(:manage, @template) authorize!(:manage, @template)
cloned_template = Templates::Clone.call(@template, cloned_template = Templates::Clone.call(
author: current_user, @template,
name: params[:name], author: current_user,
application_key: params[:application_key], name: params[:name],
folder_name: params[:folder_name]) external_id: params[:external_id].presence || params[:application_key],
folder_name: params[:folder_name]
)
cloned_template.source = :api cloned_template.source = :api
cloned_template.save! cloned_template.save!
+39 -16
View File
@@ -5,11 +5,7 @@ module Api
load_and_authorize_resource :template load_and_authorize_resource :template
def index def index
templates = Templates.search(@templates, params[:q]) templates = filter_templates(@templates, params)
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates = paginate(templates.preload(:author, documents_attachments: :blob)) templates = paginate(templates.preload(:author, documents_attachments: :blob))
@@ -28,40 +24,67 @@ module Api
end end
def update def update
if (folder_name = params.dig(:template, :folder_name)) if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name) @template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
end end
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false])
@template.archived_at = archived == true ? Time.current : nil
end
@template.update!(template_params) @template.update!(template_params)
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
def destroy def destroy
@template.update!(archived_at: Time.current) if params[:permanently] == 'true' && !Docuseal.multitenant?
@template.destroy!
else
@template.update!(archived_at: Time.current)
end
render json: @template.as_json(only: %i[id archived_at]) render json: @template.as_json(only: %i[id archived_at])
end end
private private
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates
end
def serialize_params def serialize_params
{ {
methods: %i[application_key],
include: { author: { only: %i[id email first_name last_name] }, include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } } documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
} }
end end
def template_params def template_params
params.require(:template).permit( permit_params = [
:name, :name,
schema: [%i[attachment_uuid name]], { schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
{ preferences: {}, { preferences: {},
options: [%i[value uuid]], options: [%i[value uuid]],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
) ]
if params.key?(:template)
params.require(:template).permit(*permit_params)
else
params.permit(*permit_params)
end
end end
end end
end end
+12 -1
View File
@@ -16,6 +16,8 @@ class ApplicationController < ActionController::Base
:current_account, :current_account,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do rescue_from Pagy::OverflowError do
redirect_to request.path redirect_to request.path
end end
@@ -24,7 +26,7 @@ class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
redirect_back(fallback_location: root_path, alert: e.message) redirect_to root_path, alert: e.message
end end
end end
@@ -32,6 +34,15 @@ class ApplicationController < ActionController::Base
Docuseal.default_url_options Docuseal.default_url_options
end end
def impersonate_user(user)
raise ArgumentError unless user
raise Pretender::Error unless true_user
@impersonated_user = user
request.session[:impersonated_user_id] = user.uuid
end
private private
def with_browser_locale(&) def with_browser_locale(&)
@@ -5,9 +5,9 @@ class ConsoleRedirectController < ApplicationController
skip_authorization_check skip_authorization_check
def index def index
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank? return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: current_user.uuid, auth = JsonWebToken.encode(uuid: true_user.uuid,
scope: :console, scope: :console,
exp: 1.minute.from_now.to_i) exp: 1.minute.from_now.to_i)
+7 -1
View File
@@ -52,7 +52,13 @@ class DashboardController < ApplicationController
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc) rel = templates.active.preload(:author).order(id: :desc)
rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
if params[:q].blank?
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
Templates.search(rel, params[:q]) Templates.search(rel, params[:q])
end end
+5 -2
View File
@@ -10,7 +10,8 @@ class StartFormController < ApplicationController
before_action :load_template before_action :load_template
def show def show
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid']) @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: @template.submitters.first['uuid'])
end end
def update def update
@@ -57,7 +58,8 @@ class StartFormController < ApplicationController
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true } preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {}
) )
if submitter.values.present? if submitter.values.present?
@@ -67,6 +69,7 @@ class StartFormController < ApplicationController
end end
submitter.submission ||= Submission.new(template:, submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters, template_submitters: template.submitters,
source: :link) source: :link)
+1 -1
View File
@@ -75,6 +75,6 @@ class SubmissionsController < ApplicationController
end end
def load_template def load_template
@template = current_account.templates.find(params[:template_id]) @template = Template.accessible_by(current_ability).find(params[:template_id])
end end
end end
@@ -11,9 +11,7 @@ class SubmittersSendEmailController < ApplicationController
alert: 'Email has been sent already.') alert: 'Email has been sent already.')
end end
SubmitterMailer.invitation_email(@submitter).deliver_later! SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
@submitter.sent_at ||= Time.current @submitter.sent_at ||= Time.current
@submitter.save! @submitter.save!
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class TemplateSharingsTestingController < ApplicationController
load_and_authorize_resource :template, parent: true
before_action do
authorize!(:manage, TemplateSharing.new(template: @template))
end
def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
else
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
end
head :ok
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class TemplatesCodeModalController < ApplicationController
load_and_authorize_resource :template
def show; end
end
+4 -3
View File
@@ -6,7 +6,7 @@ class TemplatesController < ApplicationController
before_action :load_base_template, only: %i[new create] before_action :load_base_template, only: %i[new create]
def show def show
submissions = @template.submissions submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q]) submissions = Submissions.search(submissions, params[:q])
@@ -47,11 +47,12 @@ class TemplatesController < ApplicationController
name: params.dig(:template, :name), name: params.dig(:template, :name),
folder_name: params[:folder_name]) folder_name: params[:folder_name])
else else
@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])
end end
@template.account = current_account
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -87,6 +88,6 @@ class TemplatesController < ApplicationController
def load_base_template def load_base_template
return if params[:base_template_id].blank? return if params[:base_template_id].blank?
@base_template = current_account.templates.find_by(id: params[:base_template_id]) @base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
end end
end end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template
def show
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
).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 :show, layout: 'plain'
end
end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class TestingAccountsController < ApplicationController
skip_authorization_check only: :destroy
def show
authorize!(:manage, current_account)
authorize!(:manage, current_user)
impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
redirect_back(fallback_location: root_path)
end
def destroy
stop_impersonating_user
redirect_back(fallback_location: root_path)
end
end
@@ -0,0 +1,12 @@
# frozen_string_literal: true
class TestingApiSettingsController < ApplicationController
def index
authorize!(:manage, current_user.access_token)
@webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_config)
end
end
+2
View File
@@ -88,7 +88,9 @@ window.customElements.define('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true' isDirectUpload: this.dataset.isDirectUpload === 'true'
}) })
+1 -1
View File
@@ -15,7 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
goToLast: this.dataset.goToLast === 'true', goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false', attribution: this.dataset.attribution !== 'false',
withConfetti: true, withConfetti: this.dataset.withConfetti !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton), completedButton: JSON.parse(this.dataset.completedButton),
+25 -22
View File
@@ -1,9 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base" class="flex absolute lg:text-base -outline-offset-1"
dir="auto" dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed border-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -81,7 +81,8 @@
</div> </div>
<div <div
v-else-if="field.type === 'checkbox'" v-else-if="field.type === 'checkbox'"
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
> >
<input <input
v-if="submittable" v-if="submittable"
@@ -100,14 +101,15 @@
</div> </div>
<div <div
v-else-if="field.type === 'radio' && area.option_uuid" v-else-if="field.type === 'radio' && area.option_uuid"
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
> >
<input <input
v-if="submittable" v-if="submittable"
type="radio" type="radio"
:value="false" :value="false"
class="aspect-square base-radio" class="aspect-square checked:checkbox checked:checkbox-xs"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === optionValue(option)" :checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', optionValue(option))" @click="$emit('update:model-value', optionValue(option))"
> >
@@ -119,7 +121,8 @@
</div> </div>
<div <div
v-else-if="field.type === 'multiple' && area.option_uuid" v-else-if="field.type === 'multiple' && area.option_uuid"
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
> >
<input <input
v-if="submittable" v-if="submittable"
@@ -233,20 +236,20 @@ export default {
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
text: 'Text', text: this.t('text'),
cells: 'Text', signature: this.t('signature'),
signature: 'Signature', initials: this.t('initials'),
date: 'Date', date: this.t('date'),
image: 'Image', image: this.t('image'),
stamp: 'Stamp', file: this.t('file'),
initials: 'Initials', select: this.t('select'),
file: 'File', checkbox: this.t('checkbox'),
select: 'Select', multiple: this.t('multiple'),
checkbox: 'Checkbox', radio: this.t('radio'),
radio: 'Radio', cells: this.t('cells'),
multiple: 'Multiple Select', stamp: this.t('stamp'),
phone: 'Phone', payment: this.t('payment'),
payment: 'Payment' phone: this.t('phone')
} }
}, },
option () { option () {
@@ -304,7 +307,7 @@ export default {
formattedDate () { formattedDate () {
if (this.field.type === 'date' && this.modelValue) { if (this.field.type === 'date' && this.modelValue) {
return this.formatDate( return this.formatDate(
new Date(this.modelValue), this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY') this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
) )
} else { } else {
@@ -42,7 +42,7 @@
> >
</template> </template>
<FileDropzone <FileDropzone
:message="`Upload ${field.name || 'Attachments'}${field.required ? '' : ' (optional)'}`" :message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:multiple="true" :multiple="true"
@@ -62,6 +62,7 @@ export default {
IconPaperclip, IconPaperclip,
IconTrashX IconTrashX
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
+6 -1
View File
@@ -4,7 +4,7 @@
<label <label
:for="field.uuid" :for="field.uuid"
class="label text-2xl" class="label text-2xl"
>{{ field.name || t('date') }} >{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<button <button
@@ -46,6 +46,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
+46 -9
View File
@@ -4,7 +4,7 @@
:steps="stepFields" :steps="stepFields"
:values="values" :values="values"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:with-label="!isAnonymousChecboxes" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']" @focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/> />
@@ -64,6 +64,7 @@
<TextStep <TextStep
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
@@ -72,12 +73,13 @@
v-else-if="currentField.type === 'date'" v-else-if="currentField.type === 'date'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
<label <label
v-if="currentField.name" v-if="showFieldNames && currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
@@ -116,15 +118,28 @@
</div> </div>
<div v-else-if="currentField.type === 'radio'"> <div v-else-if="currentField.type === 'radio'">
<label <label
v-if="currentField.name" v-if="showFieldNames && currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
</label> </label>
<div class="flex w-full"> <div class="flex w-full max-h-44 overflow-y-auto">
<div class="space-y-3.5 mx-auto"> <div
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
class="text-xl px-1"
>
<span
@click="scrollIntoField(currentField)"
>
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
>
<div <div
v-for="(option, index) in currentField.options" v-for="(option, index) in currentField.options"
:key="option.uuid" :key="option.uuid"
@@ -154,6 +169,8 @@
v-else-if="currentField.type === 'multiple'" v-else-if="currentField.type === 'multiple'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:is-last-step="stepFields.length === currentStep + 1"
:field="currentField" :field="currentField"
/> />
<div <div
@@ -168,7 +185,7 @@
<div <div
class="space-y-3.5 mx-auto" class="space-y-3.5 mx-auto"
> >
<template v-if="isAnonymousChecboxes"> <template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl"> <span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>. {{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span> </span>
@@ -228,6 +245,7 @@
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:show-field-names="showFieldNames"
@attached="[attachments.push($event), scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<SignatureStep <SignatureStep
@@ -241,6 +259,7 @@
:with-typed-signature="withTypedSignature" :with-typed-signature="withTypedSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:show-field-names="showFieldNames"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
@@ -254,6 +273,7 @@
:previous-value="previousInitialsValue" :previous-value="previousInitialsValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:show-field-names="showFieldNames"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@@ -276,6 +296,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:show-field-names="showFieldNames"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@@ -376,7 +397,7 @@ import DateStep from './date_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import { t } from './i18n' import i18n from './i18n'
export default { export default {
name: 'SubmissionForm', name: 'SubmissionForm',
@@ -401,6 +422,7 @@ export default {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea, scrollIntoArea: this.scrollIntoArea,
scrollIntoField: this.scrollIntoField,
t: this.t t: this.t
} }
}, },
@@ -431,6 +453,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
withTypedSignature: { withTypedSignature: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -481,6 +508,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
language: {
type: String,
required: false,
default: ''
},
values: { values: {
type: Object, type: Object,
required: false, required: false,
@@ -522,6 +554,9 @@ export default {
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] return this.stepFields[this.currentStep]
}, },
browserLanguage () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
},
queryParams () { queryParams () {
return new URLSearchParams(window.location.search) return new URLSearchParams(window.location.search)
}, },
@@ -586,7 +621,7 @@ export default {
this.fields.forEach((field) => { this.fields.forEach((field) => {
if (field.default_value && !field.readonly) { if (field.default_value && !field.readonly) {
this.values[field.uuid] = field.default_value this.values[field.uuid] ||= field.default_value
} }
}) })
@@ -639,7 +674,9 @@ export default {
}) })
}, },
methods: { methods: {
t, t (key) {
return i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
},
maybeTrackEmailClick () { maybeTrackEmailClick () {
const { queryParams } = this const { queryParams } = this
+253 -45
View File
@@ -1,4 +1,19 @@
const en = { const en = {
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
take_photo: 'Take photo',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
submit_form: 'Submit Form', submit_form: 'Submit Form',
type_here_: 'Type here...', type_here_: 'Type here...',
optional: 'optional', optional: 'optional',
@@ -13,8 +28,6 @@ const en = {
or_drag_and_drop_files: 'or drag and drop files', or_drag_and_drop_files: 'or drag and drop files',
send_copy_via_email: 'Send copy via email', send_copy_via_email: 'Send copy via email',
download: 'Download', download: 'Download',
signature: 'Signature',
initials: 'Initials',
clear: 'Clear', clear: 'Clear',
redraw: 'Redraw', redraw: 'Redraw',
draw_initials: 'Draw initials', draw_initials: 'Draw initials',
@@ -40,13 +53,30 @@ const en = {
draw: 'Draw', draw: 'Draw',
type: 'Type', type: 'Type',
type_text: 'Type text', type_text: 'Type text',
date: 'Date',
email_has_been_sent: 'Email has been sent', email_has_been_sent: 'Email has been sent',
processing: 'Processing', processing: 'Processing',
pay_with_strip: 'Pay with Stripe' pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files'
} }
const es = { const es = {
text: 'Texto',
signature: 'Firma',
initials: 'Iniciales',
date: 'Fecha',
image: 'Imagen',
file: 'Archivo',
select: 'Seleccionar',
checkbox: 'Casilla',
multiple: 'Múltiple',
radio: 'Radio',
cells: 'Celdas',
stamp: 'Sello',
payment: 'Pago',
phone: 'Teléfono',
take_photo: 'Tomar foto',
submit_form: 'Enviar Formulario', submit_form: 'Enviar Formulario',
type_here_: 'Escribe aquí...', type_here_: 'Escribe aquí...',
optional: 'opcional', optional: 'opcional',
@@ -60,8 +90,6 @@ const es = {
or_drag_and_drop_files: 'o arrastra y suelta archivos', or_drag_and_drop_files: 'o arrastra y suelta archivos',
send_copy_via_email: 'Enviar copia por correo electrónico', send_copy_via_email: 'Enviar copia por correo electrónico',
download: 'Descargar', download: 'Descargar',
signature: 'Firma',
initials: 'Iniciales',
clear: 'Borrar', clear: 'Borrar',
redraw: 'Redibujar', redraw: 'Redibujar',
draw_initials: 'Dibujar iniciales', draw_initials: 'Dibujar iniciales',
@@ -81,7 +109,6 @@ const es = {
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', please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
set_today: 'Establecer Hoy', set_today: 'Establecer Hoy',
date: 'Fecha',
toggle_multiline_text: 'Alternar Texto Multilínea', toggle_multiline_text: 'Alternar Texto Multilínea',
draw_signature: 'Dibujar firma', draw_signature: 'Dibujar firma',
type_initial: 'Escribir iniciales', type_initial: 'Escribir iniciales',
@@ -90,15 +117,33 @@ const es = {
type_text: 'Escribir texto', type_text: 'Escribir texto',
email_has_been_sent: 'El correo electrónico ha sido enviado', email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando', processing: 'Procesando',
pay_with_strip: 'Pagar con Stripe' pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos'
} }
const it = { const it = {
text: 'Testo',
signature: 'Firma',
initials: 'Iniziali',
date: 'Data',
image: 'Immagine',
file: 'File',
select: 'Seleziona',
checkbox: 'Casella di controllo',
multiple: 'Multiplo',
radio: 'Radio',
cells: 'Celle',
stamp: 'Timbro',
payment: 'Pagamento',
phone: 'Telefono',
submit_form: 'Invia Modulo', submit_form: 'Invia Modulo',
type_here_: 'Digita qui...', type_here_: 'Digita qui...',
optional: 'opzionale', optional: 'opzionale',
appears_on: 'Compare su', appears_on: 'Compare su',
page: 'Pagina', page: 'Pagina',
take_photo: 'Scattare una foto',
select_your_option: 'Seleziona la tua opzione', select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic', complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia', submit: 'invia',
@@ -107,8 +152,6 @@ const it = {
or_drag_and_drop_files: 'oppure trascina e rilascia i file', or_drag_and_drop_files: 'oppure trascina e rilascia i file',
send_copy_via_email: 'Invia copia via email', send_copy_via_email: 'Invia copia via email',
download: 'Scarica', download: 'Scarica',
signature: 'Firma',
initials: 'Iniziali',
clear: 'Cancella', clear: 'Cancella',
redraw: 'Ridisegna', redraw: 'Ridisegna',
draw_initials: 'Disegna iniziali', draw_initials: 'Disegna iniziali',
@@ -128,7 +171,6 @@ const it = {
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', please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
set_today: 'Imposta Oggi', set_today: 'Imposta Oggi',
date: 'Data',
draw_signature: 'Disegna firma', draw_signature: 'Disegna firma',
type_initial: 'Inserisci iniziali', type_initial: 'Inserisci iniziali',
draw: 'Disegna', draw: 'Disegna',
@@ -137,15 +179,33 @@ const it = {
toggle_multiline_text: 'Attiva Testo Multilinea', toggle_multiline_text: 'Attiva Testo Multilinea',
email_has_been_sent: "L'email è stata inviata", email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione', processing: 'Elaborazione',
pay_with_strip: 'Paga con Stripe' pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File'
} }
const de = { const de = {
text: 'Text',
signature: 'Unterschrift',
initials: 'Initialen',
date: 'Datum',
image: 'Bild',
file: 'Datei',
select: 'Auswählen',
checkbox: 'Checkbox',
multiple: 'Mehrere',
radio: 'Radio',
cells: 'Zellen',
stamp: 'Stempel',
payment: 'Zahlung',
phone: 'Telefon',
submit_form: 'Formular absenden', submit_form: 'Formular absenden',
type_here_: 'Hier eingeben...', type_here_: 'Hier eingeben...',
optional: 'optional', optional: 'optional',
appears_on: 'Erscheint auf', appears_on: 'Erscheint auf',
page: 'Seite', page: 'Seite',
take_photo: 'Foto aufnehmen',
select_your_option: 'Wähle deine Option', select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken', complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden', submit: 'absenden',
@@ -154,8 +214,6 @@ const de = {
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen', or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
send_copy_via_email: 'Kopie per E-Mail senden', send_copy_via_email: 'Kopie per E-Mail senden',
download: 'Herunterladen', download: 'Herunterladen',
signature: 'Unterschrift',
initials: 'Initialen',
clear: 'Löschen', clear: 'Löschen',
redraw: 'Neu zeichnen', redraw: 'Neu zeichnen',
draw_initials: 'Initialen zeichnen', draw_initials: 'Initialen zeichnen',
@@ -175,7 +233,6 @@ const de = {
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', please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
set_today: 'Heute einstellen', set_today: 'Heute einstellen',
date: 'Datum',
draw_signature: 'Unterschrift zeichnen', draw_signature: 'Unterschrift zeichnen',
type_initial: 'Initialen eingeben', type_initial: 'Initialen eingeben',
draw: 'Zeichnen', draw: 'Zeichnen',
@@ -184,15 +241,33 @@ const de = {
toggle_multiline_text: 'Mehrzeiligen Text umschalten', toggle_multiline_text: 'Mehrzeiligen Text umschalten',
email_has_been_sent: 'Die E-Mail wurde gesendet', email_has_been_sent: 'Die E-Mail wurde gesendet',
processing: 'Verarbeitung', processing: 'Verarbeitung',
pay_with_strip: 'Mit Stripe bezahlen' pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien'
} }
const fr = { const fr = {
text: 'Texte',
signature: 'Signature',
initials: 'Initiales',
date: 'Date',
image: 'Image',
file: 'Fichier',
select: 'Choisir',
checkbox: 'Coche',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cellules',
stamp: 'Tampon',
payment: 'Paiement',
phone: 'Téléphone',
submit_form: 'Envoyer le Formulaire', submit_form: 'Envoyer le Formulaire',
type_here_: 'Tapez ici...', type_here_: 'Tapez ici...',
optional: 'facultatif', optional: 'facultatif',
appears_on: 'Apparaît sur', appears_on: 'Apparaît sur',
page: 'Page', page: 'Page',
take_photo: 'Prendre une photo',
select_your_option: 'Sélectionnez votre option', select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez', complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer', submit: 'envoyer',
@@ -201,8 +276,6 @@ const fr = {
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers', or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
send_copy_via_email: 'Envoyer une copie par e-mail', send_copy_via_email: 'Envoyer une copie par e-mail',
download: 'Télécharger', download: 'Télécharger',
signature: 'Signature',
initials: 'Initiales',
clear: 'Effacer', clear: 'Effacer',
redraw: 'Redessiner', redraw: 'Redessiner',
draw_initials: 'Dessiner les initiales', draw_initials: 'Dessiner les initiales',
@@ -222,7 +295,6 @@ const fr = {
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', please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
set_today: "Définir Aujourd'hui", set_today: "Définir Aujourd'hui",
date: 'Date',
draw_signature: 'Dessiner une signature', draw_signature: 'Dessiner une signature',
type_initial: 'Saisir les initiales', type_initial: 'Saisir les initiales',
draw: 'Dessiner', draw: 'Dessiner',
@@ -231,10 +303,27 @@ const fr = {
toggle_multiline_text: 'Basculer le Texte Multiligne', toggle_multiline_text: 'Basculer le Texte Multiligne',
email_has_been_sent: "L'email a été envoyé", email_has_been_sent: "L'email a été envoyé",
processing: 'Traitement', processing: 'Traitement',
pay_with_strip: 'Paiement avec Stripe' pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers'
} }
const pl = { const pl = {
text: 'Tekst',
signature: 'Podpis',
initials: 'Inicjały',
date: 'Data',
image: 'Obraz',
file: 'Plik',
select: 'Wybierz',
checkbox: 'Pole wyboru',
multiple: 'Wielokrotne',
radio: 'Radio',
cells: 'Komórki',
stamp: 'Pieczątka',
payment: 'Płatność',
phone: 'Telefon',
submit_form: 'Wyślij Formularz', submit_form: 'Wyślij Formularz',
type_here_: 'Wpisz tutaj...', type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny', optional: 'opcjonalny',
@@ -244,12 +333,11 @@ const pl = {
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij', complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij', submit: 'wyślij',
next: 'dalej', next: 'dalej',
take_photo: 'Zrób zdjęcie',
click_to_upload: 'Kliknij, aby przesłać', click_to_upload: 'Kliknij, aby przesłać',
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki', or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
send_copy_via_email: 'Wyślij kopię drogą mailową', send_copy_via_email: 'Wyślij kopię drogą mailową',
download: 'Pobierz', download: 'Pobierz',
signature: 'Podpis',
initials: 'Inicjały',
clear: 'Wyczyść', clear: 'Wyczyść',
redraw: 'Przerysuj', redraw: 'Przerysuj',
draw_initials: 'Narysuj inicjały', draw_initials: 'Narysuj inicjały',
@@ -269,7 +357,6 @@ const pl = {
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', please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
set_today: 'Ustaw Dziś', set_today: 'Ustaw Dziś',
date: 'Data',
draw_signature: 'Rysuj podpis', draw_signature: 'Rysuj podpis',
type_initial: 'Wprowadź inicjały', type_initial: 'Wprowadź inicjały',
draw: 'Rysuj', draw: 'Rysuj',
@@ -278,15 +365,33 @@ const pl = {
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy', toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany', email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie', processing: 'Przetwarzanie',
pay_with_strip: 'Płatność za pomocą Stripe' pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki'
} }
const uk = { const uk = {
text: 'Текст',
signature: 'Підпис',
initials: 'Ініціали',
date: 'Дата',
image: 'Зображення',
file: 'Файл',
select: 'Вибрати',
checkbox: 'Прапорець',
multiple: 'Множинний',
radio: 'Радіо',
cells: 'Клітинки',
stamp: 'Штамп',
payment: 'Платіж',
phone: 'Телефон',
submit_form: 'Надіслати Форму', submit_form: 'Надіслати Форму',
type_here_: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
appears_on: "З'являється на", appears_on: "З'являється на",
page: 'Сторінка', page: 'Сторінка',
take_photo: 'Зробити фото',
select_your_option: 'Виберіть варіант', select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть', complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати', submit: 'надіслати',
@@ -295,8 +400,6 @@ const uk = {
or_drag_and_drop_files: 'або перетягніть файли сюди', or_drag_and_drop_files: 'або перетягніть файли сюди',
send_copy_via_email: 'Надіслати копію електронною поштою', send_copy_via_email: 'Надіслати копію електронною поштою',
download: 'Завантажити', download: 'Завантажити',
signature: 'Підпис',
initials: 'Ініціали',
clear: 'Очистити', clear: 'Очистити',
redraw: 'Перемалювати', redraw: 'Перемалювати',
draw_initials: 'Намалювати ініціали', draw_initials: 'Намалювати ініціали',
@@ -316,7 +419,6 @@ const uk = {
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий', verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля", please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
set_today: 'Задати Сьогодні', set_today: 'Задати Сьогодні',
date: 'Дата',
draw_signature: 'Намалюйте підпис', draw_signature: 'Намалюйте підпис',
type_initial: 'Введіть ініціали', type_initial: 'Введіть ініціали',
draw: 'Підпис', draw: 'Підпис',
@@ -325,16 +427,34 @@ const uk = {
toggle_multiline_text: 'Перемкнути Багаторядковий Текст', toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений', email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка', processing: 'Обробка',
pay_with_strip: 'Сплатити за допомогою Stripe' pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
} }
const cs = { const cs = {
text: 'Text',
signature: 'Podpis',
initials: 'Iniciály',
date: 'Datum',
image: 'Obrázek',
file: 'Soubor',
select: 'Vybrat',
checkbox: 'Zaškrtávací políčko',
multiple: 'Více možností',
radio: 'Rádio',
cells: 'Buňky',
stamp: 'Razítko',
payment: 'Platba',
phone: 'Telefon',
submit_form: 'Odeslat formulář', submit_form: 'Odeslat formulář',
type_here_: 'Zadejte zde', type_here_: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
appears_on: 'Zobrazuje se na', appears_on: 'Zobrazuje se na',
page: 'Stránka', page: 'Stránka',
select_your_option: 'Vyberte svou volbu', select_your_option: 'Vyberte svou volbu',
take_photo: 'Fotit',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na', complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat', submit: 'odeslat',
next: 'další', next: 'další',
@@ -342,8 +462,6 @@ const cs = {
or_drag_and_drop_files: 'nebo přetáhněte soubory sem', or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
send_copy_via_email: 'Odeslat kopii e-mailem', send_copy_via_email: 'Odeslat kopii e-mailem',
download: 'Stáhnout', download: 'Stáhnout',
signature: 'Podpis',
initials: 'Iniciály',
clear: 'Smazat', clear: 'Smazat',
redraw: 'Překreslit', redraw: 'Překreslit',
draw_initials: 'Nakreslit iniciály', draw_initials: 'Nakreslit iniciály',
@@ -363,7 +481,6 @@ const cs = {
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', please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
set_today: 'Nastavit Dnes', set_today: 'Nastavit Dnes',
date: 'Datum',
draw_signature: 'Nakreslit podpis', draw_signature: 'Nakreslit podpis',
type_initial: 'Zadat iniciály', type_initial: 'Zadat iniciály',
draw: 'Kreslit', draw: 'Kreslit',
@@ -372,15 +489,33 @@ const cs = {
toggle_multiline_text: 'Přepnout Víceřádkový Text', toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán', email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování', processing: 'Zpracování',
pay_with_strip: 'Zaplacení přes Stripe' pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory'
} }
const pt = { const pt = {
text: 'Texto',
signature: 'Assinatura',
initials: 'Iniciais',
date: 'Data',
image: 'Imagem',
file: 'Arquivo',
select: 'Selecionar',
checkbox: 'Caixa',
multiple: 'Múltiplo',
radio: 'Rádio',
cells: 'Células',
stamp: 'Carimbo',
payment: 'Pagamento',
phone: 'Telefone',
submit_form: 'Enviar Formulário', submit_form: 'Enviar Formulário',
type_here_: 'Digite aqui', type_here_: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece em', appears_on: 'Aparece em',
page: 'Página', page: 'Página',
take_photo: 'Tirar foto',
select_your_option: 'Selecione sua opção', select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique', complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar', submit: 'enviar',
@@ -389,8 +524,6 @@ const pt = {
or_drag_and_drop_files: 'ou arraste e solte arquivos', or_drag_and_drop_files: 'ou arraste e solte arquivos',
send_copy_via_email: 'Enviar cópia por e-mail', send_copy_via_email: 'Enviar cópia por e-mail',
download: 'Baixar', download: 'Baixar',
signature: 'Assinatura',
initials: 'Iniciais',
clear: 'Limpar', clear: 'Limpar',
redraw: 'Redesenhar', redraw: 'Redesenhar',
draw_initials: 'Desenhar iniciais', draw_initials: 'Desenhar iniciais',
@@ -410,7 +543,6 @@ const pt = {
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', please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
set_today: 'Definir Hoje', set_today: 'Definir Hoje',
date: 'Data',
draw_signature: 'Desenhar assinatura', draw_signature: 'Desenhar assinatura',
type_initial: 'Inserir iniciais', type_initial: 'Inserir iniciais',
draw: 'Desenhar', draw: 'Desenhar',
@@ -419,16 +551,34 @@ const pt = {
toggle_multiline_text: 'Alternar Texto Multilinha', toggle_multiline_text: 'Alternar Texto Multilinha',
email_has_been_sent: 'Email enviado', email_has_been_sent: 'Email enviado',
processing: 'Processamento', processing: 'Processamento',
pay_with_strip: 'Pagar com Stripe' pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos'
} }
const he = { const he = {
text: 'טקסט',
signature: 'חתימה',
initials: 'ראשי תיקיות',
date: 'תאריך',
image: 'תמונה',
file: 'קובץ',
select: 'בחר',
checkbox: 'תיק בחירה',
multiple: 'רב ערכים',
radio: 'רדיו',
cells: 'תאים',
stamp: 'חותמת',
payment: 'תשלום',
phone: 'טלפון',
submit_form: 'שלח טופס', submit_form: 'שלח טופס',
type_here_: 'הקלד כאן', type_here_: 'הקלד כאן',
optional: 'אופציונלי', optional: 'אופציונלי',
option: 'אפשרות', option: 'אפשרות',
appears_on: 'מופיע ב', appears_on: 'מופיע ב',
page: 'עמוד', page: 'עמוד',
take_photo: 'צלם תמונה',
select_your_option: 'בחר את האפשרות שלך', select_your_option: 'בחר את האפשרות שלך',
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ', complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
submit: 'שליחה', submit: 'שליחה',
@@ -437,8 +587,6 @@ const he = {
or_drag_and_drop_files: 'או גרור ושחרר קבצים', or_drag_and_drop_files: 'או גרור ושחרר קבצים',
send_copy_via_email: 'שלח עותק באמצעות אימייל', send_copy_via_email: 'שלח עותק באמצעות אימייל',
download: 'הורדה', download: 'הורדה',
signature: 'חתימה',
initials: 'ציוני ראשי תיבות',
clear: 'נקה', clear: 'נקה',
redraw: 'צייר מחדש', redraw: 'צייר מחדש',
draw_initials: 'צייר ציוני ראשי תיבות', draw_initials: 'צייר ציוני ראשי תיבות',
@@ -464,17 +612,77 @@ const he = {
draw: 'צייר', draw: 'צייר',
type: 'הקלד', type: 'הקלד',
type_text: 'הקלד טקסט', type_text: 'הקלד טקסט',
date: 'תאריך',
email_has_been_sent: 'האימייל נשלח', email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד', processing: 'מעבד',
pay_with_strip: 'שלם עם סטרייפ' pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he } const nl = {
text: 'Tekst',
signature: 'Handtekening',
initials: 'Initialen',
date: 'Datum',
image: 'Afbeelding',
take_photo: 'Maak een foto',
file: 'Bestand',
select: 'Selecteer',
checkbox: 'Aankruisvakje',
multiple: 'Meerdere',
radio: 'Radio',
cells: 'Cellen',
stamp: 'Stempel',
payment: 'Betaling',
phone: 'Telefoon',
submit_form: 'Formulier verzenden',
type_here_: 'Typ hier...',
optional: 'Optioneel',
option: 'Optie',
appears_on: 'Verschijnt op',
page: 'Pagina',
select_your_option: 'Selecteer uw optie',
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
submit: 'Voltooien',
next: 'Volgende',
click_to_upload: 'Klik om te uploaden',
or_drag_and_drop_files: 'of sleep en plaats bestanden',
send_copy_via_email: 'Stuur kopie per e-mail',
download: 'Downloaden',
clear: 'Wissen',
redraw: 'Opnieuw tekenen',
draw_initials: 'Initialen tekenen',
type_signature_here: 'Typ hier uw handtekening',
type_initial_here: 'Typ hier uw initialen',
form_has_been_completed: 'Formulier is voltooid!',
create_a_free_account: 'Maak een gratis account aan',
signed_with: 'Ondertekend met',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer',
sending: 'Voltooien...',
resend_code: 'Code opnieuw verzenden',
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
please_fill_all_required_fields: 'Vul alle verplichte velden in',
set_today: 'Vandaag',
toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
draw_signature: 'Handtekening tekenen',
type_initial: 'Typ initialen',
draw: 'Tekenen',
type: 'Typen',
type_text: 'Typ tekst',
email_has_been_sent: 'E-mail is verzonden',
processing: 'Verwerken',
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden'
}
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0] const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
export default i18n export default i18n
export { t }
@@ -3,13 +3,13 @@
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || 'Image' }}</label> >{{ showFieldNames && field.name ? field.name : t('image') }}</label>
<button <button
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
Reupload {{ t('reupload') }}
</button> </button>
</div> </div>
<div> <div>
@@ -27,7 +27,7 @@
<div> <div>
<FileDropzone <FileDropzone
v-if="!modelValue" v-if="!modelValue"
:message="`Upload ${field.name || 'Image'}${field.required ? '' : ' (optional)'}`" :message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:accept="'image/*'" :accept="'image/*'"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -46,6 +46,7 @@ export default {
FileDropzone, FileDropzone,
IconReload IconReload
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -56,6 +57,11 @@ export default {
required: true, required: true,
default: false default: false
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
@@ -3,7 +3,7 @@
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || t('initials') }}</label> >{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
v-if="isDrawInitials" v-if="isDrawInitials"
@@ -84,7 +84,7 @@
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded-2xl" class="bg-white border border-base-300 rounded-2xl w-full"
/> />
<input <input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue" v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
@@ -106,6 +106,8 @@ import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 }
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
const scale = 3
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
@@ -125,6 +127,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: true,
@@ -166,8 +173,10 @@ export default {
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 5 this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale)
} }
this.$refs.textInput?.focus() this.$refs.textInput?.focus()
@@ -185,8 +194,26 @@ export default {
this.$emit('start') this.$emit('start')
}) })
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale)
this.intersectionObserver?.disconnect()
}
})
})
this.intersectionObserver.observe(this.$refs.canvas)
} }
}, },
beforeUnmount () {
this.intersectionObserver?.disconnect()
},
methods: { methods: {
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
@@ -217,8 +244,8 @@ export default {
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center' context.textAlign = 'center'
context.clearRect(0, 0, canvas.width, canvas.height) context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11) context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
}, },
toggleTextInput () { toggleTextInput () {
this.remove() this.remove()
@@ -1,11 +1,11 @@
<template> <template>
<label <label
v-if="field.name" v-if="showFieldNames && field.name"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }}</label> >{{ field.name }}</label>
<div class="flex w-full"> <div class="flex w-full max-h-44 overflow-y-auto">
<input <input
v-if="modelValue.length === 0" v-if="modelValue.length === 0"
type="text" type="text"
@@ -13,7 +13,18 @@
:value="''" :value="''"
class="hidden" class="hidden"
> >
<div class="space-y-3.5 mx-auto"> <div
v-if="!showOptions"
class="text-xl px-1"
>
<span @click="scrollIntoField(field)">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ isLastStep ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showOptions }"
>
<div <div
v-for="(option, index) in field.options" v-for="(option, index) in field.options"
:key="option.uuid" :key="option.uuid"
@@ -44,12 +55,22 @@
<script> <script>
export default { export default {
name: 'MultiSelectStep', name: 'MultiSelectStep',
inject: ['t'], inject: ['t', 'scrollIntoField'],
props: { props: {
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
isLastStep: {
type: Boolean,
required: true,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: Array, type: Array,
required: false, required: false,
@@ -62,6 +83,11 @@ export default {
inputRefs: [] inputRefs: []
} }
}, },
computed: {
showOptions () {
return this.showFieldNames && (this.field.options.some((e) => e.value) || this.field.options.length < 5)
}
},
beforeUpdate () { beforeUpdate () {
this.inputRefs = [] this.inputRefs = []
}, },
+11 -2
View File
@@ -3,7 +3,7 @@
<label <label
:for="isCodeSent ? 'one_time_code' : field.uuid" :for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name || t('verified_phone_number') }} >{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<div> <div>
@@ -93,6 +93,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -140,7 +145,11 @@ export default {
}) })
}, },
async submit () { async submit () {
if (!this.isCodeSent) { if (!this.$refs.phone.value.toString().startsWith('+')) {
alert(this.t('use_international_format'))
return Promise.reject(new Error('phone invalid'))
} else if (!this.isCodeSent) {
this.sendVerificationCode() this.sendVerificationCode()
this.$emit('update:model-value', this.$refs.phone.value) this.$emit('update:model-value', this.$refs.phone.value)
@@ -3,7 +3,7 @@
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || t('signature') }}</label> >{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
v-if="isTextSignature" v-if="isTextSignature"
@@ -41,7 +41,7 @@
</span> </span>
<span <span
class="tooltip" class="tooltip"
data-tip="Take photo" :data-tip="t('take_photo')"
> >
<label <label
class="btn btn-outline btn-sm font-medium" class="btn btn-outline btn-sm font-medium"
@@ -104,7 +104,7 @@
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
style="padding: 1px; 0" style="padding: 1px; 0"
class="bg-white border border-base-300 rounded-2xl" class="bg-white border border-base-300 rounded-2xl w-full"
/> />
<input <input
v-if="isTextSignature" v-if="isTextSignature"
@@ -127,6 +127,8 @@ import AppearsOn from './appears_on'
let isFontLoaded = false let isFontLoaded = false
const scale = 3
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
components: { components: {
@@ -147,6 +149,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: true,
@@ -193,8 +200,10 @@ export default {
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3 this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
this.$refs.canvas.getContext('2d').scale(scale, scale)
} }
}) })
@@ -210,8 +219,26 @@ export default {
this.$emit('start') this.$emit('start')
}) })
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
this.$refs.canvas.getContext('2d').scale(scale, scale)
this.intersectionObserver?.disconnect()
}
})
})
this.intersectionObserver.observe(this.$refs.canvas)
} }
}, },
beforeUnmount () {
this.intersectionObserver?.disconnect()
},
methods: { methods: {
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
@@ -221,7 +248,7 @@ export default {
}, },
loadFont () { loadFont () {
if (!isFontLoaded) { if (!isFontLoaded) {
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`) const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
font.load().then((loadedFont) => { font.load().then((loadedFont) => {
document.fonts.add(loadedFont) document.fonts.add(loadedFont)
@@ -255,11 +282,12 @@ export default {
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center' context.textAlign = 'center'
context.clearRect(0, 0, canvas.width, canvas.height) context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11) context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
}, },
toggleTextInput () { toggleTextInput () {
this.remove() this.remove()
this.clear()
this.isTextSignature = !this.isTextSignature this.isTextSignature = !this.isTextSignature
if (this.isTextSignature) { if (this.isTextSignature) {
@@ -291,14 +319,16 @@ export default {
const context = canvas.getContext('2d') const context = canvas.getContext('2d')
const aspectRatio = img.width / img.height const aspectRatio = img.width / img.height
const canvasWidth = canvas.width / scale
const canvasHeight = canvas.height / scale
let targetWidth = canvas.width let targetWidth = canvasWidth
let targetHeight = canvas.height let targetHeight = canvasHeight
if (canvas.width / canvas.height > aspectRatio) { if (canvasWidth / canvasHeight > aspectRatio) {
targetWidth = canvas.height * aspectRatio targetWidth = canvasHeight * aspectRatio
} else { } else {
targetHeight = canvas.width / aspectRatio targetHeight = canvasWidth / aspectRatio
} }
if (targetHeight > targetWidth) { if (targetHeight > targetWidth) {
@@ -307,10 +337,10 @@ export default {
targetHeight = targetHeight * scale targetHeight = targetHeight * scale
} }
const x = (canvas.width - targetWidth) / 2 const x = (canvasWidth - targetWidth) / 2
const y = (canvas.height - targetHeight) / 2 const y = (canvasHeight - targetHeight) / 2
context.clearRect(0, 0, canvas.width, canvas.height) context.clearRect(0, 0, canvasWidth, canvasHeight)
context.drawImage(img, x, y, targetWidth, targetHeight) context.drawImage(img, x, y, targetWidth, targetHeight)
this.$emit('start') this.$emit('start')
@@ -318,6 +348,8 @@ export default {
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
event.target.value = null
} }
}, },
async submit () { async submit () {
+16 -8
View File
@@ -1,6 +1,6 @@
<template> <template>
<label <label
v-if="field.name" v-if="showFieldNames && field.name"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-2xl" class="label text-2xl"
@@ -26,7 +26,8 @@
v-model="text" v-model="text"
:maxlength="cellsMaxLegth" :maxlength="cellsMaxLegth"
dir="auto" dir="auto"
class="base-input !text-2xl w-full !pr-11 -mr-10" class="base-input !text-2xl w-full"
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''" :oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
@@ -50,7 +51,7 @@
@focus="$emit('focus')" @focus="$emit('focus')"
/> />
<div <div
v-if="!isTextArea && field.type !== 'cells'" v-if="!isTextArea && field.type !== 'cells' && !field.validation?.pattern"
class="tooltip" class="tooltip"
:data-tip="t('toggle_multiline_text')" :data-tip="t('toggle_multiline_text')"
> >
@@ -81,6 +82,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -117,12 +123,14 @@ export default {
} }
}, },
mounted () { mounted () {
this.isTextArea = this.modelValue?.includes('\n') if (this.modelValue) {
this.isTextArea = this.modelValue.toString().includes('\n')
if (this.isTextArea) { if (this.isTextArea) {
this.$nextTick(() => { this.$nextTick(() => {
this.resizeTextarea() this.resizeTextarea()
}) })
}
} }
}, },
methods: { methods: {
+42 -4
View File
@@ -43,7 +43,8 @@
class="border-r" class="border-r"
:compact="true" :compact="true"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'" :allow-add-new="!defaultSubmitters.length"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
:submitters="template.submitters" :submitters="template.submitters"
@update:model-value="save" @update:model-value="save"
@click="selectedAreaRef.value = area" @click="selectedAreaRef.value = area"
@@ -97,21 +98,32 @@
</div> </div>
<div <div
class="flex items-center h-full w-full" class="flex items-center h-full w-full"
dir="auto"
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']" :class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
> >
<span <span
v-if="field" v-if="field"
class="flex justify-center items-center space-x-1 h-full" class="flex justify-center items-center space-x-1 h-full"
:class="{'w-full h-full': field.type == 'checkbox'}"
> >
<div <div
v-if="field?.default_value" v-if="field?.default_value"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }" :class="{ 'w-full h-full': field.type == 'checkbox', 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
> >
<div <div
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
:class="{'w-full h-full': field.type == 'checkbox'}"
> >
<span class="whitespace-pre-wrap">{{ field.default_value }}</span> <IconCheck
v-if="field.type == 'checkbox'"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
<span
v-else
class="whitespace-pre-wrap"
>{{ field.default_value }}</span>
</div> </div>
</div> </div>
<component <component
@@ -140,13 +152,14 @@
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import FieldType from './field_type' import FieldType from './field_type'
import Field from './field' import Field from './field'
import { IconX } from '@tabler/icons-vue' import { IconX, IconCheck } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
components: { components: {
FieldType, FieldType,
IconCheck,
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
@@ -166,6 +179,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -219,6 +237,16 @@ export default {
}, },
borderColors () { borderColors () {
return [ return [
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
'border-yellow-300/80',
'border-purple-600/80',
'border-pink-500/80',
'border-cyan-500/80',
'border-orange-500/80',
'border-lime-500/80',
'border-indigo-500/80',
'border-red-500/80', 'border-red-500/80',
'border-sky-500/80', 'border-sky-500/80',
'border-emerald-500/80', 'border-emerald-500/80',
@@ -233,6 +261,16 @@ export default {
}, },
bgColors () { bgColors () {
return [ return [
'bg-red-100/80',
'bg-sky-100/80',
'bg-emerald-100/80',
'bg-yellow-100/80',
'bg-purple-100/80',
'bg-pink-100/80',
'bg-cyan-100/80',
'bg-orange-100/80',
'bg-lime-100/80',
'bg-indigo-100/80',
'bg-red-100/80', 'bg-red-100/80',
'bg-sky-100/80', 'bg-sky-100/80',
'bg-emerald-100/80', 'bg-emerald-100/80',
+41 -21
View File
@@ -5,6 +5,7 @@
> >
<div <div
v-if="$slots.buttons || withTitle" v-if="$slots.buttons || withTitle"
id="title_container"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10" class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
:class="{ sticky: withStickySubmitters || isBreakpointLg }" :class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }" :style="{ backgroundColor }"
@@ -61,6 +62,7 @@
</span> </span>
</a> </a>
<button <button
v-if="editable"
class="base-button" class="base-button"
:class="{ disabled: isSaving }" :class="{ disabled: isSaving }"
v-bind="isSaving ? { disabled: true } : {}" v-bind="isSaving ? { disabled: true } : {}"
@@ -79,6 +81,15 @@
{{ t('save') }} {{ t('save') }}
</span> </span>
</button> </button>
<a
v-else
:href="`/templates/${template.id}`"
class="base-button"
>
<span class="hidden md:inline">
{{ t('back') }}
</span>
</a>
</template> </template>
</div> </div>
</div> </div>
@@ -112,7 +123,7 @@
/> />
<div <div
class="sticky bottom-0 py-2" class="sticky bottom-0 py-2"
:style="withStickySubmitters ? { backgroundColor } : {}" :style="{ backgroundColor }"
> >
<Upload <Upload
v-if="sortedDocuments.length && editable && withUploadButton" v-if="sortedDocuments.length && editable && withUploadButton"
@@ -148,6 +159,7 @@
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="defaultFields"
:allow-draw="!onlyDefinedFields" :allow-draw="!onlyDefinedFields"
:default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -185,25 +197,6 @@
</div> </div>
</template> </template>
</div> </div>
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<MobileFields
v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields"
:default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
</div> </div>
<div <div
v-if="withFieldsList" v-if="withFieldsList"
@@ -259,6 +252,27 @@
</div> </div>
</div> </div>
</div> </div>
<div class="sticky bottom-0">
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<MobileFields
v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields"
:default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
</div>
</div> </div>
</template> </template>
@@ -301,6 +315,7 @@ export default {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t, t: this.t,
currencies: this.currencies,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes, fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
@@ -349,6 +364,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
currencies: {
type: Array,
required: false,
default: () => []
},
fieldTypes: { fieldTypes: {
type: Array, type: Array,
required: false, required: false,
@@ -884,7 +904,7 @@ export default {
this.save() this.save()
}, },
onDocumentRemove (item) { onDocumentRemove (item) {
if (window.confirm('Are you sure?')) { if (window.confirm(this.t('are_you_sure'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
@@ -10,6 +10,7 @@
:allow-draw="allowDraw" :allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
:default-fields="defaultFields" :default-fields="defaultFields"
:default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:image="image" :image="image"
@@ -42,6 +43,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
allowDraw: { allowDraw: {
type: Boolean, type: Boolean,
required: false, required: false,
+28
View File
@@ -156,6 +156,34 @@
<span class="label-text">{{ t('required') }}</span> <span class="label-text">{{ t('required') }}</span>
</label> </label>
</li> </li>
<li
v-if="field.type == 'checkbox'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.default_value"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('checked') }}</span>
</label>
</li>
<li
v-if="field.type == 'date'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('set_signing_date') }}</span>
</label>
</li>
<li <li
v-if="field.type === 'text' && !defaultField" v-if="field.type === 'text' && !defaultField"
@click.stop @click.stop
@@ -61,13 +61,13 @@
<button <button
v-if="submitters.length > 1 && editable" v-if="submitters.length > 1 && editable"
class="px-2" class="px-2"
@click.stop="remove(submitter)" @click.prevent.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <IconTrashX :width="18" />
</button> </button>
</a> </a>
</li> </li>
<li v-if="submitters.length < 10 && editable"> <li v-if="submitters.length < names.length && editable">
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2"
@@ -125,7 +125,8 @@
/> />
</div> </div>
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded"> <span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
<IconPlus <component
:is="editable ? 'IconPlus' : 'IconChevronDown'"
width="18" width="18"
height="18" height="18"
/> />
@@ -144,7 +145,7 @@
<a <a
href="#" href="#"
class="flex px-2 group justify-between items-center" class="flex px-2 group justify-between items-center"
:class="{ 'active': submitter === selectedSubmitter }" :class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
@click.prevent="selectSubmitter(submitter)" @click.prevent="selectSubmitter(submitter)"
> >
<span class="py-1 flex items-center"> <span class="py-1 flex items-center">
@@ -165,7 +166,7 @@
title="Up" title="Up"
class="relative w-2" class="relative w-2"
style="font-size: 10px; margin-bottom: -4px" style="font-size: 10px; margin-bottom: -4px"
@click.stop="[move(submitter, -1), $refs.label.focus()] " @click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
> >
▲ ▲
</button> </button>
@@ -173,7 +174,7 @@
title="Down" title="Down"
class="relative w-2" class="relative w-2"
style="font-size: 10px; margin-top: -4px" style="font-size: 10px; margin-top: -4px"
@click.stop="[move(submitter, 1), $refs.label.focus()] " @click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
> >
▼ ▼
</button> </button>
@@ -181,14 +182,14 @@
<button <button
v-if="!compact && submitters.length > 1 && editable" v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2" class="hidden group-hover:block px-2"
@click.stop="remove(submitter)" @click.prevent.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <IconTrashX :width="18" />
</button> </button>
</div> </div>
</a> </a>
</li> </li>
<li v-if="submitters.length < 10 && editable"> <li v-if="submitters.length < names.length && editable && allowAddNew">
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2"
@@ -208,7 +209,7 @@
</template> </template>
<script> <script>
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue' import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } from '@tabler/icons-vue'
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import { v4 } from 'uuid' import { v4 } from 'uuid'
@@ -216,6 +217,7 @@ export default {
name: 'FieldSubmitter', name: 'FieldSubmitter',
components: { components: {
IconUserPlus, IconUserPlus,
IconChevronDown,
Contenteditable, Contenteditable,
IconPlus, IconPlus,
IconTrashX, IconTrashX,
@@ -242,6 +244,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
allowAddNew: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: true required: true
@@ -261,6 +268,16 @@ export default {
computed: { computed: {
colors () { colors () {
return [ return [
'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',
'bg-red-500', 'bg-red-500',
'bg-sky-500', 'bg-sky-500',
'bg-emerald-500', 'bg-emerald-500',
@@ -284,7 +301,17 @@ export default {
this.t('seventh_party'), this.t('seventh_party'),
this.t('eighth_party'), this.t('eighth_party'),
this.t('ninth_party'), this.t('ninth_party'),
this.t('tenth_party') this.t('tenth_party'),
this.t('eleventh_party'),
this.t('twelfth_party'),
this.t('thirteenth_party'),
this.t('fourteenth_party'),
this.t('fifteenth_party'),
this.t('sixteenth_party'),
this.t('seventeenth_party'),
this.t('eighteenth_party'),
this.t('nineteenth_party'),
this.t('twentieth_party')
] ]
}, },
selectedSubmitter () { selectedSubmitter () {
@@ -296,7 +323,7 @@ export default {
this.$emit('update:model-value', submitter.uuid) this.$emit('update:model-value', submitter.uuid)
}, },
remove (submitter) { remove (submitter) {
if (window.confirm('Are you sure?')) { if (window.confirm(this.t('are_you_sure'))) {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
+13
View File
@@ -1,6 +1,9 @@
const en = { const en = {
are_you_sure: 'Are you sure?',
sign_yourself: 'Sign Yourself', sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date',
send: 'Send', send: 'Send',
checked: 'Checked',
save: 'Save', save: 'Save',
cancel: 'Cancel', cancel: 'Cancel',
draw_field_on_the_document: 'Draw {field} field on the document', draw_field_on_the_document: 'Draw {field} field on the document',
@@ -29,6 +32,16 @@ const en = {
eighth_party: 'Eighth Party', eighth_party: 'Eighth Party',
ninth_party: 'Ninth Party', ninth_party: 'Ninth Party',
tenth_party: 'Tenth Party', tenth_party: 'Tenth Party',
eleventh_party: 'Eleventh Party',
twelfth_party: 'Twelfth Party',
thirteenth_party: 'Thirteenth Party',
fourteenth_party: 'Fourteenth Party',
fifteenth_party: 'Fifteenth Party',
sixteenth_party: 'Sixteenth Party',
seventeenth_party: 'Seventeenth Party',
eighteenth_party: 'Eighteenth Party',
nineteenth_party: 'Nineteenth Party',
twentieth_party: 'Twentieth Party',
add: 'Add', add: 'Add',
or_add_field_without_drawing: 'Or add field without drawing', or_add_field_without_drawing: 'Or add field without drawing',
text: 'Text', text: 'Text',
@@ -1,5 +1,5 @@
<template> <template>
<div class="fixed text-center w-full bottom-0 pr-6 mb-4"> <div class="absolute text-center w-full bottom-0 pr-3 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden"> <span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<component <component
@@ -1,6 +1,6 @@
<template> <template>
<span <span
class="dropdown dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden" class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 md:hidden"
> >
<label <label
class="btn btn-neutral text-white btn-circle btn-lg group" class="btn btn-neutral text-white btn-circle btn-lg group"
+6
View File
@@ -24,6 +24,7 @@
:field="item.field" :field="item.field"
:editable="editable" :editable="editable"
:default-field="defaultFields.find((f) => f.name === item.field.name)" :default-field="defaultFields.find((f) => f.name === item.field.name)"
:default-submitters="defaultSubmitters"
@start-resize="resizeDirection = $event" @start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null" @stop-resize="resizeDirection = null"
@start-drag="isMove = true" @start-drag="isMove = true"
@@ -85,6 +86,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
drawField: { drawField: {
type: Object, type: Object,
required: false, required: false,
@@ -31,7 +31,7 @@
@change="save" @change="save"
> >
<option <option
v-for="currency in currencies" v-for="currency in currenciesList"
:key="currency" :key="currency"
:value="currency" :value="currency"
> >
@@ -167,7 +167,7 @@ export default {
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe
}, },
inject: ['backgroundColor', 'save'], inject: ['backgroundColor', 'save', 'currencies'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -187,9 +187,12 @@ export default {
redirectUri () { redirectUri () {
return document.location.origin + '/auth/stripe_connect/callback' return document.location.origin + '/auth/stripe_connect/callback'
}, },
currencies () { defaultCurrencies () {
return ['USD', 'EUR', 'GBP'] return ['USD', 'EUR', 'GBP']
}, },
currenciesList () {
return this.currencies.length ? this.currencies : this.defaultCurrencies
},
authenticityToken () { authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content return document.querySelector('meta[name="csrf-token"]')?.content
}, },
@@ -3,6 +3,8 @@
class SendFormCompletedWebhookRequestJob < ApplicationJob class SendFormCompletedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.co Webhook'
NotSuccessStatus = Class.new(StandardError)
def perform(submitter) def perform(submitter)
config = Accounts.load_webhook_configs(submitter.submission.account) config = Accounts.load_webhook_configs(submitter.submission.account)
@@ -12,13 +14,17 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value, resp = Faraday.post(config.value,
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current, 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',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
raise NotSuccessStatus, resp.status.to_s
end
end end
end end
@@ -3,6 +3,8 @@
class SendFormStartedWebhookRequestJob < ApplicationJob class SendFormStartedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.co Webhook'
NotSuccessStatus = Class.new(StandardError)
def perform(submitter) def perform(submitter)
config = Accounts.load_webhook_configs(submitter.submission.account) config = Accounts.load_webhook_configs(submitter.submission.account)
@@ -10,13 +12,17 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value, resp = Faraday.post(config.value,
{ {
event_type: 'form.started', event_type: 'form.started',
timestamp: Time.current, 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',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
raise NotSuccessStatus, resp.status.to_s
end
end end
end end
@@ -3,6 +3,8 @@
class SendFormViewedWebhookRequestJob < ApplicationJob class SendFormViewedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.co Webhook'
NotSuccessStatus = Class.new(StandardError)
def perform(submitter) def perform(submitter)
config = Accounts.load_webhook_configs(submitter.submission.account) config = Accounts.load_webhook_configs(submitter.submission.account)
@@ -10,13 +12,17 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value, resp = Faraday.post(config.value,
{ {
event_type: 'form.viewed', event_type: 'form.viewed',
timestamp: Time.current, 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',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
raise NotSuccessStatus, resp.status.to_s
end
end end
end end
@@ -4,7 +4,11 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
SubmitterMailer.invitation_email(submitter).deliver_now! mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail)
mail.deliver_now!
SubmissionEvent.create!(submitter:, event_type: 'send_email') SubmissionEvent.create!(submitter:, event_type: 'send_email')
+14
View File
@@ -22,12 +22,26 @@ class Account < ApplicationRecord
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
has_many :submissions, through: :templates has_many :submissions, through: :templates
has_many :submitters, through: :submissions has_many :submitters, through: :submissions
has_many :account_linked_accounts, dependent: :destroy
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
class_name: 'AccountLinkedAccount',
inverse_of: :account
has_one :linked_account_account, dependent: :destroy,
foreign_key: :linked_account_id,
class_name: 'AccountLinkedAccount',
inverse_of: :linked_account
has_many :linked_accounts, through: :account_linked_accounts
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
has_many :active_users, -> { active }, dependent: :destroy, has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User' inverse_of: :account, class_name: 'User'
attribute :timezone, :string, default: 'UTC' attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US' attribute :locale, :string, default: 'en-US'
def testing?
linked_account_account&.testing?
end
def default_template_folder def default_template_folder
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME, super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
author_id: users.minimum(:id)).tap(&:save!) author_id: users.minimum(:id)).tap(&:save!)
+2
View File
@@ -27,8 +27,10 @@ class AccountConfig < ApplicationRecord
BCC_EMAILS = 'bcc_emails' BCC_EMAILS = 'bcc_emails'
FORCE_MFA = 'force_mfa' FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMAILERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DEFAULT_VALUES = { DEFAULT_VALUES = {
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_linked_accounts
#
# id :bigint not null, primary key
# account_type :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# linked_account_id :bigint not null
#
# Indexes
#
# idx_on_account_id_linked_account_id_48ab9f79d2 (account_id,linked_account_id) UNIQUE
# index_account_linked_accounts_on_account_id (account_id)
# index_account_linked_accounts_on_linked_account_id (linked_account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (linked_account_id => accounts.id)
#
class AccountLinkedAccount < ApplicationRecord
belongs_to :account
belongs_to :linked_account, class_name: 'Account'
attribute :account_type, :string, default: 'testing'
scope :testing, -> { where(account_type: :testing) }
def testing?
account_type == 'testing'
end
end
+3 -1
View File
@@ -15,11 +15,13 @@
# template_submitters :text # template_submitters :text
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null
# created_by_user_id :bigint # created_by_user_id :bigint
# template_id :bigint not null # template_id :bigint not null
# #
# Indexes # Indexes
# #
# index_submissions_on_account_id (account_id)
# index_submissions_on_created_by_user_id (created_by_user_id) # index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE # index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id) # index_submissions_on_template_id (template_id)
@@ -31,7 +33,7 @@
# #
class Submission < ApplicationRecord class Submission < ApplicationRecord
belongs_to :template belongs_to :template
has_one :account, through: :template belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :created_by_user, class_name: 'User', optional: true
has_many :submitters, dependent: :destroy has_many :submitters, dependent: :destroy
+2 -1
View File
@@ -46,7 +46,8 @@ class SubmissionEvent < ApplicationRecord
phone_verified: 'phone_verified', phone_verified: 'phone_verified',
start_form: 'start_form', start_form: 'start_form',
view_form: 'view_form', view_form: 'view_form',
complete_form: 'complete_form' complete_form: 'complete_form',
api_complete_form: 'api_complete_form'
}, scope: false }, scope: false
private private
+25 -18
View File
@@ -4,23 +4,24 @@
# #
# Table name: submitters # Table name: submitters
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# application_key :string # completed_at :datetime
# completed_at :datetime # email :string
# email :string # ip :string
# ip :string # metadata :text not null
# name :string # name :string
# opened_at :datetime # opened_at :datetime
# phone :string # phone :string
# preferences :text not null # preferences :text not null
# sent_at :datetime # sent_at :datetime
# slug :string not null # slug :string not null
# ua :string # ua :string
# uuid :string not null # uuid :string not null
# values :text not null # values :text not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# submission_id :bigint not null # external_id :string
# submission_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -35,14 +36,16 @@
class Submitter < ApplicationRecord class Submitter < ApplicationRecord
belongs_to :submission belongs_to :submission
has_one :template, through: :submission has_one :template, through: :submission
has_one :account, through: :template has_one :account, through: :submission
attribute :values, :string, default: -> { {} } attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
attribute :metadata, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, coder: JSON serialize :values, coder: JSON
serialize :preferences, coder: JSON serialize :preferences, coder: JSON
serialize :metadata, coder: JSON
has_many_attached :documents has_many_attached :documents
has_many_attached :attachments has_many_attached :attachments
@@ -62,6 +65,10 @@ class Submitter < ApplicationRecord
end end
end end
def application_key
external_id
end
def friendly_name def friendly_name
if name.present? && email.present? && email.exclude?(',') if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>) %("#{name.delete('"')}" <#{email}>)
+19 -14
View File
@@ -4,20 +4,20 @@
# #
# Table name: templates # Table name: templates
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# application_key :string # archived_at :datetime
# archived_at :datetime # fields :text not null
# fields :text not null # name :string not null
# name :string not null # schema :text not null
# schema :text not null # slug :string not null
# slug :string not null # source :text not null
# source :text not null # submitters :text not null
# submitters :text not null # created_at :datetime not null
# created_at :datetime not null # updated_at :datetime not null
# updated_at :datetime not null # account_id :bigint not null
# account_id :bigint not null # author_id :bigint not null
# author_id :bigint not null # external_id :string
# folder_id :bigint not null # folder_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -57,10 +57,15 @@ class Template < ApplicationRecord
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
has_many :template_sharings, dependent: :destroy
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
def application_key
external_id
end
private private
def maybe_set_default_folder def maybe_set_default_folder
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: template_sharings
#
# id :bigint not null, primary key
# ability :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# template_id :bigint not null
#
# Indexes
#
# index_template_sharings_on_account_id_and_template_id (account_id,template_id) UNIQUE
# index_template_sharings_on_template_id (template_id)
#
# Foreign Keys
#
# fk_rails_... (template_id => templates.id)
#
class TemplateSharing < ApplicationRecord
ALL_ID = -1
belongs_to :template
belongs_to :account, optional: true
end
+2 -2
View File
@@ -59,7 +59,7 @@ class User < ApplicationRecord
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig' has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
attribute :role, :string, default: ADMIN_ROLE attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
@@ -72,7 +72,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
!archived_at? super && !archived_at?
end end
def remember_me def remember_me
+19 -1
View File
@@ -22,7 +22,7 @@
</div> </div>
<% end %> <% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %> <% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
<%= f.fields_for encrypted_config do |ff| %> <%= 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' %>
@@ -63,6 +63,24 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_RESUBMIT) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Allow to resubmit completed forms
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8">
<h2 class="text-2xl font-bold mb-2">Danger Zone</h2>
<%= button_to button_title(title: 'Delete my account'), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: 'Schedule account for deletion?' }, method: :delete, id: :account_delete_button %>
</div> </div>
<% end %> <% end %>
</div> </div>
+19 -18
View File
@@ -1,14 +1,19 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">API</h1> <div class="flex justify-between">
<h1 class="text-4xl font-bold mb-4">API</h1>
<%= render 'shared/test_mode_toggle' %>
</div>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label> <label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-row space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <div class="flex w-full space-x-4">
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %> <%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
</div> </div>
</div> </div>
</div> </div>
@@ -30,20 +35,16 @@
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \ --header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--data-raw '{ --data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>, "template_id": <%= current_account.templates.last&.id || 1 %>,
"submission": [ "submitters": [
{ {
"submitters": [ "name": "John Doe",
{ "role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>",
"name": "John Doe", "email": "<%= current_user.email.sub('@', '+test@') %>",
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>", "values": {
"email": "<%= current_user.email.sub('@', '+test@') %>", "Form Text Field Name": "Default Value"
"values": { }
"Form Text Field Name": "Default Value" },
} { "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
},
{ "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
]
}
] ]
}'<% end.to_str %> }'<% end.to_str %>
<span class="top-0 right-0 absolute"> <span class="top-0 right-0 absolute">
+69 -90
View File
@@ -1,101 +1,80 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<% if @pagy.count > 0 || params[:q].present? || @template_folders.present? %> <div class="flex justify-between mb-4 items-center">
<div class="flex justify-between mb-4 items-center"> <h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1> <div class="flex space-x-2">
<div class="flex space-x-2"> <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %> <%= render 'shared/search_input' %>
<%= render 'shared/search_input' %> <% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% end %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %>
<% end %>
</div>
</div> </div>
<% view_archived_html = capture do %> </div>
<% if current_account.templates.where.not(archived_at: nil).exists? %> <% view_archived_html = capture do %>
<div> <% if current_account.templates.where.not(archived_at: nil).exists? %>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a> <div>
</div> <a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div> </div>
<% end %> <% end %>
<% if @templates.present? %> <% end %>
<div class="grid gap-4 md:grid-cols-3"> <% if @template_folders.present? %>
<%= render partial: 'templates/template', collection: @templates %> <div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
</div> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
<% end %> </div>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% end %>
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %> <% if @templates.present? %>
<input type="hidden" name="form_id" value="<%= form_id %>"> <div class="grid gap-4 md:grid-cols-3">
<button type="submit" class="hidden"></button> <%= render partial: 'templates/template', collection: @templates %>
<file-dropzone data-submit-on-upload="true" class="w-full"> </div>
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed"> <% end %>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<div class="flex flex-col items-center"> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<span data-target="file-dropzone.icon" class="flex flex-col items-center"> <input type="hidden" name="form_id" value="<%= form_id %>">
<span> <button type="submit" class="hidden"></button>
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %> <file-dropzone data-submit-on-upload="true" class="w-full">
</span> <label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
<div class="font-medium mb-1"> <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
Upload New Document <div class="flex flex-col items-center">
</div> <span data-target="file-dropzone.icon" class="flex flex-col items-center">
<div class="text-xs"> <span>
<span class="font-medium">Click to upload</span> or drag and drop <%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</div>
</span> </span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden"> <div class="font-medium mb-1">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %> Upload New Document
<div class="font-medium mb-1"> </div>
Uploading... <div class="text-xs">
</div> <span class="font-medium">Click to upload</span> or drag and drop
</span> </div>
</div> </span>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple> <span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
</div> <%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
</label> <div class="font-medium mb-1">
</file-dropzone> Uploading...
<% end %> </div>
<% end %> </span>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div>
</div>
<% end %>
<% else %>
<div class="card bg-base-200 h-96 mb-2">
<div class="card-body text-center">
<div class="flex items-center h-full">
<div class="mx-auto">
<div class="max-w-xl mx-auto">
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
</div>
<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>
<%= link_to new_template_path, class: 'base-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1">Create Template</span>
<% end %>
</div> </div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
</div> </div>
</div> </label>
</file-dropzone>
<% end %>
<% end %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div> </div>
</div> </div>
<% end %> <% end %>
+22 -10
View File
@@ -1,6 +1,14 @@
<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? %> <% if params[:redir].to_s.ends_with?('on_premise') %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1> <h1 class="text-3xl font-bold text-center mt-8 mb-6">DocuSeal Console</h1>
<div class="alert my-4 text-sm">
Sign up in DocuSeal Console to upgrade.
On-premises app is completely standalone, Console is used only to manage your license.
</div>
<% else %>
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1>
<% end %>
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %> <%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
<% if params[:redir].present? %> <% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %> <%= hidden_field_tag :redir, params[:redir] %>
@@ -17,15 +25,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+12 -8
View File
@@ -21,15 +21,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -8,7 +8,7 @@
<% end %> <% end %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :otp_attempt, 'Two-Factor Code', class: 'label' %> <%= f.label :otp_attempt, 'Two-Factor Code from Authenticator App', class: 'label' %>
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %> <%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div> </div>
</div> </div>
+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 d="M4 13h5" />
<path d="M12 16v-8h3a2 2 0 0 1 2 2v1a2 2 0 0 1 -2 2h-3" />
<path d="M20 8v8" />
<path d="M9 16v-5.5a2.5 2.5 0 0 0 -5 0v5.5" />
</svg>

After

Width:  |  Height:  |  Size: 439 B

+4
View File
@@ -0,0 +1,4 @@
<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="M15 6l-6 6l6 6" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

+6
View File
@@ -0,0 +1,6 @@
<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="M7 8l-4 4l4 4" />
<path d="M17 8l4 4l-4 4" />
<path d="M14 4l-4 16" />
</svg>

After

Width:  |  Height:  |  Size: 369 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 d="M8.5 13.5l-1.5 -1.5l1.5 -1.5" />
<path d="M15.5 10.5l1.5 1.5l-1.5 1.5" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M13 9.5l-2 5.5" />
</svg>

After

Width:  |  Height:  |  Size: 458 B

+1 -1
View File
@@ -3,7 +3,7 @@
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below. Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
</p> </p>
<div> <div>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %> <%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'md:w-80 md:h-80 my-4 mx-auto' }) %>
</div> </div>
<div class="form-control my-6 space-y-2"> <div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %> <%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
@@ -2,7 +2,7 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %> <% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
<div class="flex space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, 'First reminder in', class: 'label' %> <%= ff.label :first_duration, 'First reminder in', class: 'label' %>
@@ -5,7 +5,7 @@
<p> <p>
Send automatic email reminders to your recipients. Send automatic email reminders to your recipients.
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
@@ -5,7 +5,7 @@
<p> <p>
Display your company name and logo when signing documents. Display your company name and logo when signing documents.
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
+5 -1
View File
@@ -1,4 +1,8 @@
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
Upgrade Upgrade
<% end %> <% end %>
<% if signed_in? && current_user != true_user %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<%= render 'shared/test_alert' %>
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span> <span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
+18 -6
View File
@@ -11,17 +11,17 @@
<%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% unless Docuseal.multitenant? %> <% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li> <li>
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -43,7 +43,7 @@
</li> </li>
<% end %> <% end %>
<% unless Docuseal.multitenant? %> <% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -69,7 +69,7 @@
<%= render 'shared/settings_nav_extra' %> <%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans Plans
<span class="badge badge-warning">New</span> <span class="badge badge-warning">New</span>
<% end %> <% end %>
@@ -88,10 +88,22 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<li>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
Test Environment
</span>
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
</li>
<% end %>
<% end %> <% end %>
</ul> </ul>
</menu-active> </menu-active>
<% if !can?(:manage, :tenants) %> <% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div class="mx-4 border-t border-base-300 hidden md:block"> <div class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3"> <div class="text-sm mt-3">
Need help? Ask a question: Need help? Ask a question:
+17
View File
@@ -0,0 +1,17 @@
<% if signed_in? && current_user != true_user && current_account.testing? %>
<div class="alert py-1 text-sm font-medium gap-x-2 flex whitespace-nowrap">
<a href="<%= testing_api_settings_path %>" data-turbo-frame="modal" class="link font-semibold flex">
<%= svg_icon('code_circle', class: 'w-5 h-5 mr-1') %>Testing Environment
</a>
<span>
|
</span>
<%= button_to testing_account_path, method: :delete, class: 'inline flex' do %>
<% title = capture do %>
<span class="link">Exit</span>
<span>&times;</span>
<% end %>
<%= button_title(title:, disabled_with: 'Leave') %>
<% end %>
</div>
<% end %>
@@ -0,0 +1,10 @@
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
<label class="flex items-center justify-between py-2.5" for="testing_toggle">
<span class="mr-2 text-lg">
Test Environment
</span>
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Pro Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -5,7 +5,7 @@
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Pro Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -24,7 +24,7 @@
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> <%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div> </div>
<% end %> <% end %>
<% if @template.submitters.to_a.size == 1 %> <% if @template.submitters.to_a.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<div> <div>
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div> </div>
@@ -25,7 +25,7 @@
<%= fff.label :endpoint, 'Endpoint (optional)', class: 'label' %> <%= fff.label :endpoint, 'Endpoint (optional)', class: 'label' %>
<%= fff.text_field :endpoint, value: configs['endpoint'], type: :url, class: 'base-input' %> <%= fff.text_field :endpoint, value: configs['endpoint'], type: :url, class: 'base-input' %>
<label class="label"> <label class="label">
<span class="label-text-alt">For AWS S3 compantiable APIs like Minio.</span> <span class="label-text-alt">For AWS S3 compatible APIs like Minio.</span>
</label> </label>
</div> </div>
<% end %> <% end %>
+1 -175
View File
@@ -6,181 +6,7 @@
</div> </div>
</div> </div>
<div class="divider uppercase normal-case mt-5 mb-6 text-neutral-700">Or embed on your website</div> <div class="divider uppercase normal-case mt-5 mb-6 text-neutral-700">Or embed on your website</div>
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst=""> <%= render 'templates/embedding', template: %>
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
<div class="mt-4"> <div class="mt-4">
<a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a> <a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a>
</div> </div>
@@ -1,5 +1,5 @@
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<div class="tooltip w-full" data-tip="Unlock with Enterpise"> <div class="tooltip w-full" data-tip="Unlock with Enterpise">
<%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %> <%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
</div> </div>
</div> </div>
+1 -1
View File
@@ -5,7 +5,7 @@
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<span> <span>
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item w-28 peer-checked:btn-active normal-case"> <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-28 peer-checked:btn-active normal-case">
<%= label %> <%= label %>
</label> </label>
</span> </span>
+13 -2
View File
@@ -41,7 +41,7 @@
</a> </a>
<% end %> <% end %>
</div> </div>
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"> <div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div class="pr-3.5 pl-0.5"> <div class="pr-3.5 pl-0.5">
<% 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) } %>
@@ -71,7 +71,7 @@
<% end %> <% end %>
</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 id="parties_view" class="hidden md:block relative w-full md:w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %> <% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %> <% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% submitter_field_counters = Hash.new { 0 } %> <% submitter_field_counters = Hash.new { 0 } %>
@@ -173,5 +173,16 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<label class="md:hidden btn btn-sm btn-neutral text-white text-base z-10 fixed bottom-2 right-2 h-16 shadow-lg">
<input type="checkbox" class="peer hidden" onclick="[document_view.classList.toggle('hidden'), parties_view.classList.toggle('hidden')]">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('users', class: 'w-8 h-8') %>
<span>Signers</span>
</span>
<span class="hidden peer-checked:flex items-center">
<%= svg_icon('chevron_left', class: 'w-8 h-8') %>
<span>Back</span>
</span>
</label>
</div> </div>
<%= render 'scripts/autosize_field' %> <%= render 'scripts/autosize_field' %>
@@ -1,4 +1,4 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %> <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% configs = Submitters::FormConfigs.call(submitter) %> <% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
+1 -1
View File
@@ -36,7 +36,7 @@
</span> </span>
</download-button> </download-button>
</div> </div>
<% if @submitter.submission.template.submitters.size == 1 %> <% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<div class="divider uppercase"><%= t('or') %></div> <div class="divider uppercase"><%= t('or') %></div>
<div> <div>
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
@@ -1,7 +1,7 @@
<% if @email_config || @body.present? %> <% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %> <% body = (@body.presence || @email_config.value['body']).to_s %>
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %> <%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %>
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) %> <% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p> <p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
<% end %> <% end %>
<% else %> <% else %>
+175
View File
@@ -0,0 +1,175 @@
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst="">
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
+53 -45
View File
@@ -1,12 +1,12 @@
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %> <% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center"> <a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-5 md:px-6 py-5 md:items-center">
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %> <% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %> <% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<div> <div>
<% submitter = submitters.first %> <% submitter = submitters.first %>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex items-center space-x-3"> <span class="flex flex-col md:flex-row md:items-center gap-3">
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold"> <span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
<%= submitter.status %> <%= submitter.status %>
@@ -20,14 +20,14 @@
</div> </div>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center">
<% if submitter.completed_at? %> <% if submitter.completed_at? %>
<form onsubmit="event.preventDefault()"> <form onsubmit="event.preventDefault()" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()"> <button onclick="event.stopPropagation()" class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %> <%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline">Download</span> <span class="inline">Download</span>
</span> </span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %> <%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<span class="inline">Downloa...</span> <span class="inline">Downloa...</span>
</span> </span>
@@ -36,26 +36,30 @@
</form> </form>
<% else %> <% else %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white md:w-36 flex"> <button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span> <span class="inline shrink-0">Sign now</span>
</span> </span>
</button> </button>
</form> </form>
<% else %> <% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %> <div class="flex-1 md:flex-none">
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
</div>
<% end %> <% end %>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div>
<% if !submission.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && can?(:destroy, submission) %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>
<div class="space-y-1 w-full md:mr-2"> <div class="space-y-1 w-full md:mr-2">
<div class="flex items-center space-x-3"> <div class="flex flex-col md:flex-row md:items-center gap-3">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
@@ -64,10 +68,10 @@
</span> </span>
</div> </div>
<% end %> <% end %>
<div class="w-full"> <div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0">
<% submitters.each_with_index do |submitter, index| %> <% submitters.each_with_index do |submitter, index| %>
<div class="flex justify-between items-center space-x-3"> <div class="relative flex justify-between items-start md:items-center space-x-3">
<span class="flex items-center space-x-3"> <span class="flex flex-col md:flex-row md:items-center gap-2">
<% unless is_submission_completed %> <% unless is_submission_completed %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold"> <span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
@@ -82,31 +86,31 @@
<% if submitter.completed_at? && !is_submission_completed %> <% if submitter.completed_at? && !is_submission_completed %>
<form onsubmit="event.preventDefault()"> <form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()"> <button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Download</span> <span class="inline">Download</span>
</span> </span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
<span class="hidden md:inline">Downloa...</span> <span class="inline">Downloa...</span>
</span> </span>
</download-button> </download-button>
</button> </button>
</form> </form>
<% elsif !is_submission_completed %> <% elsif !is_submission_completed %>
<div class="flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
<button onclick="event.stopPropagation()" class="btn btn-xs btn-outline btn-neutral bg-white md:w-36"> <button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span> <span class="inline shrink-0">Sign now</span>
</span> </span>
</button> </button>
</form> </form>
<% else %> <% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy Link', copied_title_md: 'Copied' %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -115,27 +119,31 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex space-x-2 items-center"> <div class="flex space-x-1 md:space-x-2 items-center">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<form onsubmit="event.preventDefault()"> <div class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()"> <form onsubmit="event.preventDefault()" class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36"> <button onclick="event.stopPropagation()">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<span class="inline">Download</span> <%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
</span> <span class="inline">Download</span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> </span>
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<span class="inline">Downloa...</span> <%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
</span> <span class="inline">Downloa...</span>
</download-button> </span>
</button> </download-button>
</form> </button>
</form>
</div>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div>
<% unless submission.archived_at? %> <% unless submission.archived_at? %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+4 -1
View File
@@ -7,6 +7,9 @@
<p class="flex items-center space-x-1 text-xs text-base-content/60"> <p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('user', class: 'w-4 h-4') %> <%= svg_icon('user', class: 'w-4 h-4') %>
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span> <span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
<% if template.account_id != current_account.id %>
<span class="badge badge-neutral badge-outline badge-sm text-[10px] uppercase">shared</span>
<% end %>
</p> </p>
<p class="flex text-xs text-base-content/60"> <p class="flex text-xs text-base-content/60">
<span class="flex items-center space-x-1 w-1/2"> <span class="flex items-center space-x-1 w-1/2">
@@ -24,7 +27,7 @@
</a> </a>
<div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px"> <div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px">
<div class="space-y-1"> <div class="space-y-1">
<% if can?(:update, template) && !template.archived_at? %> <% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %>
<span class="tooltip tooltip-left" data-tip="Move"> <span class="tooltip tooltip-left" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle"> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('folder_share', class: 'w-4 h-4') %> <%= svg_icon('folder_share', class: 'w-4 h-4') %>
+71 -35
View File
@@ -1,50 +1,86 @@
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:justify-between md:items-start mb-3"> <div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3">
<div> <div class="relative flex items-start justify-between w-full space-x-0">
<h1 class="text-4xl font-semibold mr-4" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <div>
<%= template.name %> <h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% if template.archived_at? %> <%= template.name %>
<span class="badge badge-outline badge-lg align-middle">Archived</span> <% if template.archived_at? %>
<% end %> <span class="badge badge-outline badge-lg align-middle">Archived</span>
</h1> <% end %>
<div class="flex items-center"> </h1>
<a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer"> <% if @template.account_id == current_account.id %>
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %> <div class="flex items-center justify-between">
<span class="text-sm"> <div class="flex items-center">
<%= @template.folder.name %> <a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer">
</span> <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %>
</a> <span class="text-sm">
<% if can?(:update, template) %> <%= @template.folder.name %>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move"> </span>
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal"> </a>
<%= svg_icon('pencil_share', class: 'w-5 h-5') %> <% if can?(:update, template) %>
</a> <span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
</span> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a>
</span>
<% end %>
</div>
</div>
<% else %>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex items-center space-x-1 mt-1 peer">
<span class="badge badge-neutral badge-outline badge-md text-xs text-white uppercase">shared</span>
</div>
</div>
</div>
<% end %> <% end %>
</div> </div>
</div>
<div class="flex md:justify-between space-x-2 flex-none pt-1">
<% if !template.archived_at? %> <% if !template.archived_at? %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %> <div class="flex items-center space-x-2">
<% if can?(:update, template) && (Docuseal.multitenant? || current_account.testing? || current_account.id == 1) %>
<div class="tooltip" data-tip="API and Embedding">
<%= link_to template_code_modal_path(template), class: 'btn btn-ghost btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('code', class: 'w-6 h-6') %>
</span>
<% end %>
</div>
<% end %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: 'Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
</div>
<% end %> <% end %>
</div>
<div class="flex space-x-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %> <% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %> <%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
<% end %> <% end %>
<% if can?(:create, template) %> <% if can?(:create, template) %>
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %> <%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
<%= svg_icon('copy', class: 'w-6 h-6') %> <span class="flex items-center justify-center space-x-2">
<span class="hidden md:inline">Clone</span> <%= svg_icon('copy', class: 'w-6 h-6') %>
<span class="inline">Clone</span>
</span>
<% end %> <% end %>
<% end %> <% end %>
<% if !template.archived_at? && can?(:update, template) %> <% if !template.archived_at? %>
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm' do %> <% if can?(:update, template) %>
<span class="flex items-center justify-center space-x-2"> <%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<%= svg_icon('pencil', class: 'w-6 h-6') %> <span class="flex items-center justify-center space-x-2">
<span>Edit</span> <%= svg_icon('pencil', class: 'w-6 h-6') %>
</span> <span>Edit</span>
</span>
<% end %>
<% elsif can?(:read, template) %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %>
<span>Preview</span>
</span>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if template.archived_at? && can?(:create, template) %> <% if template.archived_at? && can?(:create, template) %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm' %> <%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %> <% end %>
</div> </div>
</div> </div>
+2 -2
View File
@@ -11,8 +11,8 @@
<%= svg_icon('folder', class: 'w-6 h-6') %> <%= svg_icon('folder', class: 'w-6 h-6') %>
</a> </a>
<folder-autocomplete class="flex justify-between w-full"> <folder-autocomplete class="flex justify-between w-full">
<input id="folder_name" placeholder="Folder Name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off"> <input id="folder_name" placeholder="Folder Name" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5"> <a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
Change Folder Change Folder
</a> </a>
</folder-autocomplete> </folder-autocomplete>
+17 -15
View File
@@ -1,22 +1,24 @@
<%= render 'title', template: @template %> <%= render 'title', template: @template %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %> <% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
<% if !@pagy.count.zero? || params[:q].present? %> <% if !@pagy.count.zero? || params[:q].present? %>
<div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>"> <div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<p class="text-3xl font-bold">Submissions</p> <div class="flex justify-between items-center md:items-end">
<div class="flex justify-end space-x-2 mt-3 md:mt-0"> <p class="text-3xl font-bold">Submissions</p>
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %> <div class="flex justify-end space-x-2">
<%= render 'shared/search_input' %> <% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<% end %> <%= render 'shared/search_input' %>
<%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %> <% end %>
<% end %> <%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %>
<% end %>
</div>
</div> </div>
</div> </div>
<% end %> <% end %>
@@ -0,0 +1,11 @@
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">API and Embedding</p>
<p class="text-gray-700">
Unlock with DocuSeal Pro
<br>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
Whitespace-only changes.
@@ -0,0 +1,36 @@
<%= render 'shared/turbo_modal', title: 'API and Embedding', close_after_submit: false do %>
<div>
<label class="text-sm font-semibold" for="template_id">
Template ID
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="template_id" type="text" value="<%= @template.id %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<div>
<label class="text-sm font-semibold" for="embedding_url">
Embedding URL
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<%= render 'templates/embedding', template: @template %>
<% if can?(:manage, TemplateSharing.new(template: @template)) %>
<%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %>
<%= f.hidden_field :template_id, value: @template.id %>
<div class="flex items-center justify-between">
<span>
Share template with Test Environment
</span>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<div class="mb-4">
</div>
<% end %>
<%= render 'templates_code_modal/preferences' %>
<%= render 'templates_code_modal/placeholder' %>
<% end %>
@@ -0,0 +1 @@
<template-builder class="grid" data-editable="false" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
@@ -0,0 +1,18 @@
<%= render 'shared/turbo_modal', title: 'Testing Environment' do %>
<div>
<label class="text-sm font-semibold" for="api_key">
x-Auth-Token
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<%= form_for @webhook_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="space-y-2 md:flex-nowrap mt-2">
<%= f.url_field :value, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full' %>
</div>
<% end %>
<% end %>
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more