mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93f84abce4 | |||
| 3c6a32caba | |||
| 72c08e5fdb | |||
| 022d80b5ce | |||
| f48da51802 | |||
| 709d04b33f | |||
| 3a251dd063 | |||
| 9cc92b59b3 | |||
| e0469b10a7 | |||
| 39eb67b162 | |||
| 8d9bea3b0f | |||
| 7015d8dde8 | |||
| 6b54072cb5 | |||
| b898708306 | |||
| 1b1cf36839 | |||
| 18b89edc19 | |||
| 70c7ef4247 | |||
| a8196709be | |||
| a9d249ccf0 | |||
| cd83d918e5 | |||
| b64a84a362 | |||
| f4426a8ee0 | |||
| df8c44d2ce | |||
| 65ce3d4822 | |||
| e5b1d94579 | |||
| 0b4754ca24 | |||
| b3dc8d55be | |||
| e5ab7667b9 | |||
| 2a4e6435f1 | |||
| 804431c44a | |||
| f3b26bf7a0 | |||
| aa27d90017 | |||
| 0fa8cf2dd0 | |||
| 634b5c37ba | |||
| 6e16f81fc6 | |||
| ffc4024f70 | |||
| a6981185b2 | |||
| 0361fabb66 | |||
| 04e7f101be | |||
| 86d680cfdf | |||
| 7bc2152057 | |||
| 74ed9a5040 | |||
| 30115fc0dc | |||
| 8e8e5e0e29 | |||
| b58ce1b571 | |||
| df5c4e4210 | |||
| ba2e0999f1 | |||
| a1f5dd08c7 | |||
| c1123fef63 | |||
| 4dc6149530 | |||
| 1245ff2cce | |||
| 284204fd78 | |||
| f7be74eb73 | |||
| aeea619059 | |||
| ea2b7f20e7 | |||
| f40f1d25de | |||
| c04bb2d7cf | |||
| 4c1ccd65bf | |||
| 54e064b0fc | |||
| 9a8b72883c | |||
| 5d75ee2e47 | |||
| 7357fa4871 | |||
| f6850b5427 | |||
| 1e9a181e60 | |||
| 7224a1ccec | |||
| 9b935a8180 | |||
| c4a693846e | |||
| 02c7cdcd97 | |||
| 36bee92e8e | |||
| 6542804f44 | |||
| d805fd614c |
@@ -68,6 +68,9 @@ RSpec/ExampleLength:
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 15
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
|
||||
|
||||
+1
-1
@@ -461,7 +461,7 @@ GEM
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rqrcode (2.2.0)
|
||||
|
||||
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||
|
||||
@@ -13,7 +13,7 @@ module Api
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||
if blob_uuid.blank? || purp != 'blob'
|
||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
@@ -24,8 +24,9 @@ module Api
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||
|
||||
authorization_check!(attachment) if exp.blank?
|
||||
authorization_check!(attachment, @record, exp)
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
@@ -41,16 +42,22 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(attachment)
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||
!attachment.record.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
def authorization_check!(attachment, record, exp)
|
||||
return if attachment.name == 'logo'
|
||||
return if exp.to_i >= Time.current.to_i
|
||||
return if current_user && current_ability.can?(:read, record)
|
||||
|
||||
return if is_authorized
|
||||
if exp.blank?
|
||||
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||
|
||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||
|
||||
return if !require_ttl && !require_auth
|
||||
end
|
||||
|
||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
|
||||
@@ -18,12 +18,14 @@ module Api
|
||||
field: :completed_at
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submitters::SerializeForWebhook.call(s)
|
||||
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -34,10 +34,12 @@ module Api
|
||||
associations: [:blob]
|
||||
).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
id: @submission.id,
|
||||
documents: documents.map do |attachment|
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
@@ -14,16 +14,19 @@ module Api
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||
audit_trail_attachment: :blob
|
||||
audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob
|
||||
),
|
||||
field: :completed_at)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
{
|
||||
event_type: 'submission.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -18,10 +18,12 @@ module Api
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false)
|
||||
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
@@ -41,7 +43,7 @@ module Api
|
||||
end
|
||||
|
||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||
end
|
||||
|
||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||
@@ -182,6 +184,7 @@ module Api
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||
:require_phone_2fa, :expire_at, :name,
|
||||
{
|
||||
variables: {},
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
|
||||
@@ -14,9 +14,11 @@ module Api
|
||||
documents_attachments: :blob, attachments_attachments: :blob)
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
|
||||
@@ -26,13 +26,15 @@ module Api
|
||||
original_template: @template,
|
||||
documents: params[:documents])
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,13 +24,14 @@ module Api
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(
|
||||
t,
|
||||
schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments
|
||||
)
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
|
||||
@@ -13,6 +13,8 @@ class ApplicationController < ActionController::Base
|
||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||
before_action :authenticate_user!, unless: :devise_controller?
|
||||
|
||||
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
||||
|
||||
helper_method :button_title,
|
||||
:current_account,
|
||||
:form_link_host,
|
||||
@@ -123,4 +125,21 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||
end
|
||||
|
||||
def set_csp
|
||||
request.content_security_policy = current_content_security_policy.tap do |policy|
|
||||
policy.default_src :self
|
||||
policy.script_src :self
|
||||
policy.style_src :self, :unsafe_inline
|
||||
policy.img_src :self, :https, :http, :blob, :data
|
||||
policy.font_src :self, :https, :http, :blob, :data
|
||||
policy.manifest_src :self
|
||||
policy.media_src :self
|
||||
policy.frame_src :self
|
||||
policy.worker_src :self, :blob
|
||||
policy.connect_src :self
|
||||
|
||||
policy.directives['connect-src'] << 'ws:' if Rails.env.development?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PasswordsController < Devise::PasswordsController
|
||||
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user
|
||||
end
|
||||
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
|
||||
Current.user = resource
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resetting_password_path_for(_)
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def update_password
|
||||
if current_user.update(password_params)
|
||||
if current_user.update_with_password(password_params)
|
||||
bypass_sign_in(current_user)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||
else
|
||||
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
if current_user.valid_password?(params[:password])
|
||||
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||
partial: 'reveal_access_token/access_token',
|
||||
locals: { token: current_user.access_token.token })
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t('wrong_password') }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -70,7 +70,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
attachment = submitter.submission.combined_document_attachment
|
||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
||||
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
||||
attachments_attachments: :blob })
|
||||
.order(id: :asc)
|
||||
|
||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
if params[:format] == 'csv'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.csv"
|
||||
elsif params[:format] == 'xlsx'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.xlsx"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class SubmissionsFiltersController < ApplicationController
|
||||
ALLOWED_NAMES = %w[
|
||||
author
|
||||
folder
|
||||
completed_at
|
||||
status
|
||||
created_at
|
||||
|
||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||
|
||||
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
|
||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
Templates.maybe_assign_access(template)
|
||||
|
||||
template.save!
|
||||
|
||||
template
|
||||
|
||||
@@ -30,6 +30,7 @@ class UsersController < ApplicationController
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||
end
|
||||
|
||||
@user.password = SecureRandom.hex if @user.password.blank?
|
||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||
|
||||
if @user.save
|
||||
@@ -54,7 +55,7 @@ class UsersController < ApplicationController
|
||||
@user.account = account
|
||||
end
|
||||
|
||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
||||
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
||||
@@ -83,7 +84,7 @@ class UsersController < ApplicationController
|
||||
|
||||
def user_params
|
||||
if params.key?(:user)
|
||||
permitted_params = %i[email first_name last_name password archived_at]
|
||||
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
|
||||
|
||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UsersSendResetPasswordController < ApplicationController
|
||||
load_and_authorize_resource :user
|
||||
|
||||
LIMIT_DURATION = 10.minutes
|
||||
|
||||
def update
|
||||
authorize!(:manage, @user)
|
||||
|
||||
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
|
||||
else
|
||||
@user.send_reset_password_instructions
|
||||
|
||||
redirect_back fallback_location: settings_users_path,
|
||||
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleSubmit from './elements/toggle_submit'
|
||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||
import CheckOnClick from './elements/check_on_click'
|
||||
import PasswordInput from './elements/password_input'
|
||||
@@ -34,11 +35,20 @@ import MaskedInput from './elements/masked_input'
|
||||
import SetDateButton from './elements/set_date_button'
|
||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||
import AppTour from './elements/app_tour'
|
||||
import AppTourStart from './elements/app_tour_start'
|
||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||
import PageContainer from './elements/page_container'
|
||||
import EmailEditor from './elements/email_editor'
|
||||
import MountOnClick from './elements/mount_on_click'
|
||||
import RemoveOnEvent from './elements/remove_on_event'
|
||||
import ScrollTo from './elements/scroll_to'
|
||||
import SetValue from './elements/set_value'
|
||||
import ReviewForm from './elements/review_form'
|
||||
import ShowOnValue from './elements/show_on_value'
|
||||
import CustomValidation from './elements/custom_validation'
|
||||
import ToggleClasses from './elements/toggle_classes'
|
||||
import AutosizeField from './elements/autosize_field'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -55,6 +65,9 @@ document.addEventListener('keyup', (e) => {
|
||||
})
|
||||
|
||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||
document.addEventListener('turbo:before-fetch-request', (event) => {
|
||||
event.detail.fetchOptions.headers['X-Turbo'] = 'true'
|
||||
})
|
||||
document.addEventListener('turbo:submit-end', async (event) => {
|
||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||
|
||||
@@ -97,6 +110,7 @@ safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('prompt-password', PromptPassword)
|
||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||
safeRegisterElement('password-input', PasswordInput)
|
||||
safeRegisterElement('search-input', SearchInput)
|
||||
@@ -107,12 +121,21 @@ safeRegisterElement('masked-input', MaskedInput)
|
||||
safeRegisterElement('set-date-button', SetDateButton)
|
||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||
safeRegisterElement('app-tour', AppTour)
|
||||
safeRegisterElement('app-tour-start', AppTourStart)
|
||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||
safeRegisterElement('check-on-click', CheckOnClick)
|
||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('email-editor', EmailEditor)
|
||||
safeRegisterElement('mount-on-click', MountOnClick)
|
||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||
safeRegisterElement('scroll-to', ScrollTo)
|
||||
safeRegisterElement('set-value', SetValue)
|
||||
safeRegisterElement('review-form', ReviewForm)
|
||||
safeRegisterElement('show-on-value', ShowOnValue)
|
||||
safeRegisterElement('custom-validation', CustomValidation)
|
||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||
safeRegisterElement('autosize-field', AutosizeField)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
|
||||
@@ -19,7 +19,7 @@ button .disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button[disabled] .disabled {
|
||||
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ button .enabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
button[disabled] .enabled {
|
||||
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelector('form').addEventListener('submit', () => {
|
||||
window.app_tour.start()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const originalFontValue = this.field.style.fontSize
|
||||
|
||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`
|
||||
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||
|
||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`
|
||||
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||
}
|
||||
}
|
||||
|
||||
this.field.classList.remove('hidden')
|
||||
}
|
||||
|
||||
get field () {
|
||||
return this.closest('field-value')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const input = this.querySelector('input')
|
||||
const invalidMessage = this.dataset.invalidMessage || ''
|
||||
|
||||
input.addEventListener('invalid', () => {
|
||||
input.setCustomValidity(input.value ? invalidMessage : '')
|
||||
})
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
input.setCustomValidity('')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const eventType = this.dataset.on || 'click'
|
||||
const selector = document.getElementById(this.dataset.selectorId) || this
|
||||
const eventElement = eventType === 'submit' ? this.querySelector('form') : this
|
||||
|
||||
eventElement.addEventListener(eventType, (event) => {
|
||||
if (eventType === 'click') {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
selector.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('input[type="radio"]').forEach(radio => {
|
||||
radio.addEventListener('change', (event) => {
|
||||
const rating = parseInt(event.target.value)
|
||||
|
||||
if (rating === 10) {
|
||||
window.review_comment.value = ''
|
||||
window.review_comment.classList.add('hidden')
|
||||
window.review_submit.classList.add('hidden')
|
||||
event.target.form.submit()
|
||||
} else {
|
||||
window.review_comment.classList.remove('hidden')
|
||||
window.review_submit.classList.remove('hidden')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.selector = document.getElementById(this.dataset.selectorId)
|
||||
|
||||
this.addEventListener('click', () => {
|
||||
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
history.replaceState(null, null, `#${this.dataset.selectorId}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ export default class extends HTMLElement {
|
||||
this.input.classList.remove('w-60')
|
||||
}
|
||||
})
|
||||
|
||||
this.button.addEventListener('click', (event) => {
|
||||
if (!this.input.value && document.activeElement !== this.input) {
|
||||
event.preventDefault()
|
||||
|
||||
this.input.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
@@ -22,4 +30,8 @@ export default class extends HTMLElement {
|
||||
get title () {
|
||||
return document.querySelector(this.dataset.title)
|
||||
}
|
||||
|
||||
get button () {
|
||||
return this.querySelector('button')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const input = this.dataset.inputId ? document.getElementById(this.dataset.inputId) : this.querySelector('input')
|
||||
|
||||
this.firstElementChild.addEventListener(this.dataset.on || 'click', () => {
|
||||
if (this.dataset.emptyOnly !== 'true' || !input.value) {
|
||||
input.value = this.dataset.value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.addEventListener('change', (event) => {
|
||||
const targetValue = this.dataset.value
|
||||
const selectorId = this.dataset.selectorId
|
||||
const targetElement = document.getElementById(selectorId)
|
||||
|
||||
if (event.target.value === targetValue) {
|
||||
targetElement.classList.remove('hidden')
|
||||
} else {
|
||||
targetElement.classList.add('hidden')
|
||||
targetElement.value = ''
|
||||
event.target.form.requestSubmit()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,27 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
|
||||
|
||||
if (this.dataset.interval) {
|
||||
this.interval = setInterval(() => {
|
||||
this.querySelector('form').requestSubmit()
|
||||
form.requestSubmit()
|
||||
}, parseInt(this.dataset.interval))
|
||||
} else if (this.dataset.on) {
|
||||
this.lastElementChild.addEventListener(this.dataset.on, (event) => {
|
||||
if (this.dataset.disable === 'true') {
|
||||
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||
}
|
||||
|
||||
if (this.dataset.submitIfValue === 'true') {
|
||||
if (event.target.value) {
|
||||
form.requestSubmit()
|
||||
}
|
||||
} else {
|
||||
form.requestSubmit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.querySelector('form').requestSubmit()
|
||||
form.requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const button = this.querySelector('a, button')
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.dataset.classes.split(' ').forEach((cls) => {
|
||||
button.classList.toggle(cls)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,15 @@ export default actionable(class extends HTMLElement {
|
||||
trigger (event) {
|
||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||
})
|
||||
if (event.target.type === 'checkbox') {
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId)?.classList.toggle('hidden')
|
||||
})
|
||||
} else {
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||
})
|
||||
}
|
||||
|
||||
if (this.dataset.focusId) {
|
||||
document.getElementById(this.dataset.focusId)?.focus()
|
||||
|
||||
@@ -6,6 +6,7 @@ import ToggleSubmit from './elements/toggle_submit'
|
||||
import FetchForm from './elements/fetch_form'
|
||||
import ScrollButtons from './elements/scroll_buttons'
|
||||
import PageContainer from './elements/page_container'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
|
||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||
|
||||
@@ -14,6 +15,7 @@ safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||
safeRegisterElement('fetch-form', FetchForm)
|
||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
@@ -19,7 +19,7 @@ button .disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button[disabled] .disabled {
|
||||
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ button .enabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
button[disabled] .enabled {
|
||||
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ function mouseoverListener (event) {
|
||||
|
||||
const requestOptions = {
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true' },
|
||||
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true', 'X-Turbo': 'true' },
|
||||
method: 'GET',
|
||||
redirect: 'follow'
|
||||
}
|
||||
|
||||
@@ -219,6 +219,48 @@
|
||||
>
|
||||
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="field.type === 'strikethrough'"
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
v-if="(((1000.0 / pageWidth) * pageHeight) * area.h) < 40.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<line
|
||||
x1="0"
|
||||
y1="50%"
|
||||
x2="100%"
|
||||
y2="50%"
|
||||
:stroke="field.preferences?.color || 'red'"
|
||||
:stroke-width="strikethroughWidth"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
|
||||
>
|
||||
<line
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
:stroke="field.preferences?.color || 'red'"
|
||||
:stroke-width="strikethroughWidth"
|
||||
/>
|
||||
<line
|
||||
x1="100%"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
:stroke="field.preferences?.color || 'red'"
|
||||
:stroke-width="strikethroughWidth"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="whitespace-pre-wrap"
|
||||
@@ -260,6 +302,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
pageWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
pageHeight: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
isValueSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -342,6 +394,13 @@ export default {
|
||||
verification: this.t('verify_id')
|
||||
}
|
||||
},
|
||||
strikethroughWidth () {
|
||||
if (this.isInlineSize) {
|
||||
return '0.6cqmin'
|
||||
} else {
|
||||
return 'clamp(0px, 0.5vw, 6px)'
|
||||
}
|
||||
},
|
||||
isShowSignatureId () {
|
||||
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
||||
return this.field.preferences.with_signature_id
|
||||
|
||||
@@ -11,30 +11,37 @@
|
||||
v-for="(area, areaIndex) in field.areas"
|
||||
:key="areaIndex"
|
||||
>
|
||||
<Teleport
|
||||
v-if="findPageElementForArea(area)"
|
||||
:to="findPageElementForArea(area)"
|
||||
<template
|
||||
v-for="(pageElem, index) in [findPageElementForArea(area)]"
|
||||
:key="index"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
:values="values"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="submittable"
|
||||
:field-index="fieldIndex"
|
||||
:is-inline-size="isInlineSize"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="pageElem"
|
||||
:to="pageElem"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
:values="values"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="submittable"
|
||||
:page-width="1400"
|
||||
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||
:field-index="fieldIndex"
|
||||
:is-inline-size="isInlineSize"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<FormulaFieldAreas
|
||||
v-if="formulaFields.length"
|
||||
:fields="formulaFields"
|
||||
:readonly-values="readonlyConditionalFieldValues"
|
||||
:readonly-values="readonlyFieldValues"
|
||||
:values="values"
|
||||
/>
|
||||
<Teleport
|
||||
@@ -349,10 +349,10 @@
|
||||
:id="field.uuid"
|
||||
type="checkbox"
|
||||
class="base-checkbox !h-7 !w-7"
|
||||
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
||||
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||
:required="field.required"
|
||||
:checked="!!values[field.uuid]"
|
||||
@invalid="$event.target.setCustomValidity(t('please_check_the_box_to_continue'))"
|
||||
@change="$event.target.setCustomValidity($event.target.validity.valueMissing ? t('please_check_the_box_to_continue') : '')"
|
||||
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||
>
|
||||
<span
|
||||
@@ -454,7 +454,9 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:submitter-slug="submitterSlug"
|
||||
:fields="formulaFields"
|
||||
:values="values"
|
||||
:readonly-values="readonlyFieldValues"
|
||||
@attached="attachments.push($event)"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
@@ -872,7 +874,14 @@ export default {
|
||||
},
|
||||
readonlyConditionalFieldValues () {
|
||||
return this.readonlyConditionalFields.reduce((acc, f) => {
|
||||
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
|
||||
acc[f.uuid] = isEmpty(this.values[f.uuid]) ? f.default_value : this.values[f.uuid]
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
readonlyFieldValues () {
|
||||
return this.readonlyFields.reduce((acc, f) => {
|
||||
acc[f.uuid] = isEmpty(this.values[f.uuid]) ? f.default_value : this.values[f.uuid]
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
@@ -972,7 +981,10 @@ export default {
|
||||
return this.currentStepFields[0]
|
||||
},
|
||||
readonlyConditionalFields () {
|
||||
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||
},
|
||||
readonlyFields () {
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
stepFields () {
|
||||
const verificationFields = []
|
||||
@@ -1015,7 +1027,11 @@ export default {
|
||||
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
||||
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
||||
|
||||
return sortArea(aArea, bArea)
|
||||
if (aArea && bArea) {
|
||||
return sortArea(aArea, bArea)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,15 @@ export default {
|
||||
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
|
||||
},
|
||||
dom () {
|
||||
const text = this.string.replace(/(?<!\(\s*)(https?:\/\/[^\s)]+)(?!\s*\))/g, (url) => `[${url}](${url})`)
|
||||
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
||||
|
||||
const text = linkParts.map((part, index) => {
|
||||
if (part.match(/^https?:\/\//) && !linkParts[index - 1]?.match(/\(\s*$/) && !linkParts[index + 1]?.match(/^\s*\)/)) {
|
||||
return `[${part}](${part})`
|
||||
} else {
|
||||
return part
|
||||
}
|
||||
}).join('')
|
||||
|
||||
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
|
||||
}
|
||||
|
||||
@@ -92,10 +92,20 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
readonlyValues: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -109,6 +119,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldsUuidIndex () {
|
||||
return this.fields.reduce((acc, field) => {
|
||||
acc[field.uuid] = field
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
queryParams () {
|
||||
return new URLSearchParams(window.location.search)
|
||||
},
|
||||
@@ -116,6 +133,10 @@ export default {
|
||||
return this.queryParams.get('stripe_session_id')
|
||||
},
|
||||
defaultName () {
|
||||
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const { price, currency } = this.field.preferences || {}
|
||||
|
||||
const formatter = new Intl.NumberFormat([], {
|
||||
@@ -178,12 +199,23 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
calculateFormula () {
|
||||
const transformedFormula = this.field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
return this.values[uuid] || 0.0
|
||||
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||
})
|
||||
|
||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||
},
|
||||
normalizeFormula (formula, depth = 0) {
|
||||
if (depth > 10) return formula
|
||||
|
||||
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
if (this.fieldsUuidIndex[uuid]) {
|
||||
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||
} else {
|
||||
return match
|
||||
}
|
||||
})
|
||||
},
|
||||
async submit () {
|
||||
if (this.sessionId) {
|
||||
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<div
|
||||
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
|
||||
v-if="isRequiredFieldEmpty"
|
||||
class="px-1 field-description-text"
|
||||
>
|
||||
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||
@@ -100,6 +100,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRequiredFieldEmpty () {
|
||||
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||
},
|
||||
countryCode () {
|
||||
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const browserTz = browserTimeZone.split('/')[1]
|
||||
@@ -131,17 +134,19 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.isLoading = true
|
||||
if (!this.isRequiredFieldEmpty) {
|
||||
this.isLoading = true
|
||||
|
||||
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
|
||||
this.$emit('submit')
|
||||
} else {
|
||||
Promise.all([
|
||||
import('@eid-easy/eideasy-widget'),
|
||||
this.start()
|
||||
]).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
|
||||
this.$emit('submit')
|
||||
} else {
|
||||
Promise.all([
|
||||
import('@eid-easy/eideasy-widget'),
|
||||
this.start()
|
||||
]).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -156,6 +161,10 @@ export default {
|
||||
}).then(async (resp) => {
|
||||
this.eidEasyData = await resp.json()
|
||||
|
||||
if (this.eidEasyData.check_completed) {
|
||||
this.$emit('submit')
|
||||
}
|
||||
|
||||
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
|
||||
this.eidEasyData.available_methods.length === 1) {
|
||||
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
|
||||
@@ -166,7 +175,7 @@ export default {
|
||||
redirectUrl.searchParams.append('lang', this.locale)
|
||||
|
||||
this.redirectUrl = redirectUrl.toString()
|
||||
} else {
|
||||
} else if (this.$refs.widgetContainer) {
|
||||
const eidEasyWidget = document.createElement('eideasy-widget')
|
||||
|
||||
for (const key in this.widgetSettings) {
|
||||
@@ -179,15 +188,23 @@ export default {
|
||||
})
|
||||
},
|
||||
async submit () {
|
||||
return fetch(this.baseUrl + '/api/identity_verification', {
|
||||
const resp = await fetch(this.baseUrl + '/api/identity_verification', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(async (resp) => {
|
||||
return resp
|
||||
})
|
||||
|
||||
if (resp.status === 404) {
|
||||
throw new Error('Verification not completed yet')
|
||||
}
|
||||
|
||||
if (resp.ok) {
|
||||
return resp
|
||||
} else {
|
||||
throw new Error('Verification failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div
|
||||
v-if="isSelected || isDraw"
|
||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
|
||||
:class="activeBorderClasses"
|
||||
/>
|
||||
<div
|
||||
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
||||
@@ -39,7 +39,7 @@
|
||||
@pointerdown.stop
|
||||
>
|
||||
<FieldSubmitter
|
||||
v-if="field.type != 'heading'"
|
||||
v-if="field.type != 'heading' && field.type != 'strikethrough'"
|
||||
v-model="field.submitter_uuid"
|
||||
class="border-r roles-dropdown"
|
||||
:compact="true"
|
||||
@@ -164,16 +164,58 @@
|
||||
ref="touchValueTarget"
|
||||
class="flex h-full w-full field-area"
|
||||
dir="auto"
|
||||
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', bgClasses, isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||
@click="focusValueInput"
|
||||
>
|
||||
<span
|
||||
v-if="field"
|
||||
class="flex justify-center items-center space-x-1"
|
||||
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && !isDefaultValuePresent }"
|
||||
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && (!isDefaultValuePresent || field.type === 'strikethrough') }"
|
||||
>
|
||||
<div
|
||||
v-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||
v-if="field.type === 'strikethrough'"
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
v-if="(((basePageWidth / pageWidth) * pageHeight) * area.h) < 41.6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<line
|
||||
x1="0"
|
||||
y1="50%"
|
||||
x2="100%"
|
||||
y2="50%"
|
||||
:stroke="field.preferences?.color || 'red'"
|
||||
:stroke-width="strikethroughWidth"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
|
||||
>
|
||||
<line
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
:stroke="field.preferences?.color || 'red'"
|
||||
:stroke-width="strikethroughWidth"
|
||||
/>
|
||||
<line
|
||||
x1="100%"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
:stroke="field.preferences?.color || 'red'"
|
||||
:stroke-width="strikethroughWidth"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||
:class="{ 'w-full h-full': isWFullType }"
|
||||
:style="fontStyle"
|
||||
>
|
||||
@@ -397,6 +439,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
pageWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
pageHeight: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
defaultSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -436,8 +488,33 @@ export default {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
bgClasses () {
|
||||
if (this.field.type === 'heading') {
|
||||
return 'bg-gray-50'
|
||||
} else if (this.field.type === 'strikethrough') {
|
||||
return 'bg-transparent'
|
||||
} else {
|
||||
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
||||
}
|
||||
},
|
||||
activeBorderClasses () {
|
||||
if (this.field.type === 'heading') {
|
||||
return ''
|
||||
} else if (this.field.type === 'strikethrough') {
|
||||
return 'border-dashed border-gray-300'
|
||||
} else {
|
||||
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
||||
}
|
||||
},
|
||||
isWFullType () {
|
||||
return ['cells', 'checkbox', 'radio', 'multiple', 'select'].includes(this.field.type)
|
||||
return ['cells', 'checkbox', 'radio', 'multiple', 'select', 'strikethrough'].includes(this.field.type)
|
||||
},
|
||||
strikethroughWidth () {
|
||||
if (this.isInlineSize) {
|
||||
return '0.6cqmin'
|
||||
} else {
|
||||
return 'clamp(0px, 0.5vw, 6px)'
|
||||
}
|
||||
},
|
||||
fontStyle () {
|
||||
let fontSize = ''
|
||||
@@ -471,8 +548,11 @@ export default {
|
||||
lineHeight () {
|
||||
return 1.3
|
||||
},
|
||||
basePageWidth () {
|
||||
return 1040.0
|
||||
},
|
||||
fontScale () {
|
||||
return 1040 / 612.0
|
||||
return this.basePageWidth / 612.0
|
||||
},
|
||||
isDefaultValuePresent () {
|
||||
return this.field?.default_value || this.field?.default_value === 0
|
||||
@@ -742,10 +822,15 @@ export default {
|
||||
delete this.field.options
|
||||
}
|
||||
|
||||
if (['heading'].includes(this.field.type)) {
|
||||
if (this.field.type === 'heading') {
|
||||
this.field.readonly = true
|
||||
}
|
||||
|
||||
if (this.field.type === 'strikethrough') {
|
||||
this.field.readonly = true
|
||||
this.field.default_value = true
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
@@ -402,7 +402,10 @@
|
||||
:style="{ backgroundColor }"
|
||||
>
|
||||
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
|
||||
<p>
|
||||
<p v-if="(drawField?.type || drawFieldType) === 'strikethrough'">
|
||||
{{ t('draw_strikethrough_the_document') }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ t('draw_field_on_the_document') }}
|
||||
</p>
|
||||
<div>
|
||||
@@ -413,7 +416,7 @@
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<a
|
||||
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading'].includes(drawField?.type || drawFieldType)"
|
||||
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType)"
|
||||
href="#"
|
||||
class="link block mt-3 text-sm"
|
||||
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||
@@ -1084,6 +1087,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'strikethrough') {
|
||||
field.readonly = true
|
||||
field.default_value = true
|
||||
}
|
||||
|
||||
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||
field.preferences ||= {}
|
||||
field.preferences.with_signature_id = this.withSignatureId
|
||||
@@ -1265,24 +1273,31 @@ export default {
|
||||
if (!field.areas.length) {
|
||||
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||
|
||||
this.template.fields.forEach((f) => {
|
||||
(f.conditions || []).forEach((c) => {
|
||||
this.removeFieldConditions(field)
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
removeFieldConditions (field) {
|
||||
this.template.fields.forEach((f) => {
|
||||
if (f.conditions) {
|
||||
f.conditions.forEach((c) => {
|
||||
if (c.field_uuid === field.uuid) {
|
||||
f.conditions.splice(f.conditions.indexOf(c), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.template.schema.forEach((item) => {
|
||||
(item.conditions || []).forEach((c) => {
|
||||
this.template.schema.forEach((item) => {
|
||||
if (item.conditions) {
|
||||
item.conditions.forEach((c) => {
|
||||
if (c.field_uuid === field.uuid) {
|
||||
item.conditions.splice(item.conditions.indexOf(c), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.save()
|
||||
}
|
||||
})
|
||||
},
|
||||
pasteField () {
|
||||
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
||||
@@ -1350,6 +1365,9 @@ export default {
|
||||
} else if (type === 'initials') {
|
||||
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||
} else if (type === 'strikethrough') {
|
||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||
area.h = (pageMask.clientWidth / 70 / pageMask.clientWidth)
|
||||
} else {
|
||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||
@@ -1493,8 +1511,12 @@ export default {
|
||||
field.default_value = '{{date}}'
|
||||
}
|
||||
|
||||
if (['stamp', 'heading'].includes(field.type)) {
|
||||
if (['stamp', 'heading', 'strikethrough'].includes(field.type)) {
|
||||
field.readonly = true
|
||||
|
||||
if (field.type === 'strikethrough') {
|
||||
field.default_value = true
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date') {
|
||||
@@ -1582,6 +1604,11 @@ export default {
|
||||
w: area.maskW / 10 / area.maskW,
|
||||
h: area.maskW / 35 / area.maskW
|
||||
}
|
||||
} else if (fieldType === 'strikethrough') {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
h: area.maskW / 70 / area.maskW
|
||||
}
|
||||
} else {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
@@ -1715,8 +1742,15 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
this.template.fields =
|
||||
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
||||
this.template.fields = this.template.fields.reduce((acc, f) => {
|
||||
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
|
||||
this.removeFieldConditions(f)
|
||||
} else {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
this.save()
|
||||
}
|
||||
|
||||
@@ -177,10 +177,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
excludeTypes () {
|
||||
return ['heading', 'strikethrough']
|
||||
},
|
||||
fields () {
|
||||
if (this.item.submitter_uuid) {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="flex items-center p-1 space-x-1">
|
||||
<FieldType
|
||||
v-model="field.type"
|
||||
:editable="editable && !defaultField && field.type != 'heading'"
|
||||
:editable="editable && !defaultField"
|
||||
:button-width="20"
|
||||
:menu-classes="'mt-1.5'"
|
||||
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||
@@ -97,7 +97,7 @@
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
/>
|
||||
<span
|
||||
v-else-if="field.type !== 'heading'"
|
||||
v-else
|
||||
class="dropdown dropdown-end field-settings-dropdown"
|
||||
@mouseenter="renderDropdown = true"
|
||||
@touchstart="renderDropdown = true"
|
||||
@@ -421,7 +421,7 @@ export default {
|
||||
} else {
|
||||
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
||||
|
||||
if (field.type === 'heading') {
|
||||
if (field.type === 'heading' || field.type === 'strikethrough') {
|
||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||
} else {
|
||||
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
|
||||
@@ -485,10 +485,15 @@ export default {
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
if (['heading'].includes(this.field.type)) {
|
||||
if (this.field.type === 'heading') {
|
||||
this.field.readonly = true
|
||||
}
|
||||
|
||||
if (this.field.type === 'strikethrough') {
|
||||
this.field.readonly = true
|
||||
this.field.default_value = true
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
if (this.field.type === 'cells') {
|
||||
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
|
||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
@@ -470,7 +470,7 @@
|
||||
v-if="field.type != 'stamp'"
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<li v-if="['text', 'number', 'date', 'select'].includes(field.type)">
|
||||
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)">
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-font')"
|
||||
@@ -484,7 +484,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="field.type != 'stamp'"
|
||||
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@@ -499,7 +499,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="field.type != 'stamp'"
|
||||
v-if="field.type != 'stamp' && field.type != 'heading'"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FiledTypeDropdown',
|
||||
@@ -97,6 +97,7 @@ export default {
|
||||
fieldNames () {
|
||||
return {
|
||||
heading: this.t('heading'),
|
||||
strikethrough: this.t('strikeout'),
|
||||
text: this.t('text'),
|
||||
signature: this.t('signature'),
|
||||
initials: this.t('initials'),
|
||||
@@ -139,6 +140,7 @@ export default {
|
||||
fieldIcons () {
|
||||
return {
|
||||
heading: IconHeading,
|
||||
strikethrough: IconStrikethrough,
|
||||
text: IconTextSize,
|
||||
signature: IconWritingSign,
|
||||
initials: IconLetterCaseUpper,
|
||||
@@ -158,6 +160,9 @@ export default {
|
||||
verification: IconId
|
||||
}
|
||||
},
|
||||
skipTypes () {
|
||||
return ['heading', 'datenow', 'strikethrough']
|
||||
},
|
||||
fieldIconsSorted () {
|
||||
if (this.fieldTypes.length) {
|
||||
return this.fieldTypes.reduce((acc, type) => {
|
||||
@@ -166,7 +171,7 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
} else {
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => !this.skipTypes.includes(key)))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -351,6 +351,9 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
skipTypes () {
|
||||
return ['heading', 'datenow', 'strikethrough']
|
||||
},
|
||||
fieldIconsSorted () {
|
||||
if (this.fieldTypes.length) {
|
||||
return this.fieldTypes.reduce((acc, type) => {
|
||||
@@ -359,7 +362,7 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
} else {
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => !this.skipTypes.includes(key)))
|
||||
}
|
||||
},
|
||||
submitterFields () {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const en = {
|
||||
payment_link: 'Payment link',
|
||||
strikeout: 'Strikeout',
|
||||
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
||||
quantity: 'Quantity',
|
||||
prefillable: 'Prefillable',
|
||||
signature_id: 'Signature ID',
|
||||
error_message: 'Error message',
|
||||
@@ -176,6 +180,10 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
payment_link: 'Enlace de pago',
|
||||
strikeout: 'Tachar',
|
||||
draw_strikethrough_the_document: 'Dibujar una línea de tachado en el documento',
|
||||
quantity: 'Cantidad',
|
||||
prefillable: 'Rellenable',
|
||||
signature_id: 'ID de Firma',
|
||||
error_message: 'Mensaje de error',
|
||||
@@ -353,6 +361,10 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
payment_link: 'Link di pagamento',
|
||||
strikeout: 'Barrato',
|
||||
draw_strikethrough_the_document: 'Disegna una linea barrata sul documento',
|
||||
quantity: 'Quantità',
|
||||
prefillable: 'Precompilabile',
|
||||
signature_id: 'ID firma',
|
||||
error_message: 'Messaggio di errore',
|
||||
@@ -530,6 +542,10 @@ const it = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
payment_link: 'Link de pagamento',
|
||||
strikeout: 'Tachado',
|
||||
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
|
||||
quantity: 'Quantidade',
|
||||
prefillable: 'Pré-preenchível',
|
||||
signature_id: 'ID da Assinatura',
|
||||
error_message: 'Mensagem de erro',
|
||||
@@ -707,6 +723,10 @@ const pt = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
payment_link: 'Lien de paiement',
|
||||
strikeout: 'Barrer',
|
||||
draw_strikethrough_the_document: 'Tracer une ligne de suppression sur le document',
|
||||
quantity: 'Quantité',
|
||||
prefillable: 'Pré-remplissable',
|
||||
signature_id: 'ID de signature',
|
||||
error_message: 'Message d\'erreur',
|
||||
@@ -884,6 +904,10 @@ const fr = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
payment_link: 'Zahlungslink',
|
||||
strikeout: 'Streichung',
|
||||
draw_strikethrough_the_document: 'Ziehe eine Streichung auf das Dokument',
|
||||
quantity: 'Menge',
|
||||
prefillable: 'Vorausfüllbar',
|
||||
signature_id: 'Signatur-ID',
|
||||
error_message: 'Fehlermeldung',
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
:ref="setAreaRefs"
|
||||
:area="item.area"
|
||||
:input-mode="inputMode"
|
||||
:page-width="width"
|
||||
:page-height="height"
|
||||
:field="item.field"
|
||||
:editable="editable"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
@@ -40,6 +42,8 @@
|
||||
<FieldArea
|
||||
v-if="newArea"
|
||||
:is-draw="true"
|
||||
:page-width="width"
|
||||
:page-height="height"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||
:area="newArea"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span
|
||||
class="dropdown dropdown-end field-settings-dropdown"
|
||||
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }"
|
||||
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
@@ -21,7 +21,7 @@
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<div
|
||||
v-if="!('price_id' in field.preferences)"
|
||||
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
@@ -52,10 +52,9 @@
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
v-if="field.preferences.formula"
|
||||
type="number"
|
||||
:placeholder="t('price')"
|
||||
disabled="true"
|
||||
v-if="'payment_link_id' in field.preferences"
|
||||
v-model="field.preferences.payment_link_id"
|
||||
placeholder="plink_XXXXX"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||
@blur="save"
|
||||
>
|
||||
@@ -66,6 +65,14 @@
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||
@blur="save"
|
||||
>
|
||||
<input
|
||||
v-else-if="field.preferences.formula"
|
||||
type="number"
|
||||
:placeholder="t('price')"
|
||||
disabled="true"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||
@blur="save"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="field.preferences.price"
|
||||
@@ -75,29 +82,41 @@
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="field.preferences.price && !field.preferences.formula"
|
||||
v-if="(field.preferences.price || field.preferences.price_id || field.preferences.payment_link_id) && (!field.preferences.formula || ('price_id' in field.preferences) || ('payment_link_id' in field.preferences))"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('price') }}
|
||||
{{ 'payment_link_id' in field.preferences ? t('payment_link') : t('price') }}
|
||||
</label>
|
||||
<div class="flex items-center justify-center">
|
||||
<a
|
||||
href="#"
|
||||
class="hover:underline"
|
||||
style="font-size: 11px"
|
||||
:class="{'underline': !('price_id' in field.preferences)}"
|
||||
@click="delete field.preferences.price_id"
|
||||
:class="{'underline': !('payment_link_id' in field.preferences)}"
|
||||
@click="[delete field.preferences.price_id, delete field.preferences.payment_link_id]"
|
||||
>{{ t('one_off') }}</a>
|
||||
<span class="h-2.5 border-l border-base-content mx-1" />
|
||||
<template
|
||||
v-if="field.preferences.price_id"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="hover:underline"
|
||||
style="font-size: 11px"
|
||||
:class="{'underline': ('price_id' in field.preferences)}"
|
||||
@click="field.preferences.payment_link_id ??= ''"
|
||||
>{{ t('recurrent') }}</a>
|
||||
<span class="h-2.5 border-l border-base-content mx-1" />
|
||||
</template>
|
||||
<a
|
||||
href="#"
|
||||
class="hover:underline"
|
||||
style="font-size: 11px"
|
||||
:class="{'underline': ('price_id' in field.preferences)}"
|
||||
@click="field.preferences.price_id ??= ''"
|
||||
>{{ t('recurrent') }}</a>
|
||||
:class="{'underline': ('payment_link_id' in field.preferences)}"
|
||||
@click="[delete field.preferences.price_id, field.preferences.payment_link_id ??= '']"
|
||||
>{{ t('payment_link') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -184,7 +203,6 @@
|
||||
>{{ t('learn_more') }}</a>
|
||||
</div>
|
||||
<li
|
||||
v-if="!('price_id' in field.preferences)"
|
||||
class="mb-1"
|
||||
>
|
||||
<label
|
||||
@@ -195,7 +213,7 @@
|
||||
width="18"
|
||||
/>
|
||||
<span class="text-sm">
|
||||
{{ t('formula') }}
|
||||
{{ 'payment_link_id' in field.preferences ? t('quantity') : t('formula') }}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
@@ -14,10 +14,10 @@ class ProcessSubmitterCompletionJob
|
||||
|
||||
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
||||
Submissions::GenerateCombinedAttachment.call(submitter)
|
||||
Submissions::EnsureCombinedGenerated.call(submitter)
|
||||
end
|
||||
|
||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||
Submissions::EnsureAuditGenerated.call(submitter.submission)
|
||||
|
||||
enqueue_completed_emails(submitter)
|
||||
end
|
||||
@@ -38,12 +38,19 @@ class ProcessSubmitterCompletionJob
|
||||
|
||||
submission = submitter.submission
|
||||
|
||||
complete_verification_events, sms_events =
|
||||
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
||||
.partition { |e| e.event_type == 'complete_verification' }
|
||||
|
||||
complete_verification_event = complete_verification_events.first
|
||||
|
||||
completed_submitter.assign_attributes(
|
||||
submission_id: submitter.submission_id,
|
||||
account_id: submission.account_id,
|
||||
template_id: submission.template_id,
|
||||
source: submission.source,
|
||||
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
||||
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||
verification_method: complete_verification_event&.data&.dig('method'),
|
||||
completed_at: submitter.completed_at
|
||||
)
|
||||
|
||||
|
||||
@@ -38,9 +38,11 @@ class AccountConfig < ApplicationRecord
|
||||
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
|
||||
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
||||
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
|
||||
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
||||
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||
WITH_FILE_LINKS_KEY = 'with_file_links'
|
||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
#
|
||||
# Table name: completed_submitters
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# completed_at :datetime not null
|
||||
# sms_count :integer not null
|
||||
# source :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# submission_id :bigint not null
|
||||
# submitter_id :bigint not null
|
||||
# template_id :bigint
|
||||
# id :bigint not null, primary key
|
||||
# completed_at :datetime not null
|
||||
# sms_count :integer not null
|
||||
# source :string not null
|
||||
# verification_method :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# submission_id :bigint not null
|
||||
# submitter_id :bigint not null
|
||||
# template_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -29,5 +30,5 @@ class CompletedSubmitter < ApplicationRecord
|
||||
has_many :completed_documents, dependent: :destroy,
|
||||
primary_key: :submitter_id,
|
||||
foreign_key: :submitter_id,
|
||||
inverse_of: :submitter
|
||||
inverse_of: :completed_submitter
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#
|
||||
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
||||
# index_email_events_on_email (email)
|
||||
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
|
||||
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'permanent_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
|
||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||
# index_email_events_on_message_id (message_id)
|
||||
#
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: lock_events
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# event_name :string not null
|
||||
# key :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_lock_events_on_event_name_and_key (event_name,key) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
|
||||
# index_lock_events_on_key (key)
|
||||
#
|
||||
class LockEvent < ApplicationRecord
|
||||
enum :event_name, {
|
||||
complete: 'complete',
|
||||
fail: 'fail',
|
||||
start: 'start',
|
||||
retry: 'retry'
|
||||
}, scope: false
|
||||
end
|
||||
@@ -15,6 +15,8 @@
|
||||
# template_fields :text
|
||||
# template_schema :text
|
||||
# template_submitters :text
|
||||
# variables :text
|
||||
# variables_schema :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
@@ -50,6 +52,8 @@ class Submission < ApplicationRecord
|
||||
serialize :template_fields, coder: JSON
|
||||
serialize :template_schema, coder: JSON
|
||||
serialize :template_submitters, coder: JSON
|
||||
serialize :variables_schema, coder: JSON
|
||||
serialize :variables, coder: JSON
|
||||
serialize :preferences, coder: JSON
|
||||
|
||||
attribute :source, :string, default: 'link'
|
||||
@@ -114,16 +118,16 @@ class Submission < ApplicationRecord
|
||||
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
||||
end
|
||||
|
||||
def audit_trail_url
|
||||
def audit_trail_url(expires_at: nil)
|
||||
return if audit_trail.blank?
|
||||
|
||||
ActiveStorage::Blob.proxy_url(audit_trail.blob)
|
||||
ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
|
||||
end
|
||||
alias audit_log_url audit_trail_url
|
||||
|
||||
def combined_document_url
|
||||
def combined_document_url(expires_at: nil)
|
||||
return if combined_document.blank?
|
||||
|
||||
ActiveStorage::Blob.proxy_url(combined_document.blob)
|
||||
ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
|
||||
end
|
||||
end
|
||||
|
||||
+18
-16
@@ -4,22 +4,23 @@
|
||||
#
|
||||
# Table name: templates
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# archived_at :datetime
|
||||
# fields :text not null
|
||||
# name :string not null
|
||||
# preferences :text not null
|
||||
# schema :text not null
|
||||
# shared_link :boolean default(FALSE), not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# author_id :bigint not null
|
||||
# external_id :string
|
||||
# folder_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# archived_at :datetime
|
||||
# fields :text not null
|
||||
# name :string not null
|
||||
# preferences :text not null
|
||||
# schema :text not null
|
||||
# shared_link :boolean default(FALSE), not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters :text not null
|
||||
# variables_schema :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# author_id :bigint not null
|
||||
# external_id :string
|
||||
# folder_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -57,6 +58,7 @@ class Template < ApplicationRecord
|
||||
|
||||
serialize :preferences, coder: JSON
|
||||
serialize :fields, coder: JSON
|
||||
serialize :variables_schema, coder: JSON
|
||||
serialize :schema, coder: JSON
|
||||
serialize :submitters, coder: JSON
|
||||
|
||||
|
||||
@@ -50,11 +50,16 @@
|
||||
<% 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>
|
||||
<%= t('force_2fa_with_authenticator_app') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('force_2fa_with_authenticator_app') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_two_factor_authentication_2fa_with_an_authenticator_app_e_g_google_authenticator_authy_all_users_signing_documents_must_pass_the_second_factor_verification_using_a_secure_code_in_addition_to_their_password') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -62,11 +67,16 @@
|
||||
<% 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>
|
||||
<%= t('add_signature_id_to_the_documents') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('add_signature_id_to_the_documents') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, disabled: can?(:manage, :cfr) %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -74,11 +84,16 @@
|
||||
<% 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>
|
||||
<%= t('require_signing_reason') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('require_signing_reason') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, disabled: can?(:manage, :cfr) %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -86,11 +101,16 @@
|
||||
<% 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>
|
||||
<%= t('allow_typed_text_signatures') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('allow_typed_text_signatures') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_create_signatures_by_typing_their_name_instead_of_drawing_or_uploading_one') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -98,11 +118,16 @@
|
||||
<% 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>
|
||||
<%= t('allow_to_resubmit_completed_forms') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('allow_to_resubmit_completed_forms') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_resubmit_forms_after_completion_useful_when_corrections_or_multiple_submissions_are_needed') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -110,11 +135,16 @@
|
||||
<% 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>
|
||||
<%= t('allow_to_decline_documents') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('allow_to_decline_documents') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_decline_signing_a_document_the_decline_reason_notification_will_be_sent_to_the_signature_requester') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -122,11 +152,33 @@
|
||||
<% 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>
|
||||
<%= t('remember_and_pre_fill_signatures') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('remember_and_pre_fill_signatures') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('save_a_users_signature_and_automatically_pre_fill_it_in_future_signing_sessions') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
|
||||
<% 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 gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('expirable_file_download_links') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_document_download_links_expire_after_40_minutes_to_prevent_long_term_access_and_enhance_security') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -134,11 +186,16 @@
|
||||
<% 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>
|
||||
<%= t('require_authentication_for_file_download_links') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('require_authentication_for_file_download_links') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_authentication_with_user_login_or_api_key_to_access_the_document_download_links') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -146,11 +203,16 @@
|
||||
<% 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>
|
||||
<%= t('combine_completed_documents_and_audit_log') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('combine_completed_documents_and_audit_log') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('combine_signed_documents_and_the_audit_log_into_a_single_pdf_file_for_easier_recordkeeping_and_compliance') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -159,22 +221,43 @@
|
||||
<% 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>
|
||||
<%= t('always_enforce_signing_order') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('always_enforce_signing_order') %></span>
|
||||
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_the_recipients_signing_order_always_enforced_so_that_the_second_signer_can_start_signing_their_part_only_after_the_first_signer_has_completed_signing') %>">
|
||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if can?(:manage, :personalization_advanced) %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %>
|
||||
<% 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 gap-4 py-2.5">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="text-left"><%= t('use_direct_file_attachment_links_in_the_documents') %></span>
|
||||
</div>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'extra_preferences' %>
|
||||
<% if !Docuseal.multitenant? && SearchEntry.table_exists? && (!Docuseal.fulltext_search? || params[:reindex] == 'true') && can?(:manage, EncryptedConfig) %>
|
||||
<div class="flex items-center justify-between py-2.5">
|
||||
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||
<span>
|
||||
Efficient search with search index
|
||||
<%= t('efficient_search_with_search_index') %>
|
||||
</span>
|
||||
<%= button_to params[:reindex] == 'true' ? 'Reindex' : 'Build Search Index', settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
|
||||
<%= button_to params[:reindex] == 'true' ? t('reindex') : t('build_search_index'), settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,21 @@
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex w-full space-x-4">
|
||||
<% token = current_user.access_token.token %>
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
<% obscured_token = current_user.access_token.token.sub(token[5..], '*' * token[5..].size) %>
|
||||
<% if current_account.testing? %>
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
<% else %>
|
||||
<a id="access_token_container" href="<%= settings_reveal_access_token_path %>" data-turbo-frame="modal" class="flex w-full space-x-4">
|
||||
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<div class="base-button">
|
||||
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline"><%= t('copy') %></span>
|
||||
</div>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
|
||||
</div>
|
||||
@@ -35,7 +46,7 @@
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||
--header 'X-Auth-Token: API_TOKEN' \
|
||||
--data-raw '{
|
||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||
"submitters": [
|
||||
@@ -71,7 +82,7 @@
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl --location '<%= api_submissions_emails_url %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||
--header 'X-Auth-Token: API_TOKEN' \
|
||||
--data-raw '{
|
||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
||||
@@ -97,7 +108,7 @@
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
|
||||
--header 'X-Auth-Token: API_TOKEN'<% end.to_str %>
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</span>
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
<h1 class="text-4xl font-bold text-center mt-8">
|
||||
<%= t('sign_in') %>
|
||||
</h1>
|
||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||
<%= f.hidden_field :email %>
|
||||
<%= f.hidden_field :password %>
|
||||
<% if params[:redir].present? %>
|
||||
<%= hidden_field_tag :redir, params[:redir] %>
|
||||
<% end %>
|
||||
<%= render 'otp_form', **local_assigns %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %>
|
||||
<% if local_assigns[:access_error].present? %>
|
||||
<div class="alert mt-6">
|
||||
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
|
||||
<span><%= local_assigns[:access_error] %></span>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||
<%= f.hidden_field :email %>
|
||||
<%= f.hidden_field :password %>
|
||||
<% if params[:redir].present? %>
|
||||
<%= hidden_field_tag :redir, params[:redir] %>
|
||||
<% end %>
|
||||
<%= render 'otp_form', **local_assigns %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<% if params[:redir].present? %>
|
||||
<%= hidden_field_tag :redir, params[:redir] %>
|
||||
<% end %>
|
||||
<%= select_tag :lang, options_for_select((I18n.available_locales - %i[en pt-PT de-DE fr-FR it-IT es-ES]).map { |code| [t("language_#{code}"), code] }, I18n.locale), onchange: 'this.form.requestSubmit();', class: 'select select-sm border-base-content/30 text-base' %>
|
||||
<submit-form data-on="change">
|
||||
<%= select_tag :lang, options_for_select((I18n.available_locales - %i[en pt-PT de-DE fr-FR it-IT es-ES]).map { |code| [t("language_#{code}"), code] }, I18n.locale), class: 'select select-sm border-base-content/30 text-base' %>
|
||||
</submit-form>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<% eu_server = request.host == 'docuseal.eu' %>
|
||||
<server-selector>
|
||||
<div id="global_server_selector" class="text-center hidden">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-40 <%= 'bg-base-300' unless eu_server %>">
|
||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||
Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="relative btn bg-base-200 join-item w-40 <%= 'bg-base-300' if eu_server %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
Europe
|
||||
<% unless eu_server %>
|
||||
<span id="eu_server_alert" class="absolute flex space-x-0.5 hidden" style="top: -1.5rem;">
|
||||
<span class="text-xs font-normal leading-none text-base-content normal-case">
|
||||
<%= t('eu_data_residency') %>
|
||||
</span>
|
||||
<%= svg_icon 'corner_right_down', class: 'w-4 h-5 stroke-1 shrink-0 pt-1' %>
|
||||
</span>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="server_selector" class="flex justify-center <%= 'hidden' unless eu_server %>">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
||||
<% if eu_server %>
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
<span>EU Cloud</span>
|
||||
<% else %>
|
||||
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||
<span>US Cloud</span>
|
||||
<% end %>
|
||||
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
|
||||
<li>
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
|
||||
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||
US Cloud
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
EU Cloud
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</server-selector>
|
||||
<%= render 'scripts/server_selector' %>
|
||||
|
||||
@@ -148,7 +148,9 @@
|
||||
<span>
|
||||
<%= t('apply_multiple_pdf_digital_signatures_in_the_document_per_each_signer') %>
|
||||
</span>
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple' }, 'multiple', 'single' %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -160,7 +162,9 @@
|
||||
<span>
|
||||
<%= t('remove_pdf_form_fillable_fields_from_the_signed_pdf_flatten_form') %>
|
||||
</span>
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' } %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false } %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -172,9 +176,9 @@
|
||||
<span>
|
||||
<%= t('document_download_filename_format') %>
|
||||
</span>
|
||||
<div class="mt-3">
|
||||
<%= f.select :value, [["#{I18n.t('document_name')}.pdf", '{document.name}'], ["#{I18n.t('document_name')} - #{I18n.t(:signed)}.pdf", '{document.name} - {submission.status}'], ["#{I18n.t('document_name')} - name@domain.com.pdf", '{document.name} - {submission.submitters}'], ["#{I18n.t('document_name')} - name@domain.com - #{I18n.l(Time.current.beginning_of_year.in_time_zone(current_account.timezone), format: :short)}.pdf", '{document.name} - {submission.submitters} - {submission.completed_at}']], {}, class: 'base-select', onchange: 'this.form.requestSubmit()' %>
|
||||
</div>
|
||||
<submit-form data-on="change" class="block mt-3">
|
||||
<%= f.select :value, [["#{I18n.t('document_name')}.pdf", '{document.name}'], ["#{I18n.t('document_name')} - #{I18n.t(:signed)}.pdf", '{document.name} - {submission.status}'], ["#{I18n.t('document_name')} - name@domain.com.pdf", '{document.name} - {submission.submitters}'], ["#{I18n.t('document_name')} - name@domain.com - #{I18n.l(Time.current.beginning_of_year.in_time_zone(current_account.timezone), format: :short)}.pdf", '{document.name} - {submission.submitters} - {submission.completed_at}']], {}, class: 'base-select' %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<% end %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>
|
||||
<meta name="rollbar-token" content="<%= ENV.fetch('ROLLBAR_CLIENT_TOKEN', nil) %>">
|
||||
<%= javascript_pack_tag 'rollbar', 'application', defer: true %>
|
||||
@@ -18,7 +17,6 @@
|
||||
<link href="<%= canonical_url %>" rel="canonical">
|
||||
<% end %>
|
||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
||||
</head>
|
||||
<body>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
@@ -28,5 +26,6 @@
|
||||
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
|
||||
<%= yield %>
|
||||
</div>
|
||||
<%= render 'shared/body_scripts' %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<%= render 'layouts/head_tags' %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>
|
||||
<meta name="rollbar-token" content="<%= ENV.fetch('ROLLBAR_CLIENT_TOKEN', nil) %>">
|
||||
<%= javascript_pack_tag 'rollbar', 'form', defer: true %>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<%= render 'layouts/head_tags' %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>
|
||||
<meta name="rollbar-token" content="<%= ENV.fetch('ROLLBAR_CLIENT_TOKEN', nil) %>">
|
||||
<%= javascript_pack_tag 'rollbar', 'application', defer: true %>
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
<span>
|
||||
<%= t('receive_notification_emails_on_completed_submission') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: user_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, class: 'toggle', checked: user_config.value != false %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<span>
|
||||
<%= t('show_confetti_on_successful_completion') %>
|
||||
</span>
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' }, '1', '0' %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false }, '1', '0' %>
|
||||
</submit-form>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -54,19 +54,29 @@
|
||||
<p class="text-2xl font-bold mt-8 mb-4">
|
||||
<%= t('change_password') %>
|
||||
</p>
|
||||
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<div class="form-control">
|
||||
<%= f.label :password, t('new_password'), class: 'label' %>
|
||||
<%= f.password_field :password, autocomplete: 'off', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
||||
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off' } do |f| %>
|
||||
<%= f.label :password, t('new_password'), class: 'label' %>
|
||||
<%= f.password_field :password, autocomplete: 'off', class: 'base-input peer w-full', required: true %>
|
||||
<div class="<%= 'peer-invalid:hidden' if current_user.errors.blank? %> space-y-4 mt-4">
|
||||
<div class="form-control">
|
||||
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :current_password, t('current_password'), class: 'label' %>
|
||||
<%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
|
||||
<% if Accounts.can_send_emails?(current_account) %>
|
||||
<span class="label-text-alt mt-1">
|
||||
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= button_to nil, user_send_reset_password_path(current_user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_') } %>
|
||||
<p class="text-2xl font-bold mt-8 mb-4">
|
||||
<%= t('two_factor_authentication') %>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<input id="api_key" type="text" value="<%= token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= render 'shared/turbo_modal', title: t('reveal_api_key') do %>
|
||||
<%= form_tag settings_reveal_access_token_path, enctype: 'multipart/form-data', data: { turbo_frame: :_top } do %>
|
||||
<div class="form-control">
|
||||
<%= label_tag :password, t('enter_your_password_to_reveal_the_api_key'), class: 'label' %>
|
||||
<%= password_field_tag :password, nil, class: 'base-input', autocomplete: 'current-password', required: true, autofocus: true, placeholder: t('password') %>
|
||||
<% if local_assigns[:error_message].present? %>
|
||||
<span class="label-text-alt text-red-400 mt-1"><%= local_assigns[:error_message] %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control mt-4">
|
||||
<%= submit_tag t('submit'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script id="autosize_script" nonce="<%= content_security_policy_nonce %>">
|
||||
if (!window.customElements.get('autosize-field')) {
|
||||
window.customElements.define('autosize-field', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
@@ -19,4 +19,6 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.autosize_script.remove()
|
||||
</script>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<script>
|
||||
if (!window.customElements.get('server-selector')) {
|
||||
customElements.define('server-selector', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const serverSelector = this.querySelector('#server_selector');
|
||||
const globalServerSelector = this.querySelector('#global_server_selector');
|
||||
const euServerAlert = this.querySelector('#eu_server_alert');
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
||||
|
||||
if (!serverSelector.classList.contains('hidden')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (usTimezones.test(timezone)) {
|
||||
serverSelector.classList.remove('hidden');
|
||||
} else if (timezone.includes('Europe')) {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
euServerAlert?.classList?.remove('hidden');
|
||||
} else {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -11,7 +11,7 @@
|
||||
<span><%= flash[:notice] || flash[:alert] %></span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" onclick="[event.preventDefault(), window.flash.remove()]" class="mr-1">×</a>
|
||||
<remove-on-event data-event-type="click" data-selector-id="flash" class="mr-1 cursor-pointer">×</remove-on-event>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
|
||||
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||
<span>9k</span>
|
||||
<span>10k</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -61,7 +61,9 @@
|
||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
|
||||
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
|
||||
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()', style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
|
||||
</submit-form>
|
||||
<span class="whitespace-nowrap">
|
||||
<%= t('test_mode') %>
|
||||
</span>
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<search-input data-title="<%= local_assigns[:title_selector] || 'h1' %>">
|
||||
<search-input data-title="<%= local_assigns[:title_selector] || 'h1' %>" class="flex items-center">
|
||||
<input id="search" name="q" value="<%= params[:q] %>" class="input text-lg pr-10 -mr-12 w-0 md:w-60 <%= 'pl-8 input-outlined w-60' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
|
||||
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle">
|
||||
<span class="enabled">
|
||||
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
||||
</span>
|
||||
<span class="disabled">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
</span>
|
||||
</button>
|
||||
</search-input>
|
||||
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
||||
<span class="enabled">
|
||||
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
||||
</span>
|
||||
<span class="disabled">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
|
||||
<% if can?(:read, WebhookUrl) %>
|
||||
<li>
|
||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||
@@ -70,7 +72,7 @@
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
|
||||
<li>
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
||||
@@ -96,7 +98,9 @@
|
||||
<span class="mr-2 w-full">
|
||||
<%= t('test_mode') %>
|
||||
</span>
|
||||
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing? %>
|
||||
</submit-form>
|
||||
</label>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<span class="mr-2 text-lg">
|
||||
<%= t('test_mode') %>
|
||||
</span>
|
||||
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
|
||||
<submit-form data-on="change" class="flex">
|
||||
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing? %>
|
||||
</submit-form>
|
||||
</label>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</div>
|
||||
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||
<div dir="auto" class="form-control !mt-0">
|
||||
<%= f.hidden_field 'resubmit', value: params[:resubmit] %>
|
||||
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
|
||||
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
|
||||
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
<% if link_form_fields.include?('phone') %>
|
||||
<div dir="auto" class="form-control !mt-0">
|
||||
<%= f.label :phone, t('phone'), class: 'label' %>
|
||||
<%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', required: true, class: 'base-input w-full', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
|
||||
</custom-validation>
|
||||
</div>
|
||||
<% end %>
|
||||
<toggle-submit dir="auto" class="form-control">
|
||||
|
||||
@@ -33,11 +33,13 @@
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<% has_phone_field = true %>
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if prefillable_fields.present? %>
|
||||
@@ -48,11 +50,13 @@
|
||||
</submitters-autocomplete>
|
||||
<% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %>
|
||||
<% has_phone_field = true %>
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
<% end %>
|
||||
<% prefillable_fields.each do |field| %>
|
||||
<% if field['type'] == 'checkbox' %>
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
</label>
|
||||
<% end %>
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
<% if submitters.size > 1 %>
|
||||
<submitters-autocomplete data-field="name">
|
||||
<linked-input data-target-id="<%= "phone_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
<% if can_send_emails %>
|
||||
<%= render 'submissions/email_stats' %>
|
||||
<%= content_for(:edit_button) || capture do %>
|
||||
<label>
|
||||
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
|
||||
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
|
||||
</label>
|
||||
<toggle-visible data-element-ids="<%= %w[message_field].to_json %>" class="flex">
|
||||
<label>
|
||||
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
|
||||
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
|
||||
</label>
|
||||
</toggle-visible>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,19 @@
|
||||
<div class="flex w-full px-0.5 whitespace-nowrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||
<div class="w-full"><%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %></div>
|
||||
</div>
|
||||
<% elsif field['type'] == 'strikethrough' %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h']) < 40 %>
|
||||
<svg width="100%" height="100%">
|
||||
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
</svg>
|
||||
<% else %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="overflow: visible; width: calc(100% - 6px); height: calc(100% - 6px); width: calc(100% - 0.6cqmin); height: calc(100% - 0.6cqmin)">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<autosize-field></autosize-field>
|
||||
<div class="flex w-full px-0.5 whitespace-pre-wrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||
|
||||
@@ -80,12 +80,12 @@
|
||||
<% schema.each do |item| %>
|
||||
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||
<% if document.preview_images.first %>
|
||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||
<scroll-to data-selector-id="page-<%= document.uuid %>-0" class="block cursor-pointer">
|
||||
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||
<%= item['name'].presence || document.filename.base %>
|
||||
</div>
|
||||
</a>
|
||||
</scroll-to>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -109,7 +109,7 @@
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
||||
<% next if value.blank? %>
|
||||
<% submitter = submitters_index[field['submitter_uuid']] %>
|
||||
@@ -123,7 +123,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %>
|
||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -240,7 +240,7 @@
|
||||
<% submitter_field_counters[field['type']] += 1 %>
|
||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
<% next if value.blank? %>
|
||||
<% next if field['type'] == 'heading' %>
|
||||
<% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %>
|
||||
<div class="pt-2.5 border-b border-base-300">
|
||||
<div class="text-xs font-medium uppercase mb-0.5" dir="auto">
|
||||
<%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %>
|
||||
@@ -288,16 +288,20 @@
|
||||
<% end %>
|
||||
</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><%= t('signers') %></span>
|
||||
</span>
|
||||
<span class="hidden peer-checked:flex items-center">
|
||||
<%= svg_icon('chevron_left', class: 'w-8 h-8') %>
|
||||
<span><%= t('back') %></span>
|
||||
</span>
|
||||
</label>
|
||||
<toggle-visible data-element-ids="<%= %w[document_view parties_view].to_json %>">
|
||||
<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" data-action="click:toggle-visible#trigger">
|
||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
||||
<%= svg_icon('users', class: 'w-8 h-8') %>
|
||||
<span><%= t('signers') %></span>
|
||||
</span>
|
||||
<span class="hidden peer-checked:flex items-center">
|
||||
<%= svg_icon('chevron_left', class: 'w-8 h-8') %>
|
||||
<span><%= t('back') %></span>
|
||||
</span>
|
||||
</label>
|
||||
</toggle-visible>
|
||||
</div>
|
||||
<%= render 'scripts/autosize_field' %>
|
||||
<% unless request.headers['HTTP_X_TURBO'] %>
|
||||
<%= render 'scripts/autosize_field' %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %>
|
||||
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
|
||||
<div class="space-y-2">
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
||||
<div class="enabled">
|
||||
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
||||
@@ -14,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
||||
<div class="enabled">
|
||||
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<span><%= t('status') %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||
<span><%= t('folder') %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('user', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<%= render 'filter_modal', title: t('folder'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - ['folder'])) do %>
|
||||
<div class="space-y-2">
|
||||
<div class="form-control mt-6">
|
||||
<folder-autocomplete class="flex justify-between w-full">
|
||||
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="base-input w-full" name="folder" value="<%= params[:folder] %>" autocomplete="off">
|
||||
</folder-autocomplete>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -84,7 +84,7 @@
|
||||
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
|
||||
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
|
||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
|
||||
<% end %>
|
||||
</div>
|
||||
</page-container>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<%= render 'layouts/head_tags' %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>
|
||||
<meta name="rollbar-token" content="<%= ENV.fetch('ROLLBAR_CLIENT_TOKEN', nil) %>">
|
||||
<%= javascript_pack_tag 'rollbar', 'draw', defer: true %>
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
<submitters-autocomplete data-field="email">
|
||||
<%= email_field_tag 'submitter[email]', @submitter.email, autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('email')} (#{t('optional')})" %>
|
||||
</submitters-autocomplete>
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})", oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')" %>
|
||||
</submitters-autocomplete>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})" %>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
</div>
|
||||
</submitter-item>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<%= render 'shared/search_input' %>
|
||||
<% end %>
|
||||
<% if can?(:create, ::Template) %>
|
||||
<%= render 'templates/upload_button', folder_name: @template_folder.full_name %>
|
||||
<span class="hidden sm:block">
|
||||
<%= render 'templates/upload_button', folder_name: @template_folder.full_name %>
|
||||
</span>
|
||||
<%= link_to new_template_path(folder_name: @template_folder.full_name), 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"><%= t('create') %></span>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user