Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa99b4ebdc | ||
|
|
83c793a2a5 | ||
|
|
d078727070 | ||
|
|
0d8e2c5ff0 | ||
|
|
ea062d7494 | ||
|
|
d525e0597a | ||
|
|
964b5d4e74 | ||
|
|
abf17eb09f | ||
|
|
178809dad7 | ||
|
|
881d189715 | ||
|
|
48a8973910 | ||
|
|
ae891932c5 | ||
|
|
a8b5e00814 | ||
|
|
e98432d7e4 | ||
|
|
29ff58ed98 | ||
|
|
a621575ffc | ||
|
|
d7c7efc203 | ||
|
|
4f013ca927 | ||
|
|
d9823feef7 | ||
|
|
676d775544 | ||
|
|
840ad17258 | ||
|
|
ce0cd09b70 | ||
|
|
bb752fbc00 | ||
|
|
c71fb04404 | ||
|
|
e856cf8787 | ||
|
|
71aae65bc6 | ||
|
|
7a33444f9b | ||
|
|
e12ec9d986 | ||
|
|
94d1fb7dcb | ||
|
|
6e43a42274 | ||
|
|
0e0aa0259e | ||
|
|
75ab6d39ef | ||
|
|
6f84d84f96 | ||
|
|
cf46593e9b | ||
|
|
1808346348 | ||
|
|
ea59a68499 | ||
|
|
cba2d02990 | ||
|
|
e92fc0d3e1 | ||
|
|
f1eaa77092 | ||
|
|
5934bdec55 | ||
|
|
7a7d3c6ee3 | ||
|
|
cb2f58c04c | ||
|
|
a1c117f1ca | ||
|
|
1aacd98460 | ||
|
|
840162c9ca | ||
|
|
3519b0e009 | ||
|
|
53fae56c51 | ||
|
|
f4552dbaa8 | ||
|
|
dfab4c331c | ||
|
|
d2a0937038 | ||
|
|
fa1ef44b22 | ||
|
|
c1a5c91299 | ||
|
|
e6280e8f5a | ||
|
|
fd2ba57325 | ||
|
|
dc178ef03f | ||
|
|
68936e10c5 | ||
|
|
41f16e7d39 | ||
|
|
453ebbf90a | ||
|
|
7ca290c080 | ||
|
|
dd5b6a6aa6 | ||
|
|
bf2ebb995b | ||
|
|
3e929758fb | ||
|
|
4a53116586 | ||
|
|
2d1981acfa | ||
|
|
c610df269f | ||
|
|
96dac635f5 | ||
|
|
abd579103d | ||
|
|
7ed27bb413 | ||
|
|
180c542a9f | ||
|
|
89613113de | ||
|
|
3102900776 | ||
|
|
1a68053f4e | ||
|
|
5d4a032763 | ||
|
|
7481d27822 | ||
|
|
22afd01fd2 | ||
|
|
fd9eb09dd6 | ||
|
|
c53ed7f984 | ||
|
|
1d87e210be | ||
|
|
5fec952b8c | ||
|
|
c4fbcc103a | ||
|
|
9333a1ba45 | ||
|
|
de5cd92dba | ||
|
|
83e32bec9f | ||
|
|
4603efc04e | ||
|
|
6c73c82a27 | ||
|
|
8e71d9f9e9 | ||
|
|
80f60a5d88 | ||
|
|
2c33ec382d | ||
|
|
884865cffc | ||
|
|
9537fe8c91 | ||
|
|
c8fe36a2b6 | ||
|
|
7ec3282b56 | ||
|
|
ab8c41a520 | ||
|
|
4c1c70ff04 | ||
|
|
d7a6e80bb1 | ||
|
|
f87ef670d1 | ||
|
|
daba2505bc | ||
|
|
71528eda45 | ||
|
|
269e2beeaa | ||
|
|
72dd2c6bc5 | ||
|
|
9758f7a15f |
No files matched your search
@@ -1,27 +1,14 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
tags:
|
||||||
version:
|
- "*.*.*"
|
||||||
description: Version
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
image:
|
|
||||||
description: QEMU image
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: tonistiigi/binfmt:latest
|
|
||||||
os:
|
|
||||||
description: OS
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: ubuntu-24.04-arm
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ inputs.os }}
|
runs-on: ubuntu-24.04-arm
|
||||||
timeout-minutes: 20
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -34,18 +21,16 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: docuseal/docuseal
|
images: docuseal/docuseal
|
||||||
tags: latest,${{ inputs.version }}
|
tags: type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
image: ${{ inputs.image }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Create .version file
|
- name: Create .version file
|
||||||
run: echo ${{ inputs.version }} > .version
|
run: echo ${{ github.ref_name }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ Metrics/PerceivedComplexity:
|
|||||||
Style/MultipleComparison:
|
Style/MultipleComparison:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ RUN apk --no-cache add fontforge wget && \
|
|||||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.0.0/model_704_int8.onnx" && \
|
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.1.0/model_704_int8.onnx" && \
|
||||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
@@ -90,7 +90,7 @@ COPY --from=download /model.onnx /app/tmp/model.onnx
|
|||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|||||||
@@ -77,24 +77,27 @@ GEM
|
|||||||
annotaterb (4.14.0)
|
annotaterb (4.14.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1027.0)
|
aws-partitions (1.1197.0)
|
||||||
aws-sdk-core (3.214.0)
|
aws-sdk-core (3.240.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.96.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-kms (1.118.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.239.1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.176.1)
|
aws-sdk-s3 (1.208.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.234.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-secretsmanager (1.110.0)
|
aws-sdk-secretsmanager (1.110.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.1)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class AccountsController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
|
true_user.skip_reconfirmation!
|
||||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
true_user.account.update!(archived_at: Time.current)
|
true_user.account.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ module Api
|
|||||||
rescue Submitters::MaliciousFileExtension => e
|
rescue Submitters::MaliciousFileExtension => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
|||||||
@@ -182,13 +182,13 @@ module Api
|
|||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa, :expire_at, :name,
|
:require_phone_2fa, :require_email_2fa, :expire_at, :name,
|
||||||
{
|
{
|
||||||
variables: {},
|
variables: {},
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa, :order,
|
:require_phone_2fa, :require_email_2fa, :order, :invite_by,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :required, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ module Api
|
|||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa,
|
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa, :require_email_2fa,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [[:name, :uuid, :default_value, :value, :required,
|
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class InvitationsController < Devise::PasswordsController
|
class InvitationsController < Devise::PasswordsController
|
||||||
|
def update
|
||||||
|
super do |resource|
|
||||||
|
resource.confirmed_at ||= Time.current if resource.errors.empty?
|
||||||
|
|
||||||
|
PasswordsController::Current.user = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -5,6 +5,8 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ class ProfileController < ApplicationController
|
|||||||
|
|
||||||
def update_contact
|
def update_contact
|
||||||
if current_user.update(contact_params)
|
if current_user.update(contact_params)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
if current_user.try(:pending_reconfirmation?) && current_user.previous_changes.key?(:unconfirmed_email)
|
||||||
|
SendConfirmationInstructionsJob.perform_async('user_id' => current_user.id)
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path,
|
||||||
|
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||||
|
else
|
||||||
|
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_content
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class StartFormController < ApplicationController
|
|||||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
|
end
|
||||||
|
|
||||||
if @template.shared_link?
|
if @template.shared_link?
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
@@ -81,7 +83,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions)
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.find_by!(required_params)
|
.find_by!(required_params.except('name'))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class SubmissionEventsController < ApplicationController
|
|||||||
'api_complete_form' => 'check',
|
'api_complete_form' => 'check',
|
||||||
'send_reminder_email' => 'mail_forward',
|
'send_reminder_email' => 'mail_forward',
|
||||||
'send_2fa_sms' => '2fa',
|
'send_2fa_sms' => '2fa',
|
||||||
|
'send_2fa_email' => '2fa',
|
||||||
'send_sms' => 'send',
|
'send_sms' => 'send',
|
||||||
'phone_verified' => 'phone_check',
|
'phone_verified' => 'phone_check',
|
||||||
'email_verified' => 'email_check',
|
'email_verified' => 'email_check',
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ class SubmissionsController < ApplicationController
|
|||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
FIELD_ICONS = {
|
||||||
|
'text' => 'text_size', 'signature' => 'writing_sign', 'date' => 'calendar_event',
|
||||||
|
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||||
|
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||||
|
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||||
|
'payment' => 'credit_card', 'verification' => 'id'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
@@ -45,14 +53,15 @@ class SubmissionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
submissions_attrs = submissions_params[:submission].to_h.values
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
submissions_attrs, =
|
submissions_attrs, _, new_fields =
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
submissions_attrs:,
|
submissions_attrs:,
|
||||||
|
new_fields:,
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,11 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def use_signature?(submission)
|
def use_signature?(submission)
|
||||||
return false if current_user && can?(:read, submission)
|
return false if current_user && can?(:read, submission)
|
||||||
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
|
return true if submission.submitters.any? do |e|
|
||||||
|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||||
|
end
|
||||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||||
|
|
||||||
!submission_valid_ttl?(submission)
|
!submission_valid_ttl?(submission)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class SubmitFormController < ApplicationController
|
|||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
return render :email_2fa if require_email_2fa?(@submitter)
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
@@ -47,6 +48,11 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
if require_email_2fa?(@submitter)
|
||||||
|
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -77,6 +83,8 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
def completed
|
def completed
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
|
||||||
|
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
@@ -109,4 +117,12 @@ class SubmitFormController < ApplicationController
|
|||||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_email_2fa?(submitter)
|
||||||
|
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||||
|
submitter.preferences['require_email_2fa'] != true
|
||||||
|
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormEmail2fasController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter
|
||||||
|
|
||||||
|
COOKIES_TTL = 12.hours
|
||||||
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
||||||
|
|
||||||
|
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||||
|
|
||||||
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
else
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
|
||||||
|
end
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
|
||||||
|
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
|
||||||
|
end
|
||||||
|
|
||||||
|
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
|
||||||
|
|
||||||
|
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
class TemplateDocumentsController < ApplicationController
|
class TemplateDocumentsController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TemplatesDebugController < ApplicationController
|
|||||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
fields = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
attachment.metadata['pdf'] ||= {}
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
class TemplatesPreferencesController < ApplicationController
|
class TemplatesPreferencesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
RESETTABLE_PREFERENCE_KEYS = {
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||||
|
invitation_reminder_email_body],
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||||
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
|
||||||
|
completed_notification_email_body]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
config_key = params[:config_key]
|
||||||
|
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
|
||||||
|
|
||||||
|
return head :ok if preferences_to_delete.blank?
|
||||||
|
|
||||||
|
preferences_to_delete.each do |key|
|
||||||
|
@template.preferences.delete(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace("#{config_key}_form",
|
||||||
|
partial: "templates_preferences/#{config_key}_form"),
|
||||||
|
status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
|
invitation_reminder_email_subject invitation_reminder_email_body
|
||||||
documents_copy_email_subject documents_copy_email_body
|
documents_copy_email_subject documents_copy_email_body
|
||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
require_all_submitters submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||||
default_expire_at_duration shared_link_2fa
|
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||||
default_expire_at request_email_enabled
|
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
|
|||||||
@@ -24,10 +24,19 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
@user.errors.add(:email, I18n.t('already_exists'))
|
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
if existing_user
|
||||||
|
if existing_user.archived_at? &&
|
||||||
|
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
|
||||||
|
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
|
||||||
|
existing_user.archived_at = nil
|
||||||
|
@user = existing_user
|
||||||
|
else
|
||||||
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.password = SecureRandom.hex if @user.password.blank?
|
@user.password = SecureRandom.hex if @user.password.blank?
|
||||||
@@ -45,7 +54,8 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
attrs = user_params.compact_blank
|
||||||
|
attrs = attrs.merge(user_params.slice(:archived_at)) if current_ability.can?(:create, @user)
|
||||||
|
|
||||||
if params.dig(:user, :account_id).present?
|
if params.dig(:user, :account_id).present?
|
||||||
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
||||||
@@ -56,7 +66,14 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
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')
|
if @user.try(:pending_reconfirmation?) && @user.previous_changes.key?(:unconfirmed_email)
|
||||||
|
SendConfirmationInstructionsJob.perform_async('user_id' => @user.id)
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_users_path,
|
||||||
|
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import ToggleClasses from './elements/toggle_classes'
|
|||||||
import AutosizeField from './elements/autosize_field'
|
import AutosizeField from './elements/autosize_field'
|
||||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||||
import OpenModal from './elements/open_modal'
|
import OpenModal from './elements/open_modal'
|
||||||
|
import BarChart from './elements/bar_chart'
|
||||||
|
import FieldCondition from './elements/field_condition'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -140,6 +142,8 @@ safeRegisterElement('toggle-classes', ToggleClasses)
|
|||||||
safeRegisterElement('autosize-field', AutosizeField)
|
safeRegisterElement('autosize-field', AutosizeField)
|
||||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||||
safeRegisterElement('open-modal', OpenModal)
|
safeRegisterElement('open-modal', OpenModal)
|
||||||
|
safeRegisterElement('bar-chart', BarChart)
|
||||||
|
safeRegisterElement('field-condition', FieldCondition)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -167,6 +171,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
|
||||||
|
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
|
||||||
|
|
||||||
|
this.initChart()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.chartInstance) {
|
||||||
|
this.chartInstance.destroy()
|
||||||
|
this.chartInstance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initChart () {
|
||||||
|
const { default: Chart } = await import(/* webpackChunkName: "chartjs" */ 'chart.js/auto')
|
||||||
|
|
||||||
|
const canvas = this.querySelector('canvas')
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
this.chartInstance = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: this.chartLabels,
|
||||||
|
datasets: this.chartDatasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grace: '20%',
|
||||||
|
ticks: {
|
||||||
|
precision: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.targetId = this.dataset.targetId
|
||||||
|
this.fieldId = this.dataset.fieldId
|
||||||
|
this.action = (this.dataset.action || '').trim()
|
||||||
|
this.expectedValue = this.dataset.value
|
||||||
|
|
||||||
|
this.targetEl = document.getElementById(this.targetId)
|
||||||
|
this.sourceEl = document.getElementById(this.fieldId)
|
||||||
|
|
||||||
|
this.bindListeners()
|
||||||
|
|
||||||
|
this.evaluateAndApply()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.unbindListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
bindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.addEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.removeEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsFor (el) {
|
||||||
|
if (!el) return []
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'textarea') return ['input']
|
||||||
|
if (tag === 'input') return ['input', 'change']
|
||||||
|
|
||||||
|
return ['change']
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateAndApply = () => {
|
||||||
|
const fieldConditions = document.querySelectorAll(`field-condition[data-target-id="${this.targetId}"]`)
|
||||||
|
|
||||||
|
const result = [...fieldConditions].reduce((acc, cond) => {
|
||||||
|
if (cond.dataset.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || cond.checkCondition())
|
||||||
|
} else {
|
||||||
|
acc.push(cond.checkCondition())
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
this.apply(!result.includes(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCondition () {
|
||||||
|
const action = this.action
|
||||||
|
const actual = this.getSourceValue()
|
||||||
|
const expected = this.expectedValue
|
||||||
|
|
||||||
|
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||||
|
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||||
|
|
||||||
|
if (action === 'equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'contains') return this.contains(actual, expected)
|
||||||
|
|
||||||
|
if (action === 'not_equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return !list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'does_not_contain') return !this.contains(actual, expected)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceValue () {
|
||||||
|
const el = this.sourceEl
|
||||||
|
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
const type = (el.getAttribute('type') || '').toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'select') return el.value
|
||||||
|
if (tag === 'textarea') return el.value
|
||||||
|
if (tag === 'input' && type === 'checkbox') return el.checked ? (el.value || '1') : null
|
||||||
|
if (tag === 'input') return el.value
|
||||||
|
|
||||||
|
return el.value ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty (obj) {
|
||||||
|
if (obj == null) return true
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj.trim().length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
return Object.keys(obj).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj === false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
contains (actual, expected) {
|
||||||
|
if (expected === null || expected === undefined) return false
|
||||||
|
|
||||||
|
const exp = String(expected)
|
||||||
|
|
||||||
|
if (Array.isArray(actual)) return actual.filter((v) => v !== null && v !== undefined).map(String).includes(exp)
|
||||||
|
|
||||||
|
if (typeof actual === 'string') return actual.includes(exp)
|
||||||
|
|
||||||
|
return actual !== null && actual !== undefined && String(actual) === exp
|
||||||
|
}
|
||||||
|
|
||||||
|
apply (passed) {
|
||||||
|
const controls = this.targetEl.matches('input, select, textarea, button')
|
||||||
|
? [this.targetEl]
|
||||||
|
: Array.from(this.targetEl.querySelectorAll('input, select, textarea, button'))
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
this.targetEl.style.display = ''
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = '' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = false))
|
||||||
|
} else {
|
||||||
|
this.targetEl.style.display = 'none'
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = 'none' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ export default {
|
|||||||
return style
|
return style
|
||||||
},
|
},
|
||||||
isNarrow () {
|
isNarrow () {
|
||||||
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
return this.area.h > 0 && ((this.area.w * this.pageWidth) / (this.area.h * this.pageHeight)) > 4.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
:scroll-el="scrollEl"
|
:scroll-el="scrollEl"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:with-label="!isAnonymousChecboxes && showFieldNames"
|
:with-label="withFieldLabels && !isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
@@ -133,6 +133,12 @@
|
|||||||
name="_method"
|
name="_method"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="validate === false"
|
||||||
|
value="false"
|
||||||
|
name="validate"
|
||||||
|
type="hidden"
|
||||||
|
>
|
||||||
<div class="md:mt-4">
|
<div class="md:mt-4">
|
||||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||||
<TextStep
|
<TextStep
|
||||||
@@ -351,8 +357,6 @@
|
|||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:checked="!!values[field.uuid]"
|
: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]]"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -533,7 +537,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="stepFields.length < 80"
|
v-if="stepFields.length < 80"
|
||||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
|
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
||||||
>
|
>
|
||||||
<div class="flex items-center flex-wrap steps-progress">
|
<div class="flex items-center flex-wrap steps-progress">
|
||||||
<a
|
<a
|
||||||
@@ -718,6 +722,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
validate: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withDisclosure: {
|
withDisclosure: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -728,6 +737,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withFieldLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const en = {
|
|||||||
esignature_disclosure: 'eSignature Disclosure',
|
esignature_disclosure: 'eSignature Disclosure',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Drawn signature on a touchscreen device',
|
sign_on_the_touchscreen: 'Sign on the touchscreen',
|
||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
reviewed: 'Reviewed',
|
reviewed: 'Reviewed',
|
||||||
other: 'Other',
|
other: 'Other',
|
||||||
@@ -120,7 +120,7 @@ const es = {
|
|||||||
select_a_reason: 'Selecciona una razón',
|
select_a_reason: 'Selecciona una razón',
|
||||||
value_is_invalid: 'El valor no es válido',
|
value_is_invalid: 'El valor no es válido',
|
||||||
verification_code_is_invalid: 'El código de verificación no es válido',
|
verification_code_is_invalid: 'El código de verificación no es válido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Firma dibujada en un dispositivo con pantalla táctil',
|
sign_on_the_touchscreen: 'Firmar en pantalla táctil',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||||
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||||
@@ -221,7 +221,7 @@ const it = {
|
|||||||
select_a_reason: 'Seleziona una ragione',
|
select_a_reason: 'Seleziona una ragione',
|
||||||
value_is_invalid: 'Il valore non è valido',
|
value_is_invalid: 'Il valore non è valido',
|
||||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
sign_on_the_touchscreen: 'Firma su schermo tattile',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
@@ -316,7 +316,7 @@ const de = {
|
|||||||
esignature_disclosure: 'Hinweis zur eSignatur',
|
esignature_disclosure: 'Hinweis zur eSignatur',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Auf einem Touchscreen-Gerät gezeichnete Unterschrift',
|
sign_on_the_touchscreen: 'Auf Touchscreen signieren',
|
||||||
approved: 'Genehmigt',
|
approved: 'Genehmigt',
|
||||||
reviewed: 'Geprüft',
|
reviewed: 'Geprüft',
|
||||||
other: 'Sonstiges',
|
other: 'Sonstiges',
|
||||||
@@ -417,7 +417,7 @@ const fr = {
|
|||||||
esignature_disclosure: 'Déclaration eSignature',
|
esignature_disclosure: 'Déclaration eSignature',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
sign_on_the_touchscreen: 'Signer sur écran tactile',
|
||||||
approved: 'Approuvé',
|
approved: 'Approuvé',
|
||||||
reviewed: 'Révisé',
|
reviewed: 'Révisé',
|
||||||
other: 'Autre',
|
other: 'Autre',
|
||||||
@@ -524,7 +524,7 @@ const pl = {
|
|||||||
select_a_reason: 'Wybierz powód',
|
select_a_reason: 'Wybierz powód',
|
||||||
value_is_invalid: 'Wartość jest nieprawidłowa',
|
value_is_invalid: 'Wartość jest nieprawidłowa',
|
||||||
verification_code_is_invalid: 'Kod weryfikacyjny jest nieprawidłowy',
|
verification_code_is_invalid: 'Kod weryfikacyjny jest nieprawidłowy',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Podpis odręczny na urządzeniu z ekranem dotykowym',
|
sign_on_the_touchscreen: 'Podpisz na ekranie dotykowym',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||||
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||||
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||||
@@ -625,7 +625,7 @@ const uk = {
|
|||||||
select_a_reason: 'Виберіть причину',
|
select_a_reason: 'Виберіть причину',
|
||||||
value_is_invalid: 'Значення є неправильним',
|
value_is_invalid: 'Значення є неправильним',
|
||||||
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Підпис на сенсорному пристрої',
|
sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||||
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||||
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||||
@@ -726,7 +726,7 @@ const cs = {
|
|||||||
select_a_reason: 'Vyberte důvod',
|
select_a_reason: 'Vyberte důvod',
|
||||||
value_is_invalid: 'Hodnota je neplatná',
|
value_is_invalid: 'Hodnota je neplatná',
|
||||||
verification_code_is_invalid: 'Ověřovací kód je neplatný',
|
verification_code_is_invalid: 'Ověřovací kód je neplatný',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Namalovaný podpis na dotykovém zařízení',
|
sign_on_the_touchscreen: 'Podepsat na dotykové obrazovce',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||||
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||||
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||||
@@ -827,7 +827,7 @@ const pt = {
|
|||||||
select_a_reason: 'Selecione um motivo',
|
select_a_reason: 'Selecione um motivo',
|
||||||
value_is_invalid: 'Valor é inválido',
|
value_is_invalid: 'Valor é inválido',
|
||||||
verification_code_is_invalid: 'Código de verificação é inválido',
|
verification_code_is_invalid: 'Código de verificação é inválido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Assinatura desenhada em um dispositivo com tela sensível ao toque',
|
sign_on_the_touchscreen: 'Assinar na tela sensível',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||||
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||||
@@ -928,7 +928,7 @@ const he = {
|
|||||||
select_a_reason: 'בחר סיבה',
|
select_a_reason: 'בחר סיבה',
|
||||||
value_is_invalid: 'ערך לא תקין',
|
value_is_invalid: 'ערך לא תקין',
|
||||||
verification_code_is_invalid: 'קוד האימות אינו תקין',
|
verification_code_is_invalid: 'קוד האימות אינו תקין',
|
||||||
drawn_signature_on_a_touchscreen_device: 'חתימה שנוצרה במכשיר עם מסך מגע',
|
sign_on_the_touchscreen: 'חתום על מסך המגע',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||||
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||||
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
@@ -1029,7 +1029,7 @@ const nl = {
|
|||||||
select_a_reason: 'Selecteer een reden',
|
select_a_reason: 'Selecteer een reden',
|
||||||
value_is_invalid: 'Waarde is ongeldig',
|
value_is_invalid: 'Waarde is ongeldig',
|
||||||
verification_code_is_invalid: 'Verificatiecode is ongeldig',
|
verification_code_is_invalid: 'Verificatiecode is ongeldig',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Getekende handtekening op een apparaat met een touchscreen',
|
sign_on_the_touchscreen: 'Onderteken op touchscreen',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
|
||||||
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
||||||
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
||||||
@@ -1131,7 +1131,7 @@ const ar = {
|
|||||||
value_is_invalid: 'القيمة غير صالحة',
|
value_is_invalid: 'القيمة غير صالحة',
|
||||||
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
||||||
already_paid: 'تم الدفع بالفعل',
|
already_paid: 'تم الدفع بالفعل',
|
||||||
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
sign_on_the_touchscreen: 'وقع على شاشة اللمس',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
@@ -1229,7 +1229,7 @@ const ko = {
|
|||||||
reviewed_by: '검토자',
|
reviewed_by: '검토자',
|
||||||
authored_by: '작성자',
|
authored_by: '작성자',
|
||||||
select_a_reason: '이유 선택',
|
select_a_reason: '이유 선택',
|
||||||
drawn_signature_on_a_touchscreen_device: '터치스크린 장치에서 그린 서명',
|
sign_on_the_touchscreen: '터치스크린에서 서명',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
|
||||||
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
electronic_signature_disclosure: '전자 서명 공개',
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
@@ -1326,7 +1326,7 @@ const ja = {
|
|||||||
esignature_disclosure: '電子署名開示',
|
esignature_disclosure: '電子署名開示',
|
||||||
signature: '署名',
|
signature: '署名',
|
||||||
initials: 'イニシャル',
|
initials: 'イニシャル',
|
||||||
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
|
sign_on_the_touchscreen: 'タッチスクリーンで署名',
|
||||||
approved: '承認済み',
|
approved: '承認済み',
|
||||||
reviewed: '確認済み',
|
reviewed: '確認済み',
|
||||||
other: 'その他',
|
other: 'その他',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="space-x-2 flex flex-none">
|
<div class="space-x-2 flex flex-none">
|
||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('type_initial')"
|
:data-tip="t('type_initial')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="tooltip ml-2"
|
class="md:tooltip ml-2"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('click_to_upload')"
|
:data-tip="t('click_to_upload')"
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="phone"
|
ref="phone"
|
||||||
:value="phoneValue"
|
:value="defaultValue && detectedPhoneValueDialCode ? phoneValue.split('+' + detectedPhoneValueDialCode).slice(-1).join('') : phoneValue"
|
||||||
:readonly="!!defaultValue"
|
:readonly="!!defaultValue"
|
||||||
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="space-x-2 flex flex-none">
|
<div class="space-x-2 flex flex-none">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
||||||
class="tooltip ml-2"
|
class="md:tooltip ml-2"
|
||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
>
|
>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('take_photo')"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class="tooltip before:translate-x-[-90%]"
|
class="md:tooltip before:translate-x-[-90%]"
|
||||||
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
:data-tip="t('sign_on_the_touchscreen')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="relative"
|
class="relative select-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute top-1.5 right-1.5 tooltip"
|
class="absolute top-1.5 right-1.5 md:tooltip"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
v-if="requireSigningReason && !isOtherReason"
|
v-if="withSigningReason && !isOtherReason"
|
||||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||||
:class="{ 'text-gray-300': !reason }"
|
:class="{ 'text-gray-300': !reason }"
|
||||||
required
|
required
|
||||||
@@ -226,24 +226,37 @@
|
|||||||
>
|
>
|
||||||
{{ t('select_a_reason') }}
|
{{ t('select_a_reason') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<template v-if="field.preferences?.reasons">
|
||||||
v-for="(label, option) in defaultReasons"
|
<option
|
||||||
:key="option"
|
v-for="option in field.preferences.reasons"
|
||||||
:value="option"
|
:key="option"
|
||||||
:selected="reason === option"
|
:value="option"
|
||||||
class="text-base-content"
|
:selected="reason === option"
|
||||||
>
|
class="text-base-content"
|
||||||
{{ label }}
|
>
|
||||||
</option>
|
{{ option }}
|
||||||
<option
|
</option>
|
||||||
value="other"
|
</template>
|
||||||
class="text-base-content"
|
<template v-else>
|
||||||
>
|
<option
|
||||||
{{ t('other') }}
|
v-for="(label, option) in defaultReasons"
|
||||||
</option>
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
:selected="reason === option"
|
||||||
|
class="text-base-content"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="other"
|
||||||
|
class="text-base-content"
|
||||||
|
>
|
||||||
|
{{ t('other') }}
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
v-if="requireSigningReason && isOtherReason"
|
v-if="withSigningReason && isOtherReason"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
required
|
required
|
||||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||||
@@ -253,7 +266,7 @@
|
|||||||
@input="$emit('update:reason', $event.target.value)"
|
@input="$emit('update:reason', $event.target.value)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="requireSigningReason"
|
v-if="withSigningReason"
|
||||||
hidden
|
hidden
|
||||||
name="with_reason"
|
name="with_reason"
|
||||||
:value="field.preferences.reason_field_uuid"
|
:value="field.preferences.reason_field_uuid"
|
||||||
@@ -268,7 +281,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="withDisclosure"
|
v-else-if="withDisclosure"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="text-base-content/60 text-xs text-center w-full mt-1"
|
class="text-base-content/60 text-xs text-center w-full mt-1 select-none"
|
||||||
>
|
>
|
||||||
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||||
href="https://www.docuseal.com/esign-disclosure"
|
href="https://www.docuseal.com/esign-disclosure"
|
||||||
@@ -406,6 +419,9 @@ export default {
|
|||||||
format () {
|
format () {
|
||||||
return this.field.preferences?.format
|
return this.field.preferences?.format
|
||||||
},
|
},
|
||||||
|
withSigningReason () {
|
||||||
|
return this.requireSigningReason || this.field.preferences?.reasons?.length
|
||||||
|
},
|
||||||
defaultReasons () {
|
defaultReasons () {
|
||||||
return {
|
return {
|
||||||
[this.t('approved_by')]: this.t('approved'),
|
[this.t('approved_by')]: this.t('approved'),
|
||||||
@@ -424,10 +440,11 @@ export default {
|
|||||||
created () {
|
created () {
|
||||||
this.isSignatureStarted = !!this.computedPreviousValue
|
this.isSignatureStarted = !!this.computedPreviousValue
|
||||||
|
|
||||||
if (this.requireSigningReason) {
|
if (this.withSigningReason) {
|
||||||
this.field.preferences ||= {}
|
this.field.preferences ||= {}
|
||||||
this.field.preferences.reason_field_uuid ||= v4()
|
this.field.preferences.reason_field_uuid ||= v4()
|
||||||
this.isOtherReason = this.reason && !this.defaultReasons[this.reason]
|
this.isOtherReason = this.reason && !this.defaultReasons[this.reason] &&
|
||||||
|
(!this.field.preferences?.reasons?.length || !this.field.preferences.reasons.includes(this.reason))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
|
|||||||
@@ -175,7 +175,10 @@
|
|||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown dropdown-end">
|
<div
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
:class="{ 'dropdown-open': isDownloading }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
|
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
|
||||||
@@ -209,6 +212,30 @@
|
|||||||
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="withDownload">
|
||||||
|
<button
|
||||||
|
class="flex space-x-2"
|
||||||
|
:disabled="isDownloading"
|
||||||
|
@click.stop.prevent="download"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isDownloading"
|
||||||
|
class="animate-spin w-6 h-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<IconDownload
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="isDownloading"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>{{ t('downloading_') }}</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>{{ t('download') }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -457,6 +484,7 @@
|
|||||||
:show-tour-start-form="showTourStartForm"
|
:show-tour-start-form="showTourStartForm"
|
||||||
@add-field="addField"
|
@add-field="addField"
|
||||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
@select-submitter="selectedSubmitter = $event"
|
||||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||||
@set-drag="dragField = $event"
|
@set-drag="dragField = $event"
|
||||||
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||||
@@ -511,7 +539,7 @@ import DocumentPreview from './preview'
|
|||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import MobileFields from './mobile_fields'
|
import MobileFields from './mobile_fields'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed, toRaw } from 'vue'
|
import { ref, computed, toRaw } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
@@ -537,6 +565,7 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
IconDownload,
|
||||||
IconAdjustments,
|
IconAdjustments,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconDeviceFloppy
|
IconDeviceFloppy
|
||||||
@@ -584,6 +613,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withDownload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -805,6 +839,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
documentRefs: [],
|
documentRefs: [],
|
||||||
isBreakpointLg: false,
|
isBreakpointLg: false,
|
||||||
|
isDownloading: false,
|
||||||
isLoadingBlankPage: false,
|
isLoadingBlankPage: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
@@ -963,6 +998,75 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toRaw,
|
toRaw,
|
||||||
|
download () {
|
||||||
|
this.isDownloading = true
|
||||||
|
|
||||||
|
this.baseFetch(`/templates/${this.template.id}/documents`).then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const urls = await response.json()
|
||||||
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
|
if (isSafariIos && urls.length > 1) {
|
||||||
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(this.t('failed_to_download_files'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
).finally(() => {
|
||||||
|
this.isDownloading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise.all(fileRequests).then((links) => {
|
||||||
|
links.forEach((link, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.isDownloading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
onDragover (e) {
|
onDragover (e) {
|
||||||
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
||||||
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
||||||
|
|||||||
@@ -154,15 +154,22 @@
|
|||||||
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
||||||
ref="options"
|
ref="options"
|
||||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
draggable="true"
|
@dragover="onOptionDragover"
|
||||||
@dragstart.prevent.stop
|
@drop="reorderOptions"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
class="flex space-x-1.5 items-center"
|
class="flex space-x-1.5 items-center"
|
||||||
|
:data-option-uuid="option.uuid"
|
||||||
>
|
>
|
||||||
<span class="text-sm w-3.5">
|
<span
|
||||||
|
class="text-sm w-3.5 cursor-grab select-none"
|
||||||
|
:draggable="editable && !defaultField"
|
||||||
|
@dragstart.stop="onOptionDragstart($event, option)"
|
||||||
|
@dragend.stop="optionDragRef = null"
|
||||||
|
@dragover.prevent.stop="onOptionDragover"
|
||||||
|
>
|
||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
@@ -176,6 +183,7 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
required
|
required
|
||||||
:placeholder="`${t('option')} ${index + 1}`"
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -198,6 +206,7 @@
|
|||||||
:readonly="!editable || defaultField"
|
:readonly="!editable || defaultField"
|
||||||
required
|
required
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
@focus="maybeFocusOnOptionArea(option)"
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
@@ -217,7 +226,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="field.options && editable && !defaultField"
|
v-else-if="field.options && editable && !defaultField"
|
||||||
class="field-add-option text-center text-sm w-full pb-1"
|
class="field-add-option text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOptionAt(field.options.length)"
|
||||||
>
|
>
|
||||||
+ {{ t('add_option') }}
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -363,7 +372,8 @@ export default {
|
|||||||
isShowFontModal: false,
|
isShowFontModal: false,
|
||||||
isShowConditionsModal: false,
|
isShowConditionsModal: false,
|
||||||
isShowDescriptionModal: false,
|
isShowDescriptionModal: false,
|
||||||
renderDropdown: false
|
renderDropdown: false,
|
||||||
|
optionDragRef: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -450,15 +460,17 @@ export default {
|
|||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
addOption () {
|
addOptionAt (index) {
|
||||||
this.isExpandOptions = true
|
this.isExpandOptions = true
|
||||||
|
|
||||||
this.field.options.push({ value: '', uuid: v4() })
|
const insertAt = index ?? this.field.options.length
|
||||||
|
|
||||||
|
this.field.options.splice(insertAt, 0, { value: '', uuid: v4() })
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const inputs = this.$refs.options.querySelectorAll('input')
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
inputs[inputs.length - 1]?.focus()
|
inputs[insertAt]?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
@@ -515,6 +527,70 @@ export default {
|
|||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
|
},
|
||||||
|
onOptionDragstart (event, option) {
|
||||||
|
this.optionDragRef = option
|
||||||
|
|
||||||
|
const root = this.$el.getRootNode()
|
||||||
|
const hiddenEl = document.createElement('div')
|
||||||
|
|
||||||
|
hiddenEl.style.width = '1px'
|
||||||
|
hiddenEl.style.height = '1px'
|
||||||
|
hiddenEl.style.opacity = '0'
|
||||||
|
hiddenEl.style.position = 'fixed'
|
||||||
|
|
||||||
|
root.querySelector('#docuseal_modal_container')?.appendChild(hiddenEl)
|
||||||
|
event.dataTransfer?.setDragImage(hiddenEl, 0, 0)
|
||||||
|
|
||||||
|
setTimeout(() => { hiddenEl.remove() }, 1000)
|
||||||
|
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
},
|
||||||
|
onOptionDragover (e) {
|
||||||
|
if (!this.optionDragRef) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const targetRow = e.target.closest('[data-option-uuid]')
|
||||||
|
|
||||||
|
if (!targetRow) return
|
||||||
|
|
||||||
|
const dragRow = this.$refs.options?.querySelector(`[data-option-uuid="${this.optionDragRef.uuid}"]`)
|
||||||
|
|
||||||
|
if (!dragRow) return
|
||||||
|
if (targetRow === dragRow) return
|
||||||
|
|
||||||
|
const rows = Array.from(this.$refs.options.querySelectorAll('[data-option-uuid]'))
|
||||||
|
|
||||||
|
const currentIndex = rows.indexOf(dragRow)
|
||||||
|
const targetIndex = rows.indexOf(targetRow)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
targetRow.after(dragRow)
|
||||||
|
} else {
|
||||||
|
targetRow.before(dragRow)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reorderOptions (e) {
|
||||||
|
if (!this.optionDragRef) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const rows = Array.from(this.$refs.options.querySelectorAll('[data-option-uuid]'))
|
||||||
|
|
||||||
|
const newOrder = rows
|
||||||
|
.map((el) => this.field.options.find((opt) => opt.uuid === el.dataset.optionUuid))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
if (newOrder.length === this.field.options.length) {
|
||||||
|
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.optionDragRef = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
ref="fields"
|
ref="fields"
|
||||||
class="fields mb-1 mt-2"
|
class="fields mb-1 mt-2"
|
||||||
@dragover.prevent="onFieldDragover"
|
@dragover.prevent="onFieldDragover"
|
||||||
@drop="reorderFields"
|
@drop="fieldsDragFieldRef.value ? reorderFields() : null"
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
@@ -222,12 +222,21 @@
|
|||||||
width="22"
|
width="22"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span
|
||||||
|
v-if="analyzingProgress"
|
||||||
|
class="hidden md:inline"
|
||||||
|
>
|
||||||
|
{{ Math.round(analyzingProgress * 100) }}% {{ t('analyzing_') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="hidden md:inline"
|
||||||
|
>
|
||||||
{{ fieldPagesLoaded }} / {{ numberOfPages }} {{ t('processing_') }}
|
{{ fieldPagesLoaded }} / {{ numberOfPages }} {{ t('processing_') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<IconListSearch width="22" />
|
<IconSparkles width="22" />
|
||||||
<span
|
<span
|
||||||
class="hidden md:inline"
|
class="hidden md:inline"
|
||||||
>
|
>
|
||||||
@@ -259,7 +268,7 @@
|
|||||||
import Field from './field'
|
import Field from './field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconListSearch } from '@tabler/icons-vue'
|
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
|
||||||
import IconDrag from './icon_drag'
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -268,7 +277,7 @@ export default {
|
|||||||
Field,
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
IconCirclePlus,
|
IconCirclePlus,
|
||||||
IconListSearch,
|
IconSparkles,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
@@ -363,10 +372,11 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
|
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
fieldPagesLoaded: null,
|
fieldPagesLoaded: null,
|
||||||
|
analyzingProgress: 0,
|
||||||
defaultFieldsSearch: ''
|
defaultFieldsSearch: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -448,8 +458,6 @@ export default {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader.read()
|
const { value, done } = await reader.read()
|
||||||
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
const lines = buffer.split('\n\n')
|
const lines = buffer.split('\n\n')
|
||||||
@@ -462,12 +470,27 @@ export default {
|
|||||||
const data = JSON.parse(jsonStr)
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
alert(data.error)
|
if ((data.fields || fields).length) {
|
||||||
|
this.template.fields = data.fields || fields
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else {
|
||||||
|
alert(data.error)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
} else if (data.analyzing) {
|
||||||
|
this.analyzingProgress = data.progress
|
||||||
} else if (data.completed) {
|
} else if (data.completed) {
|
||||||
this.fieldPagesLoaded = null
|
this.fieldPagesLoaded = null
|
||||||
this.template.fields = fields
|
|
||||||
|
if (data.submitters) {
|
||||||
|
this.template.submitters = data.submitters
|
||||||
|
this.$emit('select-submitter', this.template.submitters[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields = data.fields || fields
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -484,11 +507,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (done) break
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error in streaming message: ', error)
|
console.error('Error in streaming message: ', error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.fieldPagesLoaded = null
|
this.fieldPagesLoaded = null
|
||||||
|
this.analyzingProgress = null
|
||||||
this.isFieldsLoading = false
|
this.isFieldsLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
analyzing_: 'Analyzing...',
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Downloading...',
|
||||||
view: 'View',
|
view: 'View',
|
||||||
autodetect_fields: 'Autodetect fields',
|
autodetect_fields: 'Autodetect fields',
|
||||||
payment_link: 'Payment link',
|
payment_link: 'Payment link',
|
||||||
@@ -185,6 +188,10 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
autodetect_fields: 'Autodetectar campos',
|
||||||
|
analyzing_: 'Analizando...',
|
||||||
|
download: 'Descargar',
|
||||||
|
downloading_: 'Descargando...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Enlace de pago',
|
payment_link: 'Enlace de pago',
|
||||||
strikeout: 'Tachar',
|
strikeout: 'Tachar',
|
||||||
@@ -370,6 +377,10 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
autodetect_fields: 'Rileva campi',
|
||||||
|
analyzing_: 'Analisi...',
|
||||||
|
download: 'Scarica',
|
||||||
|
downloading_: 'Download in corso...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Link di pagamento',
|
payment_link: 'Link di pagamento',
|
||||||
strikeout: 'Barrato',
|
strikeout: 'Barrato',
|
||||||
@@ -555,6 +566,10 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
autodetect_fields: 'Detectar campos',
|
||||||
|
analyzing_: 'Analisando...',
|
||||||
|
download: 'Baixar',
|
||||||
|
downloading_: 'Baixando...',
|
||||||
view: 'Visualizar',
|
view: 'Visualizar',
|
||||||
payment_link: 'Link de pagamento',
|
payment_link: 'Link de pagamento',
|
||||||
strikeout: 'Tachado',
|
strikeout: 'Tachado',
|
||||||
@@ -740,6 +755,10 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
autodetect_fields: 'Détecter les champs',
|
||||||
|
analyzing_: 'Analyse...',
|
||||||
|
download: 'Télécharger',
|
||||||
|
downloading_: 'Téléchargement...',
|
||||||
view: 'Voir',
|
view: 'Voir',
|
||||||
payment_link: 'Lien de paiement',
|
payment_link: 'Lien de paiement',
|
||||||
strikeout: 'Rature',
|
strikeout: 'Rature',
|
||||||
@@ -925,6 +944,10 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
autodetect_fields: 'Felder erkennen',
|
||||||
|
analyzing_: 'Analysiere...',
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Download...',
|
||||||
view: 'Anzeigen',
|
view: 'Anzeigen',
|
||||||
payment_link: 'Zahlungslink',
|
payment_link: 'Zahlungslink',
|
||||||
strikeout: 'Durchstreichen',
|
strikeout: 'Durchstreichen',
|
||||||
@@ -1110,6 +1133,10 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
autodetect_fields: 'Velden detecteren',
|
||||||
|
analyzing_: 'Analyseren...',
|
||||||
|
download: 'Downloaden',
|
||||||
|
downloading_: 'Downloaden...',
|
||||||
view: 'Bekijken',
|
view: 'Bekijken',
|
||||||
payment_link: 'Betaallink',
|
payment_link: 'Betaallink',
|
||||||
strikeout: 'Doorhalen',
|
strikeout: 'Doorhalen',
|
||||||
|
|||||||
@@ -49,7 +49,12 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<div
|
||||||
|
v-if="fieldTypes.length && submitterDefaultFields.length"
|
||||||
|
class="bg-base-300"
|
||||||
|
style="height: 1px; margin-top: 1px; margin-bottom: 1px"
|
||||||
|
/>
|
||||||
|
<template v-if="fieldTypes.length || !submitterDefaultFields.length">
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ class ProcessSubmissionExpiredJob
|
|||||||
include Sidekiq::Job
|
include Sidekiq::Job
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
return if submission.archived_at?
|
return if submission.archived_at?
|
||||||
return if submission.template&.archived_at?
|
return if submission.template&.archived_at?
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
completed_submitter.assign_attributes(
|
completed_submitter.assign_attributes(
|
||||||
submission_id: submitter.submission_id,
|
submission_id: submitter.submission_id,
|
||||||
account_id: submission.account_id,
|
account_id: submission.account_id,
|
||||||
|
is_first: !CompletedSubmitter.exists?(submission: submitter.submission_id, is_first: true),
|
||||||
template_id: submission.template_id,
|
template_id: submission.template_id,
|
||||||
source: submission.source,
|
source: submission.source,
|
||||||
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendConfirmationInstructionsJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
user = User.find(params['user_id'])
|
||||||
|
|
||||||
|
user.send_confirmation_instructions
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,13 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 12
|
MAX_ATTEMPTS = 12
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendFormStartedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendFormViewedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionCompletedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionExpiredWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class SendSubmitterInvitationEmailJob
|
|||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
return if submitter.completed_at?
|
return if submitter.completed_at?
|
||||||
|
return if submitter.submission.archived_at?
|
||||||
|
return if submitter.template&.archived_at?
|
||||||
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
||||||
|
|
||||||
unless Accounts.can_send_invitation_emails?(submitter.account)
|
unless Accounts.can_send_invitation_emails?(submitter.account)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendSubmitterVerificationEmailJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
|
locale = params['locale'].presence || submitter.account.locale
|
||||||
|
|
||||||
|
SubmitterMailer.otp_verification_email(submitter, locale:).deliver_now!
|
||||||
|
|
||||||
|
SubmissionEvent.create!(submitter_id: params['submitter_id'],
|
||||||
|
event_type: 'send_2fa_email',
|
||||||
|
data: { email: submitter.email })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,13 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
template = Template.find(params['template_id'])
|
template = Template.find_by(id: params['template_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless template
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
template = Template.find(params['template_id'])
|
template = Template.find_by(id: params['template_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless template
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,28 @@ class SendTestWebhookRequestJob
|
|||||||
|
|
||||||
USER_AGENT = 'DocuSeal.com Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
def perform(params = {})
|
HttpsError = Class.new(StandardError)
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
LocalhostError = Class.new(StandardError)
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
|
||||||
return unless webhook_url && submitter
|
def perform(params = {})
|
||||||
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
uri = begin
|
||||||
|
URI(webhook_url.url)
|
||||||
|
rescue URI::Error
|
||||||
|
Addressable::URI.parse(webhook_url.url).normalize
|
||||||
|
end
|
||||||
|
|
||||||
|
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
|
||||||
|
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||||
|
end
|
||||||
|
|
||||||
Faraday.post(webhook_url.url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -144,6 +144,17 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def otp_verification_email(submitter, locale: nil)
|
||||||
|
@submitter = submitter
|
||||||
|
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
|
||||||
|
|
||||||
|
assign_message_metadata('otp_verification_email', submitter)
|
||||||
|
|
||||||
|
I18n.with_locale(locale || submitter.account.locale) do
|
||||||
|
mail(to: submitter.email, subject: I18n.t('email_verification'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
|
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ class Account < ApplicationRecord
|
|||||||
linked_account_account&.testing?
|
linked_account_account&.testing?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tz_info
|
||||||
|
@tz_info ||= TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
|
||||||
|
end
|
||||||
|
|
||||||
def default_template_folder
|
def default_template_folder
|
||||||
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
|
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
|
||||||
author_id: users.minimum(:id)).tap(&:save!)
|
author_id: users.minimum(:id)).tap(&:save!)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class AccountConfig < ApplicationRecord
|
class AccountConfig < ApplicationRecord
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||||
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
@@ -44,10 +45,12 @@ class AccountConfig < ApplicationRecord
|
|||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_FILE_LINKS_KEY = 'with_file_links'
|
WITH_FILE_LINKS_KEY = 'with_file_links'
|
||||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||||
|
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
|
||||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||||
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
||||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||||
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||||
|
WITH_FIELD_LABELS_KEY = 'with_field_labels'
|
||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||||
POLICY_LINKS_KEY = 'policy_links'
|
POLICY_LINKS_KEY = 'policy_links'
|
||||||
@@ -59,6 +62,12 @@ class AccountConfig < ApplicationRecord
|
|||||||
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
|
||||||
|
{
|
||||||
|
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
|
||||||
|
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
||||||
|
}
|
||||||
|
},
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
|
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
|
||||||
{
|
{
|
||||||
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
|
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# completed_at :datetime not null
|
# completed_at :datetime not null
|
||||||
|
# is_first :boolean
|
||||||
# sms_count :integer not null
|
# sms_count :integer not null
|
||||||
# source :string not null
|
# source :string not null
|
||||||
# verification_method :string
|
# verification_method :string
|
||||||
@@ -18,8 +19,10 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_completed_submitters_on_account_id (account_id)
|
# index_completed_submitters_account_id_completed_at_is_first (account_id,completed_at) WHERE (is_first = true)
|
||||||
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
|
# index_completed_submitters_on_account_id_and_completed_at (account_id,completed_at)
|
||||||
|
# index_completed_submitters_on_submission_id (submission_id) UNIQUE WHERE (is_first = true)
|
||||||
|
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
|
||||||
#
|
#
|
||||||
class CompletedSubmitter < ApplicationRecord
|
class CompletedSubmitter < ApplicationRecord
|
||||||
belongs_to :submitter
|
belongs_to :submitter
|
||||||
|
|||||||
@@ -10,17 +10,21 @@
|
|||||||
# event_type :string not null
|
# event_type :string not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint
|
||||||
# submission_id :bigint not null
|
# submission_id :bigint not null
|
||||||
# submitter_id :bigint
|
# submitter_id :bigint
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submission_events_on_created_at (created_at)
|
# index_submission_events_on_account_id (account_id)
|
||||||
# index_submission_events_on_submission_id (submission_id)
|
# index_submission_events_on_created_at (created_at)
|
||||||
# index_submission_events_on_submitter_id (submitter_id)
|
# index_submission_events_on_submission_id (submission_id)
|
||||||
|
# index_submission_events_on_submitter_id (submitter_id)
|
||||||
|
# index_submissions_events_on_sms_event_types (account_id,created_at) WHERE ((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (submission_id => submissions.id)
|
# fk_rails_... (submission_id => submissions.id)
|
||||||
# fk_rails_... (submitter_id => submitters.id)
|
# fk_rails_... (submitter_id => submitters.id)
|
||||||
#
|
#
|
||||||
@@ -35,6 +39,7 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
serialize :data, coder: JSON
|
serialize :data, coder: JSON
|
||||||
|
|
||||||
before_validation :set_submission_id, on: :create
|
before_validation :set_submission_id, on: :create
|
||||||
|
before_validation :set_account_id, on: :create
|
||||||
|
|
||||||
enum :event_type, {
|
enum :event_type, {
|
||||||
send_email: 'send_email',
|
send_email: 'send_email',
|
||||||
@@ -43,6 +48,7 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
send_reminder_email: 'send_reminder_email',
|
send_reminder_email: 'send_reminder_email',
|
||||||
send_sms: 'send_sms',
|
send_sms: 'send_sms',
|
||||||
send_2fa_sms: 'send_2fa_sms',
|
send_2fa_sms: 'send_2fa_sms',
|
||||||
|
send_2fa_email: 'send_2fa_email',
|
||||||
open_email: 'open_email',
|
open_email: 'open_email',
|
||||||
click_email: 'click_email',
|
click_email: 'click_email',
|
||||||
click_sms: 'click_sms',
|
click_sms: 'click_sms',
|
||||||
@@ -63,4 +69,8 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
def set_submission_id
|
def set_submission_id
|
||||||
self.submission_id = submitter&.submission_id
|
self.submission_id = submitter&.submission_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_account_id
|
||||||
|
self.account_id = submitter&.account_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# archived_at :datetime
|
# archived_at :datetime
|
||||||
|
# confirmation_sent_at :datetime
|
||||||
|
# confirmation_token :string
|
||||||
|
# confirmed_at :datetime
|
||||||
# consumed_timestep :integer
|
# consumed_timestep :integer
|
||||||
# current_sign_in_at :datetime
|
# current_sign_in_at :datetime
|
||||||
# current_sign_in_ip :string
|
# current_sign_in_ip :string
|
||||||
@@ -24,8 +27,9 @@
|
|||||||
# reset_password_token :string
|
# reset_password_token :string
|
||||||
# role :string not null
|
# role :string not null
|
||||||
# sign_in_count :integer default(0), not null
|
# sign_in_count :integer default(0), not null
|
||||||
|
# unconfirmed_email :string
|
||||||
# unlock_token :string
|
# unlock_token :string
|
||||||
# uuid :text not null
|
# uuid :string not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint not null
|
# account_id :bigint not null
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<p>Hello <%= @resource.email %>!</p>
|
<p><%= @resource.first_name.present? ? t('hello_name', name: @resource.first_name) : t('hi_there') %>,</p>
|
||||||
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
|
<p><%= t('you_requested_to_reset_your_password_use_the_link_below_to_continue') %>:</p>
|
||||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
<p><%= link_to t('change_my_password'), edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||||
<p>If you didn't request this, please ignore this email.</p>
|
<p><%= t('your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one') %></p>
|
||||||
<p>Your password won't change until you access the link above and create a new one.</p>
|
<p><%= t('if_you_didnt_request_this_you_can_ignore_this_email') %></p>
|
||||||
|
<p>
|
||||||
|
<%= t('thanks') %>,<br>
|
||||||
|
<%= Docuseal.product_name %>
|
||||||
|
</p>
|
||||||
|
<% content_for(:remove_attribution, true) %>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 5m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M16 3l0 4"></path>
|
||||||
|
<path d="M8 3l0 4"></path>
|
||||||
|
<path d="M4 11l16 0"></path>
|
||||||
|
<path d="M8 15h2v2h-2z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 450 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 11l3 3l8 -8"></path>
|
||||||
|
<path d="M20 12v6a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h9"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 346 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M7 12l5 5l10 -10"></path>
|
||||||
|
<path d="M2 12l5 5m5 -5l5 -5"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 3m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1zm6 -1v18m6 -18v18"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 5m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path>
|
||||||
|
<path d="M3 10l18 0"></path>
|
||||||
|
<path d="M7 15l.01 0"></path>
|
||||||
|
<path d="M11 15l2 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10z" />
|
||||||
|
<path d="M7 20h10" /><path d="M9 16v4" />
|
||||||
|
<path d="M15 16v4" /></svg>
|
||||||
|
<path d="M9 16v4" />
|
||||||
|
<path d="M15 16v4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 497 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M6 5a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2v-14z" />
|
||||||
|
<path d="M11 4h2" />
|
||||||
|
<path d="M12 17v.01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 423 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M5 4a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1v-16z" />
|
||||||
|
<path d="M11 17a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v10a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path>
|
||||||
|
<path d="M9 10m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
|
||||||
|
<path d="M15 8l2 0"></path>
|
||||||
|
<path d="M15 12l2 0"></path>
|
||||||
|
<path d="M7 16l10 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 478 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 19v-10.5a3.5 3.5 0 0 1 7 0v10.5"></path>
|
||||||
|
<path d="M3 13h7"></path>
|
||||||
|
<path d="M14 19v-10.5a3.5 3.5 0 0 1 7 0v10.5"></path>
|
||||||
|
<path d="M14 13h7"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 394 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15 8h.01"></path>
|
||||||
|
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"></path>
|
||||||
|
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"></path>
|
||||||
|
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 17.85h-18c0 -4.05 1.421 -4.05 3.79 -4.05c5.21 0 1.21 -4.59 1.21 -6.8a4 4 0 1 1 8 0c0 2.21 -4 6.8 1.21 6.8c2.369 0 3.79 0 3.79 4.05z"></path>
|
||||||
|
<path d="M5 21h14"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 411 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M9 11l3 3l3 -3"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M10 10l2 -2v8"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 360 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 7v-2h13v2"></path>
|
||||||
|
<path d="M10 5v14"></path>
|
||||||
|
<path d="M12 19h-4"></path>
|
||||||
|
<path d="M15 13v-1h6v1"></path>
|
||||||
|
<path d="M18 12v7"></path>
|
||||||
|
<path d="M17 19h2"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 410 B |
@@ -3,7 +3,7 @@
|
|||||||
<%= svg_icon('waving_hand', class: 'h-10 w-10') %>
|
<%= svg_icon('waving_hand', class: 'h-10 w-10') %>
|
||||||
<span><%= t('welcome_to_product_name', product_name: Docuseal.product_name) %></span>
|
<span><%= t('welcome_to_product_name', product_name: Docuseal.product_name) %></span>
|
||||||
</h1>
|
</h1>
|
||||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: invitation_path, html: { method: :put, class: 'space-y-6' }) do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
<%= render 'devise/shared/error_messages', resource: %>
|
||||||
<%= f.hidden_field :reset_password_token %>
|
<%= f.hidden_field :reset_password_token %>
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :email, t('email'), class: 'label' %>
|
<%= f.label :email, t('email'), class: 'label' %>
|
||||||
<%= f.email_field :email, autocomplete: 'off', class: 'base-input' %>
|
<%= f.email_field :email, autocomplete: 'off', class: 'base-input' %>
|
||||||
|
<% if current_user.try(:pending_reconfirmation?) %>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt"><%= t('email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm', email: f.object.unconfirmed_email) %></span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control pt-2">
|
<div class="form-control pt-2">
|
||||||
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
||||||
|
|||||||
@@ -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">
|
<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">
|
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||||
<%= svg_icon('start', class: 'h-3 w-3') %>
|
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||||
<span>10k</span>
|
<span>11k</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
|
||||||
<%= t('plans') %>
|
<%= t('plans') %>
|
||||||
<span class="badge badge-warning"><%= t('pro') %></span>
|
<span class="badge badge-warning"><%= t('pro') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
<% end %>
|
<% 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>
|
<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 %>
|
<%= 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: { turbo: false } do %>
|
||||||
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% if Docuseal.multitenant? %>
|
<% if Docuseal.multitenant? %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
|
||||||
<%= t('embedding') %>
|
<%= t('embedding') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -28,9 +28,16 @@
|
|||||||
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
|
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
|
||||||
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
|
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
|
||||||
</span>
|
</span>
|
||||||
<p class="text-sm leading-none text-base-content/60 pt-1.5">
|
<div class="flex items-center space-x-1 pt-1.5">
|
||||||
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
<span class="text-sm leading-none text-base-content/60">
|
||||||
</p>
|
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||||
|
</span>
|
||||||
|
<% if (device = DetectBrowserDevice.call(event.data['ua'])) %>
|
||||||
|
<span class="tooltip tooltip-top" data-tip="<%= t(device) %>">
|
||||||
|
<%= svg_icon("device_#{device}", class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<p class="text-base-content/80 mt-1">
|
<p class="text-base-content/80 mt-1">
|
||||||
<% if event.event_type == 'complete_verification' %>
|
<% if event.event_type == 'complete_verification' %>
|
||||||
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
|
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
|
||||||
<% submitters.each_with_index do |item, index| %>
|
<% submitters.each_with_index do |item, index| %>
|
||||||
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %>
|
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] }.presence %>
|
||||||
|
<% prefillable_fields ||= local_assigns[:recipient_form_fields].presence %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<% if submitters.size > 1 %>
|
<% if submitters.size > 1 %>
|
||||||
<label class="label pt-0 pb-1">
|
<label class="label pt-0 pb-1">
|
||||||
@@ -29,14 +30,14 @@
|
|||||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
|
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% has_phone_field = true %>
|
<% has_phone_field = true %>
|
||||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<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 %>
|
<%= 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: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</custom-validation>
|
</custom-validation>
|
||||||
@@ -59,17 +60,23 @@
|
|||||||
</custom-validation>
|
</custom-validation>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% prefillable_fields.each do |field| %>
|
<% prefillable_fields.each do |field| %>
|
||||||
|
<% field_id = "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}" %>
|
||||||
<% if field['type'] == 'checkbox' %>
|
<% if field['type'] == 'checkbox' %>
|
||||||
<label for="detailed_field_<%= field['uuid'] %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
|
<label for="<%= field_id %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
|
||||||
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
|
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
|
||||||
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid']}]", id: "detailed_field_#{field['uuid']}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
|
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", id: field_id, class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
|
||||||
</label>
|
</label>
|
||||||
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
|
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
|
||||||
<%= select_tag "submission[1][submitters][][values][#{field['uuid']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{field['uuid']}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
|
<%= select_tag "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: field_id, class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
|
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: field_id, required: field['required'] %>
|
||||||
<% elsif field['type'] != 'phone' %>
|
<% elsif field['type'] != 'phone' %>
|
||||||
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
|
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: field_id, required: field['required'] %>
|
||||||
|
<% end %>
|
||||||
|
<% field['conditions']&.each do |condition| %>
|
||||||
|
<% if (condition_field = prefillable_fields.find { |f| f['uuid'] == condition['field_uuid'] || f['name'] == condition['field_name'] }) %>
|
||||||
|
<field-condition data-target-id="<%= field_id %>" data-field-id="<%= "detailed_field_#{index}_#{condition['field_uuid'] || condition['field_name'].parameterize}" %>" data-action="<%= condition['action'] %>" data-value="<%= condition_field['options'].present? ? condition_field['options'].find { |o| o['uuid'] == condition['value'] }&.dig('value') || condition['value'] : condition['value'] %>" data-operation="<%= condition['operation'] %>"></field-condition>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -79,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span><%= t('add_new') %></span>
|
<span><%= t('add_new') %></span>
|
||||||
|
|||||||
@@ -1,116 +1 @@
|
|||||||
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
|
<%= render partial: 'submissions/send_email_base', locals: local_assigns %>
|
||||||
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
|
|
||||||
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
|
|
||||||
<div class="form-control">
|
|
||||||
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
|
||||||
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
|
|
||||||
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
|
|
||||||
<% end %>
|
|
||||||
<div>
|
|
||||||
<% if can_send_emails %>
|
|
||||||
<%= render 'submissions/email_stats' %>
|
|
||||||
<%= content_for(:edit_button) || capture do %>
|
|
||||||
<toggle-visible data-element-ids="<%= [message_field_id].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>
|
|
||||||
</div>
|
|
||||||
<% if !Docuseal.multitenant? && !can_send_emails %>
|
|
||||||
<div class="alert my-4">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
|
||||||
<div>
|
|
||||||
<p class="font-bold"><%= t('smtp_not_configured') %></p>
|
|
||||||
<p class="text-gray-700">
|
|
||||||
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
|
|
||||||
<br>
|
|
||||||
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
|
|
||||||
<%= t('go_to_smtp_settings') %>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
|
||||||
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
|
|
||||||
<div class="card-body">
|
|
||||||
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
|
||||||
<div class="peer-checked:hidden form-control space-y-2">
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<%= f.label :subject, t('subject'), class: 'label' %>
|
|
||||||
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
|
||||||
<label for="<%= toggle_uuid %>" class="label underline">
|
|
||||||
<%= t('edit_per_party') %>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= f.label :message, t('body'), class: 'label' %>
|
|
||||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<autoresize-textarea>
|
|
||||||
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
|
||||||
</autoresize-textarea>
|
|
||||||
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
|
||||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
|
||||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
|
||||||
<span class="label"><%= t('save_as_default_template_message') %></span>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<%= render 'submissions/message_fields' %>
|
|
||||||
</div>
|
|
||||||
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
|
||||||
<div class="hidden peer-checked:block form-control space-y-2">
|
|
||||||
<% uuid = SecureRandom.uuid %>
|
|
||||||
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
|
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
|
||||||
<ul class="tabs w-full flex flex-nowrap">
|
|
||||||
<% options.each_with_index do |(label, val), index| %>
|
|
||||||
<div class="w-full">
|
|
||||||
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
|
||||||
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</toggle-visible>
|
|
||||||
<% template_submitters.each_with_index do |submitter, index| %>
|
|
||||||
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
|
|
||||||
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
|
||||||
</div>
|
|
||||||
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= ff.label :message, t('body'), class: 'label' %>
|
|
||||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<autoresize-textarea>
|
|
||||||
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
|
||||||
</autoresize-textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
|
||||||
|
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
|
||||||
|
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
|
||||||
|
<div class="form-control">
|
||||||
|
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
|
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
|
||||||
|
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<% if can_send_emails %>
|
||||||
|
<%= render 'submissions/email_stats' %>
|
||||||
|
<%= content_for(:edit_button) || capture do %>
|
||||||
|
<toggle-visible data-element-ids="<%= [message_field_id].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>
|
||||||
|
</div>
|
||||||
|
<% if !Docuseal.multitenant? && !can_send_emails %>
|
||||||
|
<div class="alert my-4">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold"><%= t('smtp_not_configured') %></p>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
|
||||||
|
<%= t('go_to_smtp_settings') %>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||||
|
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
|
||||||
|
<div class="card-body">
|
||||||
|
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
||||||
|
<div class="peer-checked:hidden form-control space-y-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<%= f.label :subject, t('subject'), class: 'label' %>
|
||||||
|
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
||||||
|
<label for="<%= toggle_uuid %>" class="label underline">
|
||||||
|
<%= t('edit_per_party') %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= f.label :message, t('body'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
||||||
|
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||||
|
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||||
|
<span class="label"><%= t('save_as_default_template_message') %></span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= render 'submissions/message_fields' %>
|
||||||
|
</div>
|
||||||
|
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
||||||
|
<div class="hidden peer-checked:block form-control space-y-2">
|
||||||
|
<% uuid = SecureRandom.uuid %>
|
||||||
|
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
|
||||||
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
||||||
|
<ul class="tabs w-full flex flex-nowrap">
|
||||||
|
<% options.each_with_index do |(label, val), index| %>
|
||||||
|
<div class="w-full">
|
||||||
|
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
||||||
|
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</toggle-visible>
|
||||||
|
<% template_submitters.each_with_index do |submitter, index| %>
|
||||||
|
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
|
||||||
|
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
|
</div>
|
||||||
|
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :message, t('body'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
||||||
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
|
<% is_narrow = area['h'].positive? && ((area['w'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
|
||||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||||
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
||||||
@@ -50,12 +50,13 @@
|
|||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
||||||
<% option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] } %>
|
<% if (option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] }) %>
|
||||||
<% option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}" %>
|
<% option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}" %>
|
||||||
<% if option && Array.wrap(value).include?(option_name) %>
|
<% if Array.wrap(value).include?(option_name) %>
|
||||||
<div class="w-full flex items-center justify-center">
|
<div class="w-full flex items-center justify-center">
|
||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||||
|
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
||||||
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
||||||
<% only_detailed = require_phone_2fa || prefillable_fields.present? %>
|
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||||
|
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
||||||
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
||||||
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
|
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
|
||||||
<%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
|
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
|
||||||
</div>
|
</div>
|
||||||
<div id="list" class="hidden">
|
<div id="list" class="hidden">
|
||||||
<%= render 'list_form', template: @template %>
|
<%= render 'list_form', template: @template %>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
|
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
|
||||||
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %>
|
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %>
|
||||||
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
|
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
|
||||||
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true %>
|
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %>
|
||||||
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
||||||
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
||||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
<div style="max-width: 1600px" class="mx-auto pl-4">
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
<% if document.preview_images.first %>
|
<% if document.preview_images.first %>
|
||||||
<scroll-to data-selector-id="page-<%= document.uuid %>-0" 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">
|
<img src="<%= (document.preview_images.find { |e| e.filename.base.to_i.zero? } || 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">
|
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||||
<%= item['name'].presence || document.filename.base %>
|
<%= item['name'].presence || document.filename.base %>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,8 +93,10 @@
|
|||||||
<div class="pr-3.5 pl-0.5">
|
<div class="pr-3.5 pl-0.5">
|
||||||
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
||||||
<% submitters_index = @submission.submitters.index_by(&:uuid) %>
|
<% submitters_index = @submission.submitters.index_by(&:uuid) %>
|
||||||
|
<% submitters_order_index = nil %>
|
||||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
||||||
|
<% bg_classes = %w[bg-red-100 bg-sky-100 bg-emerald-100 bg-yellow-100 bg-purple-100 bg-pink-100 bg-cyan-100 bg-orange-100 bg-lime-100 bg-indigo-100] %>
|
||||||
<% schema.each do |item| %>
|
<% schema.each do |item| %>
|
||||||
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
@@ -103,7 +105,7 @@
|
|||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= width %>" class="rounded" height="<%= height %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
@@ -111,19 +113,29 @@
|
|||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% 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 = 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' %>
|
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
||||||
<% next if value.blank? %>
|
|
||||||
<% submitter = submitters_index[field['submitter_uuid']] %>
|
<% submitter = submitters_index[field['submitter_uuid']] %>
|
||||||
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
|
<% if value.present? %>
|
||||||
<span class="group">
|
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
|
||||||
<span class="hidden group-hover:inline">
|
<span class="group">
|
||||||
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
|
<span class="hidden group-hover:inline">
|
||||||
|
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
|
||||||
|
</span>
|
||||||
|
<span class="group-hover:hidden">
|
||||||
|
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="group-hover:hidden">
|
<% else %>
|
||||||
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
|
<%= 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: %>
|
||||||
</span>
|
<% end %>
|
||||||
</span>
|
<% elsif field['readonly'] != true && submitter && !submitter.completed_at? %>
|
||||||
<% else %>
|
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
||||||
<%= 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: %>
|
<% submitter_index = submitters_order_index[submitter.uuid] %>
|
||||||
|
<% bg_class = bg_classes[submitter_index % bg_classes.size] %>
|
||||||
|
<div class="absolute overflow-visible" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%;">
|
||||||
|
<div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>">
|
||||||
|
<%= svg_icon(SubmissionsController::FIELD_ICONS[field['type']], class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
|
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
|
||||||
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
||||||
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-field-labels="<%= configs[:with_field_labels] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
|
||||||
|
<% I18n.with_locale(@submitter.account.locale) do %>
|
||||||
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
|
||||||
|
<% end %>
|
||||||
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
|
<div class="space-y-6 mx-auto">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<%= render 'start_form/banner' %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
|
||||||
|
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
|
||||||
|
<% if last_submitter %>
|
||||||
|
<p dir="auto" class="text-sm">
|
||||||
|
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if @submitter.email.present? %>
|
||||||
|
<div>
|
||||||
|
<%= t('the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html', email: TextUtils.mask_email(@submitter.email, 2)) %>
|
||||||
|
</div>
|
||||||
|
<% if params[:status] == 'sent' || params[:status] == 'error' %>
|
||||||
|
<%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||||
|
<div dir="auto" class="form-control !mt-0">
|
||||||
|
<%= f.hidden_field :submitter_slug, value: @submitter.slug %>
|
||||||
|
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
|
||||||
|
<div class="flex justify-between items-center mt-1">
|
||||||
|
<span>
|
||||||
|
<% if flash[:alert] %>
|
||||||
|
<span class="text-red-500">
|
||||||
|
<%= flash[:alert] %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<%= flash[:notice] %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_email) %></label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<toggle-submit dir="auto" class="form-control">
|
||||||
|
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
||||||
|
</toggle-submit>
|
||||||
|
<% end %>
|
||||||
|
<%= button_to t(:re_send_email), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true }, method: :put, id: 'resend_code', class: 'hidden' %>
|
||||||
|
<% else %>
|
||||||
|
<% if params[:t] %>
|
||||||
|
<fetch-form data-onload="true">
|
||||||
|
<%= button_to '', api_submitter_email_clicks_path, params: { submitter_slug: @submitter.slug, t: params[:t] }, method: :post, class: 'hidden' %>
|
||||||
|
</fetch-form>
|
||||||
|
<% end %>
|
||||||
|
<toggle-submit dir="auto" class="form-control">
|
||||||
|
<%= button_to button_title(title: t('send_verification_code')), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug }, method: :put, class: 'base-button w-full' %>
|
||||||
|
</toggle-submit>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<p><%= t('your_verification_code_to_access_the_name', name: @submitter.submission.name || @submitter.submission.template.name) %></p>
|
||||||
|
<p><b><%= @otp_code %></b></p>
|
||||||
|
<p><%= t('please_reply_to_this_email_if_you_didnt_request_this') %></p>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<div id="<%= AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY %>_form">
|
||||||
|
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
|
||||||
|
<% template_email_preferences_values = @template.preferences.values_at('completed_notification_email_subject', 'completed_notification_email_body').compact_blank.presence %>
|
||||||
|
<% is_custom_template_email = template_email_preferences_values.present? %>
|
||||||
|
<% if is_custom_template_email %>
|
||||||
|
<%= button_to nil, template_preferences_path(@template), id: 'submitter_completed_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_completed_email_template_form' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body']) do |ff| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<% if is_custom_template_email %>
|
||||||
|
<label for="submitter_completed_email_reset_link" class="label underline">
|
||||||
|
<%= t('reset_default') %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
|
||||||
|
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('attach_documents_to_the_email') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('attach_audit_log_pdf_to_the_email') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('send_emails_automatically_on_completion') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_completed_email_template_form', class: 'base-button' %>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<span id="email_saved_alert3" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div id="<%= AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY %>_form">
|
||||||
|
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
|
||||||
|
<% template_email_preferences_values = @template.preferences.values_at('documents_copy_email_subject', 'documents_copy_email_body').compact_blank.presence %>
|
||||||
|
<% is_custom_template_email = template_email_preferences_values.present? %>
|
||||||
|
<% if is_custom_template_email %>
|
||||||
|
<%= button_to nil, template_preferences_path(@template), id: 'submitter_documents_copy_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_documents_copy_email_template_form' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body']) do |ff| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<% if is_custom_template_email %>
|
||||||
|
<label for="submitter_documents_copy_email_reset_link" class="label underline">
|
||||||
|
<%= t('reset_default') %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
</div>
|
||||||
|
<% if can?(:manage, :reply_to) %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :documents_copy_email_reply_to, t('reply_to'), class: 'label' %>
|
||||||
|
<%= ff.email_field :documents_copy_email_reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_documents_copy_email_template_form' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
|
||||||
|
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('attach_documents_to_the_email') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('attach_audit_log_pdf_to_the_email') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :documents_copy_email_attach_audit, { checked: ff.object.documents_copy_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('send_emails_automatically_on_completion') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_documents_copy_email_template_form', class: 'base-button' %>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<span id="email_saved_alert2" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<div id="<%= AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY %>_form">
|
||||||
|
<% template_email_preferences_values = @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank %>
|
||||||
|
<% default_template_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %>
|
||||||
|
<% is_custom_template_email = template_email_preferences_values.present? %>
|
||||||
|
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %>
|
||||||
|
<% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %>
|
||||||
|
<%= button_to nil, template_preferences_path(@template), id: 'submitter_invitation_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_invitation_email_template_form' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
|
||||||
|
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
|
||||||
|
<div class="peer-checked:hidden">
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(template_email_preferences_values.presence || default_template_email_preferences_values)) do |ff| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<% if is_custom_template_email %>
|
||||||
|
<label for="submitter_invitation_email_reset_link" class="label underline">
|
||||||
|
<%= t('reset_default') %>
|
||||||
|
</label>
|
||||||
|
<% elsif multiple_submitters %>
|
||||||
|
<label for="request_email_per_submitter" class="label underline">
|
||||||
|
<%= t('edit_per_party') %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% if multiple_submitters %>
|
||||||
|
<div class="hidden peer-checked:block">
|
||||||
|
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
|
||||||
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
||||||
|
<ul class="tabs w-full flex flex-nowrap mb-2">
|
||||||
|
<% options.each_with_index do |(label, val), index| %>
|
||||||
|
<div class="w-full">
|
||||||
|
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
||||||
|
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</toggle-visible>
|
||||||
|
<%= f.fields_for :preferences do |ff| %>
|
||||||
|
<% @template.submitters.each_with_index do |submitter, index| %>
|
||||||
|
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||||
|
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
|
||||||
|
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
|
||||||
|
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || template_email_preferences_values.presence || default_template_email_preferences_values)), index: nil do |fff| %>
|
||||||
|
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||||
|
<% if submitter_email_preferences_values.present? %>
|
||||||
|
<label for="submitter_invitation_email_reset_link" class="label underline">
|
||||||
|
<%= t('reset_default') %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||||
|
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
|
||||||
|
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('send_signature_request_email') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_invitation_email_template_form', class: 'base-button' %>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<span id="email_saved_alert1" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||||
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
|
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
|
||||||
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
|
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
|
||||||
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true) do |ff| %>
|
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa, :require_email_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true, @template.preferences['require_email_2fa'] == true) do |ff| %>
|
||||||
<div class="form-control mb-2">
|
<div class="form-control mb-2">
|
||||||
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %>
|
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %>
|
||||||
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
|
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
|
||||||
@@ -81,6 +81,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'templates_preferences/form_fields', ff: %>
|
<%= render 'templates_preferences/form_fields', ff: %>
|
||||||
|
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
||||||
|
<span>
|
||||||
|
<%= t('require_email_2fa_to_open') %>
|
||||||
|
</span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :require_email_2fa, { checked: ff.object.require_email_2fa == true, class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control pt-2">
|
<div class="form-control pt-2">
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
||||||
@@ -98,157 +106,17 @@
|
|||||||
<%= t('signature_request_email') %>
|
<%= t('signature_request_email') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
<%= render 'templates_preferences/submitter_invitation_email_form' %>
|
||||||
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
|
|
||||||
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
|
|
||||||
<div class="peer-checked:hidden">
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
|
||||||
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
|
|
||||||
<label for="request_email_per_submitter" class="label underline">
|
|
||||||
<%= t('edit_per_party') %>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
|
|
||||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<autoresize-textarea>
|
|
||||||
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
|
||||||
</autoresize-textarea>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
|
|
||||||
<div class="hidden peer-checked:block">
|
|
||||||
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
|
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
|
||||||
<ul class="tabs w-full flex flex-nowrap mb-2">
|
|
||||||
<% options.each_with_index do |(label, val), index| %>
|
|
||||||
<div class="w-full">
|
|
||||||
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
|
||||||
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</toggle-visible>
|
|
||||||
<%= f.fields_for :preferences do |ff| %>
|
|
||||||
<% @template.submitters.each_with_index do |submitter, index| %>
|
|
||||||
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
|
||||||
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
|
|
||||||
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))), index: nil do |fff| %>
|
|
||||||
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
|
||||||
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
|
|
||||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<autoresize-textarea>
|
|
||||||
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
|
||||||
</autoresize-textarea>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
|
|
||||||
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= 'Send signature request email' %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle' }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control pt-2">
|
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<span id="email_saved_alert1" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
|
||||||
<div class="collapse collapse-arrow join-item border border-base-300">
|
<div class="collapse collapse-arrow join-item border border-base-300">
|
||||||
<input type="checkbox" name="accordion">
|
<input type="checkbox" name="accordion">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
<%= t('documents_copy_email') %>
|
<%= t('documents_copy_email') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
<%= render 'templates_preferences/submitter_documents_copy_email_form' %>
|
||||||
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
|
|
||||||
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
|
|
||||||
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
|
|
||||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<autoresize-textarea>
|
|
||||||
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
|
||||||
</autoresize-textarea>
|
|
||||||
</div>
|
|
||||||
<% if can?(:manage, :reply_to) %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.label :documents_copy_email_reply_to, t('reply_to'), class: 'label' %>
|
|
||||||
<%= ff.email_field :documents_copy_email_reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= t('attach_documents_to_the_email') %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= t('attach_audit_log_pdf_to_the_email') %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :documents_copy_email_attach_audit, { checked: ff.object.documents_copy_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= t('send_emails_automatically_on_completion') %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control pt-2">
|
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<span id="email_saved_alert2" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse collapse-arrow join-item border border-base-300">
|
<div class="collapse collapse-arrow join-item border border-base-300">
|
||||||
@@ -257,57 +125,7 @@
|
|||||||
<%= t('completed_notification_email') %>
|
<%= t('completed_notification_email') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
<%= render 'templates_preferences/submitter_completed_email_form' %>
|
||||||
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
|
|
||||||
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
|
|
||||||
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
|
|
||||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<autoresize-textarea>
|
|
||||||
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
|
||||||
</autoresize-textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= t('attach_documents_to_the_email') %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= t('attach_audit_log_pdf_to_the_email') %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
|
|
||||||
<span>
|
|
||||||
<%= t('send_emails_automatically_on_completion') %>
|
|
||||||
</span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control pt-2">
|
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<span id="email_saved_alert3" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,9 +177,9 @@
|
|||||||
<span>
|
<span>
|
||||||
<%= t('share_template_with_test_mode') %>
|
<%= t('share_template_with_test_mode') %>
|
||||||
</span>
|
</span>
|
||||||
<submit-form data-on="change" class="flex">
|
<submit-form data-on="change" class="flex">
|
||||||
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
|
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
|
||||||
</submit-form>
|
</submit-form>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|||||||
@@ -3,12 +3,19 @@
|
|||||||
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
|
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
|
||||||
<div class="mt-2 mb-4 px-5">
|
<div class="mt-2 mb-4 px-5">
|
||||||
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
|
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
|
||||||
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
|
<% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %>
|
||||||
<span><%= t('enable_shared_link') %></span>
|
<label for="template_shared_link" class="tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
|
||||||
<submit-form data-on="change" class="flex">
|
<span><%= t('enable_shared_link') %></span>
|
||||||
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
|
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
|
||||||
</submit-form>
|
</label>
|
||||||
</label>
|
<% else %>
|
||||||
|
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
|
||||||
|
<span><%= t('enable_shared_link') %></span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
<div class="flex gap-2 mt-3">
|
<div class="flex gap-2 mt-3">
|
||||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
||||||
<check-on-click data-element-id="template_shared_link">
|
<check-on-click data-element-id="template_shared_link">
|
||||||
@@ -31,48 +38,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="collapse collapse-arrow join-item mt-4">
|
<% if @template.preferences&.dig('require_email_2fa') != true && @template.preferences&.dig('require_phone_2fa') != true %>
|
||||||
<input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
|
<div class="collapse collapse-arrow join-item mt-4">
|
||||||
<div class="collapse-title min-h-0 after:!right-3 p-1">
|
<input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
|
||||||
<%= t('advanced_settings') %>
|
<div class="collapse-title min-h-0 after:!right-3 p-1">
|
||||||
</div>
|
<%= t('advanced_settings') %>
|
||||||
<div class="collapse-content !p-0">
|
</div>
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
|
<div class="collapse-content !p-0">
|
||||||
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
|
||||||
<label class="pl-1"><%= t('link_form_fields') %></label>
|
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
|
||||||
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
|
<label class="pl-1"><%= t('link_form_fields') %></label>
|
||||||
<% %w[name email phone].each do |field| %>
|
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
|
||||||
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
|
<% %w[name email phone].each do |field| %>
|
||||||
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
|
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
|
||||||
<span class="font-medium w-full text-center"><%= t(field) %></span>
|
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
|
||||||
|
<span class="font-medium w-full text-center"><%= t(field) %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</required-checkbox-group>
|
||||||
|
<% end %>
|
||||||
|
<% if multiple_submitters && enough_defined_submitters %>
|
||||||
|
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
|
||||||
|
<input type="checkbox" name="accordion" class="min-h-0">
|
||||||
|
<div class="collapse-title py-4 min-h-0">
|
||||||
|
<%= t('default_parties') %>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content !pb-0">
|
||||||
|
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
|
||||||
|
<label for="template_preferences_shared_link_2fa" class="flex items-center mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
|
||||||
|
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</submit-form>
|
||||||
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</required-checkbox-group>
|
|
||||||
<% end %>
|
|
||||||
<% if multiple_submitters && enough_defined_submitters %>
|
|
||||||
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
|
|
||||||
<input type="checkbox" name="accordion" class="min-h-0">
|
|
||||||
<div class="collapse-title py-4 min-h-0">
|
|
||||||
<%= t('default_parties') %>
|
|
||||||
</div>
|
|
||||||
<div class="collapse-content !pb-0">
|
|
||||||
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
|
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4' }, data: { close_on_submit: false } do |f| %>
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
|
|
||||||
<label for="template_preferences_shared_link_2fa" class="flex items-center mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
|
|
||||||
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
|
|
||||||
<submit-form data-on="change" class="flex">
|
|
||||||
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
|
|
||||||
</submit-form>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -13,6 +13,11 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :email, t('email'), class: 'label' %>
|
<%= f.label :email, t('email'), class: 'label' %>
|
||||||
<%= f.email_field :email, required: true, class: 'base-input' %>
|
<%= f.email_field :email, required: true, class: 'base-input' %>
|
||||||
|
<% if user.try(:pending_reconfirmation?) %>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt"><%= t('email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm', email: f.object.unconfirmed_email) %></span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
<% if user.persisted? && Accounts.can_send_emails?(current_account) %>
|
<% if user.persisted? && Accounts.can_send_emails?(current_account) %>
|
||||||
<span class="label-text-alt mt-2 mx-1">
|
<span class="label-text-alt mt-2 mx-1">
|
||||||
<%= t('click_here_to_send_a_reset_password_email_html') %>
|
<%= t('click_here_to_send_a_reset_password_email_html') %>
|
||||||
|
|||||||
@@ -12,15 +12,24 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
|
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
|
||||||
<% if params[:status].blank? && can?(:create, User.new(account: current_account)) %>
|
<% if params[:status].blank? %>
|
||||||
<%= render 'users/extra_buttons' %>
|
<% if can?(:create, User.new(account: current_account)) %>
|
||||||
<% if content_for(:add_user_button) %>
|
<%= render 'users/extra_buttons' %>
|
||||||
<%= content_for(:add_user_button) %>
|
<% if content_for(:add_user_button) %>
|
||||||
<% else %>
|
<%= content_for(:add_user_button) %>
|
||||||
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
<% else %>
|
||||||
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
||||||
<span><%= t('new_user') %></span>
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span><%= t('new_user') %></span>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<div class="tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
|
||||||
|
<%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span><%= t('new_user') %></span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +61,13 @@
|
|||||||
<%= user.full_name %>
|
<%= user.full_name %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<%= user.email %>
|
<% if user.try(:pending_reconfirmation?) %>
|
||||||
|
<%= user.unconfirmed_email %>
|
||||||
|
<br>
|
||||||
|
<span class="label-text-alt">(<%= t('unconfirmed') %>)</span>
|
||||||
|
<% else %>
|
||||||
|
<%= user.email %>
|
||||||
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-info badge-outline whitespace-nowrap">
|
<span class="badge badge-info badge-outline whitespace-nowrap">
|
||||||
@@ -73,7 +88,7 @@
|
|||||||
<%= t('remove') %>
|
<%= t('remove') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if params[:status] == 'archived' && can?(:manage, user) && user != current_user && user.archived_at? %>
|
<% if params[:status] == 'archived' && can?(:create, user) && user != current_user && user.archived_at? %>
|
||||||
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||||
<%= t('unarchive') %>
|
<%= t('unarchive') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ignored_warnings": [
|
"ignored_warnings": [
|
||||||
{
|
{
|
||||||
"fingerprint": "bbd1bdad94998e53a48921859065e06cd1595502d4dc40362afcaa90307b591a",
|
"fingerprint": "de39648cd3f79d51e95e45f05fc77da49b0ad68bf80458061151016c0ef78208",
|
||||||
"note": "Permitted parameters are necessary for creating submitters via API"
|
"note": "Permitted parameters are necessary for creating submitters via API"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:active_job) do
|
||||||
|
ActiveJob::LogSubscriber.class_eval do
|
||||||
|
def args_info(_job)
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,10 @@ module Devise
|
|||||||
assign_message_metadata(action, record)
|
assign_message_metadata(action, record)
|
||||||
|
|
||||||
initialize_from_record(record)
|
initialize_from_record(record)
|
||||||
mail(headers_for(action, opts), &)
|
|
||||||
|
I18n.with_locale(record.account.locale) do
|
||||||
|
mail(headers_for(action, opts), &)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -31,6 +34,7 @@ end
|
|||||||
#
|
#
|
||||||
# Use this hook to configure devise mailer, warden hooks and so forth.
|
# Use this hook to configure devise mailer, warden hooks and so forth.
|
||||||
# Many of these configuration options can be set straight in your model.
|
# Many of these configuration options can be set straight in your model.
|
||||||
|
# rubocop:disable Metrics/BlockLength
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
|
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
|
||||||
@@ -166,7 +170,7 @@ Devise.setup do |config|
|
|||||||
# without confirming their account.
|
# without confirming their account.
|
||||||
# Default is 0.days, meaning the user cannot access the website without
|
# Default is 0.days, meaning the user cannot access the website without
|
||||||
# confirming their account.
|
# confirming their account.
|
||||||
# config.allow_unconfirmed_access_for = 2.days
|
config.allow_unconfirmed_access_for = nil
|
||||||
|
|
||||||
# A period that the user is allowed to confirm their account before their
|
# A period that the user is allowed to confirm their account before their
|
||||||
# token becomes invalid. For example, if set to 3.days, the user can confirm
|
# token becomes invalid. For example, if set to 3.days, the user can confirm
|
||||||
@@ -332,3 +336,4 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
ActiveSupport.run_load_hooks(:devise_config, config)
|
ActiveSupport.run_load_hooks(:devise_config, config)
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/BlockLength
|
||||||