mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 22:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1123fef63 | ||
|
|
4dc6149530 | ||
|
|
1245ff2cce | ||
|
|
284204fd78 | ||
|
|
f7be74eb73 | ||
|
|
aeea619059 | ||
|
|
ea2b7f20e7 | ||
|
|
f40f1d25de | ||
|
|
c04bb2d7cf | ||
|
|
4c1ccd65bf | ||
|
|
54e064b0fc | ||
|
|
9a8b72883c | ||
|
|
5d75ee2e47 | ||
|
|
7357fa4871 | ||
|
|
f6850b5427 | ||
|
|
1e9a181e60 | ||
|
|
7224a1ccec | ||
|
|
9b935a8180 | ||
|
|
c4a693846e | ||
|
|
02c7cdcd97 | ||
|
|
36bee92e8e | ||
|
|
6542804f44 | ||
|
|
d805fd614c | ||
|
|
21299dc65f | ||
|
|
0df0946c2a | ||
|
|
1bb38d50a1 | ||
|
|
242987cb41 | ||
|
|
5456abde54 | ||
|
|
4a07427440 | ||
|
|
cf60dae2ff | ||
|
|
659997c6c1 | ||
|
|
389a47a152 | ||
|
|
b6b81ca487 | ||
|
|
719c1786f1 | ||
|
|
4685bac4b3 | ||
|
|
a05275cc08 | ||
|
|
1abf1b1658 | ||
|
|
8893dc5698 | ||
|
|
f8467b2da1 | ||
|
|
f39b8b000e | ||
|
|
961036fa31 | ||
|
|
8b9056894e | ||
|
|
7f979e9396 | ||
|
|
8db1192d0e | ||
|
|
3bd515951a | ||
|
|
2bd5b9676b |
No files matched your search
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
|
|||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
gem 'csv'
|
gem 'csv'
|
||||||
|
gem 'csv-safe'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
|
|||||||
+9
-4
@@ -150,6 +150,8 @@ GEM
|
|||||||
css_parser (1.21.0)
|
css_parser (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.2)
|
csv (3.3.2)
|
||||||
|
csv-safe (3.3.1)
|
||||||
|
csv (~> 3.0)
|
||||||
cuprite (0.15.1)
|
cuprite (0.15.1)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.15.0)
|
ferrum (~> 0.15.0)
|
||||||
@@ -271,10 +273,11 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.2)
|
hashdiff (1.1.2)
|
||||||
hexapdf (1.0.3)
|
hexapdf (1.4.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
|
strscan (>= 3.1.2)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
@@ -288,7 +291,7 @@ GEM
|
|||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.9.1)
|
json (2.13.2)
|
||||||
jwt (2.9.3)
|
jwt (2.9.3)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
@@ -353,13 +356,13 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-musl)
|
nokogiri (1.18.9-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.8)
|
oj (3.16.11)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.3)
|
||||||
package_json (0.1.0)
|
package_json (0.1.0)
|
||||||
pagy (9.3.3)
|
pagy (9.3.3)
|
||||||
parallel (1.26.3)
|
parallel (1.26.3)
|
||||||
@@ -548,6 +551,7 @@ GEM
|
|||||||
stringio (3.1.7)
|
stringio (3.1.7)
|
||||||
strip_attributes (1.14.1)
|
strip_attributes (1.14.1)
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
|
strscan (3.1.5)
|
||||||
thor (1.4.0)
|
thor (1.4.0)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
@@ -610,6 +614,7 @@ DEPENDENCIES
|
|||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
csv
|
csv
|
||||||
|
csv-safe
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Api
|
|||||||
def show
|
def show
|
||||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
if blob_uuid.blank? || purp != 'blob'
|
||||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :not_found
|
return head :not_found
|
||||||
@@ -24,8 +24,9 @@ module Api
|
|||||||
attachment = blob.attachments.take
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
@record = attachment.record
|
@record = attachment.record
|
||||||
|
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||||
|
|
||||||
authorization_check!(attachment) if exp.blank?
|
authorization_check!(attachment, @record, exp)
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -41,16 +42,22 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(attachment)
|
def authorization_check!(attachment, record, exp)
|
||||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
return if attachment.name == 'logo'
|
||||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
return if exp.to_i >= Time.current.to_i
|
||||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
return if current_user && current_ability.can?(:read, record)
|
||||||
!attachment.record.account.account_configs
|
|
||||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
|
||||||
|
|
||||||
return if is_authorized
|
if exp.blank?
|
||||||
|
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||||
|
|
||||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||||
|
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||||
|
|
||||||
|
return if !require_ttl && !require_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||||
|
|
||||||
raise CanCan::AccessDenied
|
raise CanCan::AccessDenied
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ module Api
|
|||||||
field: :completed_at
|
field: :completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map do |s|
|
data: submitters.map do |s|
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: s.completed_at,
|
timestamp: s.completed_at,
|
||||||
data: Submitters::SerializeForWebhook.call(s)
|
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ module Api
|
|||||||
associations: [:blob]
|
associations: [:blob]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
id: @submission.id,
|
id: @submission.id,
|
||||||
documents: documents.map do |attachment|
|
documents: documents.map do |attachment|
|
||||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,16 +14,19 @@ module Api
|
|||||||
:created_by_user, :submission_events,
|
:created_by_user, :submission_events,
|
||||||
template: :folder,
|
template: :folder,
|
||||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||||
audit_trail_attachment: :blob
|
audit_trail_attachment: :blob,
|
||||||
|
combined_document_attachment: :blob
|
||||||
),
|
),
|
||||||
field: :completed_at)
|
field: :completed_at)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.map do |s|
|
data: submissions.map do |s|
|
||||||
{
|
{
|
||||||
event_type: 'submission.completed',
|
event_type: 'submission.completed',
|
||||||
timestamp: s.completed_at,
|
timestamp: s.completed_at,
|
||||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ module Api
|
|||||||
combined_document_attachment: :blob,
|
combined_document_attachment: :blob,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.map do |s|
|
data: submissions.map do |s|
|
||||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||||
with_events: false, with_documents: false, with_values: false)
|
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submissions.size,
|
count: submissions.size,
|
||||||
@@ -189,7 +191,7 @@ module Api
|
|||||||
{ 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,
|
||||||
{ default_value: [], value: [], preferences: {} }] }]]
|
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ module Api
|
|||||||
documents_attachments: :blob, attachments_attachments: :blob)
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map do |s|
|
data: submitters.map do |s|
|
||||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submitters.size,
|
count: submitters.size,
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ module Api
|
|||||||
original_template: @template,
|
original_template: @template,
|
||||||
documents: params[:documents])
|
documents: params[:documents])
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(cloned_template)
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -24,13 +24,14 @@ module Api
|
|||||||
name: :preview_images)
|
name: :preview_images)
|
||||||
.preload(:blob)
|
.preload(:blob)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: templates.map do |t|
|
data: templates.map do |t|
|
||||||
Templates::SerializeForApi.call(
|
Templates::SerializeForApi.call(t,
|
||||||
t,
|
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||||
schema_documents.select { |e| e.record_id == t.id },
|
preview_image_attachments:,
|
||||||
preview_image_attachments
|
expires_at:)
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: templates.size,
|
count: templates.size,
|
||||||
@@ -111,9 +112,10 @@ module Api
|
|||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description, :prefillable,
|
:title, :description, :prefillable,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PasswordsController < Devise::PasswordsController
|
class PasswordsController < Devise::PasswordsController
|
||||||
|
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||||
|
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||||
|
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||||
|
|
||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
Current.user = resource
|
Current.user = resource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_resetting_password_path_for(_)
|
||||||
|
new_session_path(resource_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,7 @@ class ProfileController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_password
|
def update_password
|
||||||
if current_user.update(password_params)
|
if current_user.update_with_password(password_params)
|
||||||
bypass_sign_in(current_user)
|
bypass_sign_in(current_user)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||||
else
|
else
|
||||||
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def password_params
|
def password_params
|
||||||
params.require(:user).permit(:password, :password_confirmation)
|
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RevealAccessTokenController < ApplicationController
|
||||||
|
def show
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
if current_user.valid_password?(params[:password])
|
||||||
|
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||||
|
partial: 'reveal_access_token/access_token',
|
||||||
|
locals: { token: current_user.access_token.token })
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||||
|
locals: { error_message: I18n.t('wrong_password') }),
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
|||||||
attachments_attachments: :blob })
|
attachments_attachments: :blob })
|
||||||
.order(id: :asc)
|
.order(id: :asc)
|
||||||
|
|
||||||
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
if params[:format] == 'csv'
|
if params[:format] == 'csv'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||||
filename: "#{@template.name}.csv"
|
filename: "#{@template.name}.csv"
|
||||||
elsif params[:format] == 'xlsx'
|
elsif params[:format] == 'xlsx'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||||
filename: "#{@template.name}.xlsx"
|
filename: "#{@template.name}.xlsx"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
|
|
||||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
|
|||||||
@template.account = current_account
|
@template.account = current_account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
@@ -122,9 +124,10 @@ class TemplatesController < ApplicationController
|
|||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description, :prefillable,
|
:title, :description, :prefillable,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(template)
|
||||||
|
|
||||||
template.save!
|
template.save!
|
||||||
|
|
||||||
template
|
template
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class UsersController < ApplicationController
|
|||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@user.password = SecureRandom.hex if @user.password.blank?
|
||||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@@ -54,7 +55,7 @@ class UsersController < ApplicationController
|
|||||||
@user.account = account
|
@user.account = account
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||||
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
|
||||||
@@ -83,7 +84,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
if params.key?(:user)
|
if params.key?(:user)
|
||||||
permitted_params = %i[email first_name last_name password archived_at]
|
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
|
||||||
|
|
||||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UsersSendResetPasswordController < ApplicationController
|
||||||
|
load_and_authorize_resource :user
|
||||||
|
|
||||||
|
LIMIT_DURATION = 10.minutes
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize!(:manage, @user)
|
||||||
|
|
||||||
|
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
|
||||||
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
|
||||||
|
else
|
||||||
|
@user.send_reset_password_instructions
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_users_path,
|
||||||
|
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -29,8 +29,15 @@
|
|||||||
{{ optionValue(option) }}
|
{{ optionValue(option) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ field.title || field.name || fieldNames[field.type] }}
|
<MarkdownContent
|
||||||
<template v-if="field.type === 'checkbox' && !field.name">
|
v-if="field.title"
|
||||||
|
:text-only="true"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name || fieldNames[field.type] }}
|
||||||
|
</template>
|
||||||
|
<template v-if="field.type === 'checkbox' && !field.name && !field.title">
|
||||||
{{ fieldIndex + 1 }}
|
{{ fieldIndex + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
@@ -222,12 +229,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
components: {
|
components: {
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
|
MarkdownContent,
|
||||||
IconCheck
|
IconCheck
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
@@ -422,10 +431,14 @@ export default {
|
|||||||
},
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return this.formatDate(
|
try {
|
||||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
return this.formatDate(
|
||||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
)
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="sanitizeHref(completedButton.url)"
|
:href="sanitizeUrl(completedButton.url)"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full completed-form-completed-button"
|
class="white-button flex items-center w-full completed-form-completed-button"
|
||||||
>
|
>
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
@@ -198,6 +199,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
sanitizeUrl,
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
this.isSendingCopy = true
|
this.isSendingCopy = true
|
||||||
|
|
||||||
@@ -252,11 +254,6 @@ export default {
|
|||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sanitizeHref (href) {
|
|
||||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
|
||||||
return href.replace(/javascript:/g, '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
|
v-if="withToday"
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
@@ -46,6 +47,8 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
|
:min="validationMin"
|
||||||
|
:max="validationMax"
|
||||||
class="base-input !text-2xl text-center w-full"
|
class="base-input !text-2xl text-center w-full"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -89,6 +92,44 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['update:model-value', 'focus', 'submit'],
|
emits: ['update:model-value', 'focus', 'submit'],
|
||||||
computed: {
|
computed: {
|
||||||
|
dateNowString () {
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
const yyyy = today.getFullYear()
|
||||||
|
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(today.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${yyyy}-${mm}-${dd}`
|
||||||
|
},
|
||||||
|
validationMin () {
|
||||||
|
if (this.field.validation?.min) {
|
||||||
|
return ['{{date}}', '{date}'].includes(this.field.validation.min) ? this.dateNowString : this.field.validation.min
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationMax () {
|
||||||
|
if (this.field.validation?.max) {
|
||||||
|
return ['{{date}}', '{date}'].includes(this.field.validation.max) ? this.dateNowString : this.field.validation.max
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withToday () {
|
||||||
|
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
if (this.validationMin) {
|
||||||
|
if (new Date(this.validationMin).setHours(0, 0, 0, 0) <= todayDate) {
|
||||||
|
return this.validationMax ? (new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate) : true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (this.validationMax) {
|
||||||
|
return new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
|
|||||||
@@ -572,6 +572,7 @@ import FormCompleted from './completed'
|
|||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
|
|
||||||
const isEmpty = (obj) => {
|
const isEmpty = (obj) => {
|
||||||
if (obj == null) return true
|
if (obj == null) return true
|
||||||
@@ -1014,7 +1015,11 @@ export default {
|
|||||||
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
||||||
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
||||||
|
|
||||||
return sortArea(aArea, bArea)
|
if (aArea && bArea) {
|
||||||
|
return sortArea(aArea, bArea)
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1476,7 +1481,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.completedRedirectUrl) {
|
if (this.completedRedirectUrl) {
|
||||||
window.location.href = this.completedRedirectUrl
|
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
|
},
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.fields.reduce((acc, field) => {
|
||||||
|
acc[field.uuid] = field
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
@@ -96,8 +103,19 @@ export default {
|
|||||||
findPageElementForArea (area) {
|
findPageElementForArea (area) {
|
||||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
},
|
},
|
||||||
|
normalizeFormula (formula, depth = 0) {
|
||||||
|
if (depth > 10) return formula
|
||||||
|
|
||||||
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
if (this.fieldsUuidIndex[uuid]) {
|
||||||
|
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
|
} else {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
calculateFormula (field) {
|
calculateFormula (field) {
|
||||||
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<template
|
<span v-if="textOnly">
|
||||||
v-for="(item, index) in items"
|
{{ dom.body.textContent }}
|
||||||
:key="index"
|
</span>
|
||||||
>
|
<template v-else>
|
||||||
<a
|
|
||||||
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
|
||||||
:href="sanitizeHref(extractAttr(item, 'href'))"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
:class="extractAttr(item, 'class') || 'link'"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ extractText(item) }}
|
|
||||||
</a>
|
|
||||||
<b
|
|
||||||
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
|
||||||
>
|
|
||||||
{{ extractText(item) }}
|
|
||||||
</b>
|
|
||||||
<i
|
|
||||||
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
|
||||||
>
|
|
||||||
{{ extractText(item) }}
|
|
||||||
</i>
|
|
||||||
<br
|
|
||||||
v-else-if="item === '<br>' || item === '\n'"
|
|
||||||
>
|
|
||||||
<template
|
<template
|
||||||
v-else
|
v-for="(item, index) in nodes || dom.body.childNodes"
|
||||||
|
:key="index"
|
||||||
>
|
>
|
||||||
{{ item }}
|
<a
|
||||||
|
v-if="item.tagName === 'A' && item.getAttribute('href') !== 'undefined'"
|
||||||
|
:href="sanitizeUrl(item.getAttribute('href'))"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
:class="item.getAttribute('class') || 'link'"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<MarkdownContent :nodes="item.childNodes" />
|
||||||
|
</a>
|
||||||
|
<component
|
||||||
|
:is="item.tagName"
|
||||||
|
v-else-if="safeTags.includes(item.tagName)"
|
||||||
|
>
|
||||||
|
<MarkdownContent :nodes="item.childNodes" />
|
||||||
|
</component>
|
||||||
|
<br v-else-if="item.tagName === 'BR' || item.nodeValue === '\n'">
|
||||||
|
<template v-else>
|
||||||
|
{{ item.textContent }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -37,8 +34,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import snarkdown from 'snarkdown'
|
import snarkdown from 'snarkdown'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MarkdownContent',
|
name: 'MarkdownContent',
|
||||||
@@ -47,10 +43,23 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
nodes: {
|
||||||
|
type: [Array, Object],
|
||||||
|
require: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
textOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
items () {
|
safeTags () {
|
||||||
|
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
|
||||||
|
},
|
||||||
|
dom () {
|
||||||
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
||||||
|
|
||||||
const text = linkParts.map((part, index) => {
|
const text = linkParts.map((part, index) => {
|
||||||
@@ -61,25 +70,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sanitizeHref (href) {
|
sanitizeUrl
|
||||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
|
||||||
return href.replace(/javascript:/g, '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extractAttr (text, attr) {
|
|
||||||
if (text.includes(attr)) {
|
|
||||||
return text.split(attr).pop().split('"')[1]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extractText (text) {
|
|
||||||
if (text) {
|
|
||||||
return text.match(/>(.+?)</)?.[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -39,8 +39,10 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
v-model="number"
|
v-model="number"
|
||||||
type="number"
|
type="number"
|
||||||
|
:step="field.validation?.step || 'any'"
|
||||||
|
:min="field.validation?.min"
|
||||||
|
:max="field.validation?.max"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
step="any"
|
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
|
|||||||
@@ -72,11 +72,11 @@
|
|||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="!isValueInput"
|
v-if="!isValueInput && !isSelectInput"
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
v-model="field.required"
|
v-model="field.required"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -84,14 +84,14 @@
|
|||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="!isValueInput"
|
v-if="!isValueInput && !isSelectInput"
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>{{ t('required') }}</label>
|
>{{ t('required') }}</label>
|
||||||
<input
|
<input
|
||||||
v-if="isValueInput"
|
v-if="isValueInput || isSelectInput"
|
||||||
:id="`readonly-checkbox-${field.uuid}`"
|
:id="`readonly-checkbox-${field.uuid}`"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="isValueInput"
|
v-if="isValueInput || isSelectInput"
|
||||||
:for="`readonly-checkbox-${field.uuid}`"
|
:for="`readonly-checkbox-${field.uuid}`"
|
||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
||||||
@@ -164,30 +164,39 @@
|
|||||||
ref="touchValueTarget"
|
ref="touchValueTarget"
|
||||||
class="flex h-full w-full field-area"
|
class="flex h-full w-full field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||||
@click="focusValueInput"
|
@click="focusValueInput"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1"
|
class="flex justify-center items-center space-x-1"
|
||||||
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
|
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && !isDefaultValuePresent }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
v-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
:class="{ 'w-full h-full': isWFullType }"
|
||||||
:style="fontStyle"
|
:style="fontStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
:style="{ color: field.preferences?.color }"
|
:style="{ color: field.preferences?.color }"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
:class="{ 'w-full h-full': isWFullType }"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
class="aspect-square mx-auto"
|
class="aspect-square mx-auto"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
/>
|
/>
|
||||||
|
<template
|
||||||
|
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
|
||||||
|
class="aspect-square mx-auto"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
@@ -210,14 +219,39 @@
|
|||||||
{{ char }}
|
{{ char }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<select
|
||||||
|
v-else-if="isSelectInput"
|
||||||
|
ref="defaultValueSelect"
|
||||||
|
class="bg-transparent outline-none focus:outline-none w-full"
|
||||||
|
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
||||||
|
@focus="selectedAreaRef.value = area"
|
||||||
|
@keydown.enter="onDefaultValueEnter"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
:disabled="!field.default_value?.length"
|
||||||
|
:selected="!field.default_value?.length"
|
||||||
|
:value="''"
|
||||||
|
>
|
||||||
|
{{ t(field.default_value?.length ? 'none' : 'select') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(option, index) in field.options"
|
||||||
|
:key="index"
|
||||||
|
:selected="field.default_value === option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.value }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
ref="defaultValue"
|
ref="defaultValue"
|
||||||
:contenteditable="isValueInput"
|
:contenteditable="isValueInput"
|
||||||
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
|
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
||||||
:class="{ 'cursor-text': isValueInput }"
|
:class="{ 'cursor-text': isValueInput }"
|
||||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
|
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
||||||
@blur="onDefaultValueBlur"
|
@blur="onDefaultValueBlur"
|
||||||
|
@focus="selectedAreaRef.value = area"
|
||||||
@paste.prevent="onPaste"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter="onDefaultValueEnter"
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>{{ field.default_value }}</span>
|
>{{ field.default_value }}</span>
|
||||||
@@ -225,7 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="fieldIcons[field.type]"
|
:is="fieldIcons[field.type]"
|
||||||
v-else
|
v-else-if="!isCheckboxInput"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10 opacity-50"
|
class="max-h-10 opacity-50"
|
||||||
@@ -233,12 +267,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isValueInput"
|
v-if="!isValueInput && !isSelectInput"
|
||||||
ref="touchTarget"
|
ref="touchTarget"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0"
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
||||||
@dblclick="maybeToggleDefaultValue"
|
@dblclick="maybeToggleDefaultValue"
|
||||||
@click="maybeToggleCheckboxValue"
|
@click="inputMode && maybeToggleCheckboxValue()"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type && editable"
|
v-if="field?.type && editable"
|
||||||
@@ -253,6 +287,7 @@
|
|||||||
<FormulaModal
|
<FormulaModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -264,6 +299,7 @@
|
|||||||
<FontModal
|
<FontModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -275,6 +311,7 @@
|
|||||||
<ConditionsModal
|
<ConditionsModal
|
||||||
:item="field"
|
:item="field"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
:default-field="defaultField"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -285,6 +322,7 @@
|
|||||||
<DescriptionModal
|
<DescriptionModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -398,6 +436,9 @@ export default {
|
|||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
isWFullType () {
|
||||||
|
return ['cells', 'checkbox', 'radio', 'multiple', 'select'].includes(this.field.type)
|
||||||
|
},
|
||||||
fontStyle () {
|
fontStyle () {
|
||||||
let fontSize = ''
|
let fontSize = ''
|
||||||
|
|
||||||
@@ -417,6 +458,13 @@ export default {
|
|||||||
|
|
||||||
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||||
},
|
},
|
||||||
|
optionsUuidIndex () {
|
||||||
|
return this.field.options.reduce((acc, option) => {
|
||||||
|
acc[option.uuid] = option
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
fontSizePx () {
|
fontSizePx () {
|
||||||
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||||
},
|
},
|
||||||
@@ -427,14 +475,17 @@ export default {
|
|||||||
return 1040 / 612.0
|
return 1040 / 612.0
|
||||||
},
|
},
|
||||||
isDefaultValuePresent () {
|
isDefaultValuePresent () {
|
||||||
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
return this.field?.default_value || this.field?.default_value === 0
|
||||||
return false
|
},
|
||||||
} else {
|
isSelectInput () {
|
||||||
return this.field?.default_value || this.field?.default_value === 0
|
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
||||||
}
|
},
|
||||||
|
isCheckboxInput () {
|
||||||
|
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
||||||
},
|
},
|
||||||
isValueInput () {
|
isValueInput () {
|
||||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
|
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
||||||
|
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
||||||
},
|
},
|
||||||
modalContainerEl () {
|
modalContainerEl () {
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
@@ -549,6 +600,11 @@ export default {
|
|||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
|
buildAreaOptionValue (area) {
|
||||||
|
const option = this.optionsUuidIndex[area.option_uuid]
|
||||||
|
|
||||||
|
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||||
|
},
|
||||||
maybeToggleDefaultValue () {
|
maybeToggleDefaultValue () {
|
||||||
if (!this.editable) {
|
if (!this.editable) {
|
||||||
return
|
return
|
||||||
@@ -558,22 +614,41 @@ export default {
|
|||||||
this.isContenteditable = true
|
this.isContenteditable = true
|
||||||
|
|
||||||
this.focusValueInput()
|
this.focusValueInput()
|
||||||
} else if (this.field.type === 'checkbox') {
|
|
||||||
this.field.readonly = !this.field.readonly
|
|
||||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
} else if (this.field.type === 'date') {
|
} else if (this.field.type === 'date') {
|
||||||
this.field.readonly = !this.field.readonly
|
this.field.readonly = !this.field.readonly
|
||||||
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
|
} else {
|
||||||
|
this.maybeToggleCheckboxValue()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeToggleCheckboxValue () {
|
maybeToggleCheckboxValue () {
|
||||||
if (this.inputMode && this.field.type === 'checkbox') {
|
if (this.field.type === 'checkbox') {
|
||||||
this.field.readonly = !this.field.readonly
|
|
||||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||||
|
this.field.readonly = this.field.default_value === true
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else if (this.field.type === 'radio' && this.area.option_uuid) {
|
||||||
|
const value = this.buildAreaOptionValue(this.area)
|
||||||
|
|
||||||
|
this.field.default_value === value ? delete this.field.default_value : this.field.default_value = value
|
||||||
|
|
||||||
|
this.field.readonly = !!this.field.default_value?.length
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else if (this.field.type === 'multiple' && this.area.option_uuid) {
|
||||||
|
const value = this.buildAreaOptionValue(this.area)
|
||||||
|
|
||||||
|
if (this.field.default_value?.includes(value)) {
|
||||||
|
this.field.default_value.splice(this.field.default_value.indexOf(value), 1)
|
||||||
|
|
||||||
|
if (!this.field.default_value?.length) delete this.field.default_value
|
||||||
|
} else {
|
||||||
|
Array.isArray(this.field.default_value) ? this.field.default_value.push(value) : this.field.default_value = [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.field.readonly = !!this.field.default_value?.length
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
@@ -749,7 +824,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask' && this.editable) {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
|
|
||||||
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||||
@@ -765,7 +840,9 @@ export default {
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.isDragged = true
|
if (this.editable) {
|
||||||
|
this.isDragged = true
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
@@ -818,7 +895,9 @@ export default {
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.isDragged = true
|
if (this.editable) {
|
||||||
|
this.isDragged = true
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
|||||||
@@ -1065,7 +1065,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'stamp') {
|
if (type === 'stamp') {
|
||||||
@@ -1109,7 +1109,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'stamp') {
|
if (type === 'stamp') {
|
||||||
@@ -1265,24 +1265,31 @@ export default {
|
|||||||
if (!field.areas.length) {
|
if (!field.areas.length) {
|
||||||
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
|
||||||
this.template.fields.forEach((f) => {
|
this.removeFieldConditions(field)
|
||||||
(f.conditions || []).forEach((c) => {
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeFieldConditions (field) {
|
||||||
|
this.template.fields.forEach((f) => {
|
||||||
|
if (f.conditions) {
|
||||||
|
f.conditions.forEach((c) => {
|
||||||
if (c.field_uuid === field.uuid) {
|
if (c.field_uuid === field.uuid) {
|
||||||
f.conditions.splice(f.conditions.indexOf(c), 1)
|
f.conditions.splice(f.conditions.indexOf(c), 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.template.schema.forEach((item) => {
|
this.template.schema.forEach((item) => {
|
||||||
(item.conditions || []).forEach((c) => {
|
if (item.conditions) {
|
||||||
|
item.conditions.forEach((c) => {
|
||||||
if (c.field_uuid === field.uuid) {
|
if (c.field_uuid === field.uuid) {
|
||||||
item.conditions.splice(item.conditions.indexOf(c), 1)
|
item.conditions.splice(item.conditions.indexOf(c), 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
},
|
||||||
pasteField () {
|
pasteField () {
|
||||||
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
||||||
@@ -1301,7 +1308,13 @@ export default {
|
|||||||
this.copiedArea.option_uuid ||= field.options[0].uuid
|
this.copiedArea.option_uuid ||= field.options[0].uuid
|
||||||
area.option_uuid = v4()
|
area.option_uuid = v4()
|
||||||
|
|
||||||
field.options.push({ uuid: area.option_uuid })
|
const lastOption = field.options[field.options.length - 1]
|
||||||
|
|
||||||
|
if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
|
||||||
|
area.option_uuid = lastOption.uuid
|
||||||
|
} else {
|
||||||
|
field.options.push({ uuid: area.option_uuid })
|
||||||
|
}
|
||||||
|
|
||||||
field.areas.push(area)
|
field.areas.push(area)
|
||||||
} else {
|
} else {
|
||||||
@@ -1477,7 +1490,7 @@ export default {
|
|||||||
if (this.dragField?.options?.length) {
|
if (this.dragField?.options?.length) {
|
||||||
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
|
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
|
||||||
} else {
|
} else {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1709,8 +1722,15 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.template.fields =
|
this.template.fields = this.template.fields.reduce((acc, f) => {
|
||||||
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
|
||||||
|
this.removeFieldConditions(f)
|
||||||
|
} else {
|
||||||
|
acc.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
|
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -160,6 +160,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
buildDefaultName: {
|
buildDefaultName: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ field.name || buildDefaultName(field, template.fields) }}
|
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -73,6 +73,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.options && withOptions"
|
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"
|
draggable="true"
|
||||||
@@ -216,12 +216,30 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-else-if="field.options && editable && !defaultField"
|
v-else-if="field.options && editable && !defaultField"
|
||||||
class="text-center text-sm w-full pb-1"
|
class="field-add-option text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOption"
|
||||||
>
|
>
|
||||||
+ {{ t('add_option') }}
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.options && withOptions && !isExpandOptions && field.options.length > 4"
|
||||||
|
class="border-t border-base-300 mx-2 space-y-1.5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="field-expand-options text-center text-sm w-full py-1 flex space-x-0.5 justify-center items-center"
|
||||||
|
@click="isExpandOptions = true"
|
||||||
|
>
|
||||||
|
<span class="lowercase">
|
||||||
|
{{ field.options.length }} {{ t('options') }}
|
||||||
|
</span>
|
||||||
|
<IconChevronDown
|
||||||
|
class="ml-2 mr-2 mt-0.5"
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Teleport
|
<Teleport
|
||||||
v-if="isShowFormulaModal"
|
v-if="isShowFormulaModal"
|
||||||
@@ -230,6 +248,7 @@
|
|||||||
<FormulaModal
|
<FormulaModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -241,6 +260,7 @@
|
|||||||
<FontModal
|
<FontModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -251,6 +271,7 @@
|
|||||||
>
|
>
|
||||||
<ConditionsModal
|
<ConditionsModal
|
||||||
:item="field"
|
:item="field"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -262,6 +283,7 @@
|
|||||||
<DescriptionModal
|
<DescriptionModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -278,7 +300,7 @@ import FormulaModal from './formula_modal'
|
|||||||
import FontModal from './font_modal'
|
import FontModal from './font_modal'
|
||||||
import ConditionsModal from './conditions_modal'
|
import ConditionsModal from './conditions_modal'
|
||||||
import DescriptionModal from './description_modal'
|
import DescriptionModal from './description_modal'
|
||||||
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
|
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -288,6 +310,7 @@ export default {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
FieldSettings,
|
FieldSettings,
|
||||||
PaymentSettings,
|
PaymentSettings,
|
||||||
|
IconChevronDown,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
FormulaModal,
|
FormulaModal,
|
||||||
FontModal,
|
FontModal,
|
||||||
@@ -333,6 +356,7 @@ export default {
|
|||||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
isExpandOptions: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
showPaymentModal: false,
|
showPaymentModal: false,
|
||||||
isShowFormulaModal: false,
|
isShowFormulaModal: false,
|
||||||
@@ -427,6 +451,8 @@ export default {
|
|||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
addOption () {
|
addOption () {
|
||||||
|
this.isExpandOptions = true
|
||||||
|
|
||||||
this.field.options.push({ value: '', uuid: v4() })
|
this.field.options.push({ value: '', uuid: v4() })
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
@@ -1,31 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
|
||||||
v-if="field.type === 'number'"
|
|
||||||
class="py-1.5 px-1 relative"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
:placeholder="t('format')"
|
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
|
||||||
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="format in numberFormats"
|
|
||||||
:key="format"
|
|
||||||
:value="format"
|
|
||||||
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
|
||||||
>
|
|
||||||
{{ formatNumber(123456789.567, format) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label
|
|
||||||
:style="{ backgroundColor }"
|
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
|
||||||
style="font-size: 8px"
|
|
||||||
>
|
|
||||||
{{ t('format') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'verification'"
|
v-if="field.type === 'verification'"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -217,6 +190,77 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'number'"
|
||||||
|
class="py-1.5 px-1 relative flex space-x-1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('min')"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:value="field.validation?.min"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.validation?.min"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('min') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('max')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
:value="field.validation?.max"
|
||||||
|
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.validation?.max"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('max') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'number'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in numberFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
||||||
|
>
|
||||||
|
{{ formatNumber(123456789.567, format) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -393,7 +437,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
v-if="['text', 'number'].includes(field.type)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -401,6 +445,7 @@
|
|||||||
v-model="field.readonly"
|
v-model="field.readonly"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
>
|
>
|
||||||
<span class="label-text">{{ t('read_only') }}</span>
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -202,6 +202,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -28,14 +28,21 @@
|
|||||||
class="link"
|
class="link"
|
||||||
>{{ t('available_in_pro') }}</a>
|
>{{ t('available_in_pro') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-inline mb-2 gap-2 space-y-1">
|
<div class="flex flex-wrap mb-2 gap-y-1 pt-1">
|
||||||
<button
|
<button
|
||||||
v-for="f in fields"
|
v-for="f in fields"
|
||||||
:key="f.uuid"
|
:key="f.uuid"
|
||||||
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
||||||
>
|
>
|
||||||
|
<IconMathFunction
|
||||||
|
v-if="f.preferences?.formula"
|
||||||
|
width="17"
|
||||||
|
height="17"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
<IconCodePlus
|
<IconCodePlus
|
||||||
|
v-else
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
@@ -57,7 +64,7 @@
|
|||||||
<div class="mb-3 mt-1">
|
<div class="mb-3 mt-1">
|
||||||
<div
|
<div
|
||||||
target="blank"
|
target="blank"
|
||||||
class="text-sm mb-2 inline space-x-2"
|
class="text-sm mb-2 inline space-x-2 font-mono"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@@ -83,12 +90,6 @@
|
|||||||
>
|
>
|
||||||
/
|
/
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
|
||||||
@click="insertTextUnderCursor(' % ')"
|
|
||||||
>
|
|
||||||
%
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@click="insertTextUnderCursor('^')"
|
@click="insertTextUnderCursor('^')"
|
||||||
@@ -122,12 +123,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCodePlus } from '@tabler/icons-vue'
|
import { IconCodePlus, IconMathFunction } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormulaModal',
|
name: 'FormulaModal',
|
||||||
components: {
|
components: {
|
||||||
IconCodePlus
|
IconCodePlus,
|
||||||
|
IconMathFunction
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'template', 'withFormula'],
|
inject: ['t', 'save', 'template', 'withFormula'],
|
||||||
props: {
|
props: {
|
||||||
@@ -135,6 +137,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -154,7 +161,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fields () {
|
fields () {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
|
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +231,11 @@ export default {
|
|||||||
|
|
||||||
this.formula = newText
|
this.formula = newText
|
||||||
|
|
||||||
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
this.$nextTick(() => {
|
||||||
|
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
||||||
|
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const en = {
|
|||||||
copy_to_all_pages: 'Copy to All Pages',
|
copy_to_all_pages: 'Copy to All Pages',
|
||||||
add_option: 'Add option',
|
add_option: 'Add option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
options: 'Options',
|
||||||
condition: 'Condition',
|
condition: 'Condition',
|
||||||
first_party: 'First Party',
|
first_party: 'First Party',
|
||||||
second_party: 'Second Party',
|
second_party: 'Second Party',
|
||||||
@@ -255,6 +256,7 @@ const es = {
|
|||||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||||
add_option: 'Agregar opción',
|
add_option: 'Agregar opción',
|
||||||
option: 'Opción',
|
option: 'Opción',
|
||||||
|
options: 'Opciones',
|
||||||
first_party: 'Primera Parte',
|
first_party: 'Primera Parte',
|
||||||
second_party: 'Segunda Parte',
|
second_party: 'Segunda Parte',
|
||||||
third_party: 'Tercera Parte',
|
third_party: 'Tercera Parte',
|
||||||
@@ -435,6 +437,7 @@ const it = {
|
|||||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||||
add_option: 'Aggiungi opzione',
|
add_option: 'Aggiungi opzione',
|
||||||
option: 'Opzione',
|
option: 'Opzione',
|
||||||
|
options: 'Opzioni',
|
||||||
condition: 'Condizione',
|
condition: 'Condizione',
|
||||||
first_party: 'Prima parte',
|
first_party: 'Prima parte',
|
||||||
second_party: 'Seconda parte',
|
second_party: 'Seconda parte',
|
||||||
@@ -607,6 +610,7 @@ const pt = {
|
|||||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||||
add_option: 'Adicionar opção',
|
add_option: 'Adicionar opção',
|
||||||
option: 'Opção',
|
option: 'Opção',
|
||||||
|
options: 'Opções',
|
||||||
first_party: 'Primeira Parte',
|
first_party: 'Primeira Parte',
|
||||||
second_party: 'Segunda Parte',
|
second_party: 'Segunda Parte',
|
||||||
third_party: 'Terceira Parte',
|
third_party: 'Terceira Parte',
|
||||||
@@ -782,6 +786,7 @@ const fr = {
|
|||||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||||
add_option: 'Ajouter une option',
|
add_option: 'Ajouter une option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
options: 'Options',
|
||||||
first_party: 'Première partie',
|
first_party: 'Première partie',
|
||||||
second_party: 'Deuxième partie',
|
second_party: 'Deuxième partie',
|
||||||
third_party: 'Troisième partie',
|
third_party: 'Troisième partie',
|
||||||
@@ -958,6 +963,7 @@ const de = {
|
|||||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||||
add_option: 'Option hinzufügen',
|
add_option: 'Option hinzufügen',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
options: 'Options',
|
||||||
first_party: 'Erste Partei',
|
first_party: 'Erste Partei',
|
||||||
second_party: 'Zweite Partei',
|
second_party: 'Zweite Partei',
|
||||||
third_party: 'Dritte Partei',
|
third_party: 'Dritte Partei',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||||
:class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
|
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
|
||||||
style="container-type: size"
|
style="container-type: size"
|
||||||
:style="{ aspectRatio: `${width} / ${height}`}"
|
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,12 +38,19 @@ class ProcessSubmitterCompletionJob
|
|||||||
|
|
||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
|
|
||||||
|
complete_verification_events, sms_events =
|
||||||
|
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
||||||
|
.partition { |e| e.event_type == 'complete_verification' }
|
||||||
|
|
||||||
|
complete_verification_event = complete_verification_events.first
|
||||||
|
|
||||||
completed_submitter.assign_attributes(
|
completed_submitter.assign_attributes(
|
||||||
submission_id: submitter.submission_id,
|
submission_id: submitter.submission_id,
|
||||||
account_id: submission.account_id,
|
account_id: submission.account_id,
|
||||||
template_id: submission.template_id,
|
template_id: submission.template_id,
|
||||||
source: submission.source,
|
source: submission.source,
|
||||||
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||||
|
verification_method: complete_verification_event&.data&.dig('method'),
|
||||||
completed_at: submitter.completed_at
|
completed_at: submitter.completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
|
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
|
||||||
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
||||||
|
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
|
||||||
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
||||||
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
|
|||||||
@@ -4,16 +4,17 @@
|
|||||||
#
|
#
|
||||||
# Table name: completed_submitters
|
# Table name: completed_submitters
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# completed_at :datetime not null
|
# completed_at :datetime not null
|
||||||
# sms_count :integer not null
|
# sms_count :integer not null
|
||||||
# source :string not null
|
# source :string not null
|
||||||
# created_at :datetime not null
|
# verification_method :string
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :bigint not null
|
# updated_at :datetime not null
|
||||||
# submission_id :bigint not null
|
# account_id :bigint not null
|
||||||
# submitter_id :bigint not null
|
# submission_id :bigint not null
|
||||||
# template_id :bigint
|
# submitter_id :bigint not null
|
||||||
|
# template_id :bigint
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -29,5 +30,5 @@ class CompletedSubmitter < ApplicationRecord
|
|||||||
has_many :completed_documents, dependent: :destroy,
|
has_many :completed_documents, dependent: :destroy,
|
||||||
primary_key: :submitter_id,
|
primary_key: :submitter_id,
|
||||||
foreign_key: :submitter_id,
|
foreign_key: :submitter_id,
|
||||||
inverse_of: :submitter
|
inverse_of: :completed_submitter
|
||||||
end
|
end
|
||||||
@@ -114,16 +114,16 @@ class Submission < ApplicationRecord
|
|||||||
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def audit_trail_url
|
def audit_trail_url(expires_at: nil)
|
||||||
return if audit_trail.blank?
|
return if audit_trail.blank?
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(audit_trail.blob)
|
ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
|
||||||
end
|
end
|
||||||
alias audit_log_url audit_trail_url
|
alias audit_log_url audit_trail_url
|
||||||
|
|
||||||
def combined_document_url
|
def combined_document_url(expires_at: nil)
|
||||||
return if combined_document.blank?
|
return if combined_document.blank?
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(combined_document.blob)
|
ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -130,6 +130,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
<%= t('expirable_file_download_links') %>
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if can?(:manage, account_config) %>
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
|||||||
@@ -11,10 +11,21 @@
|
|||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
<div class="flex w-full space-x-4">
|
<div class="flex w-full space-x-4">
|
||||||
<% token = current_user.access_token.token %>
|
<% token = current_user.access_token.token %>
|
||||||
<masked-input class="block w-full" data-token="<%= token %>">
|
<% obscured_token = current_user.access_token.token.sub(token[5..], '*' * token[5..].size) %>
|
||||||
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
<% if current_account.testing? %>
|
||||||
</masked-input>
|
<masked-input class="block w-full" data-token="<%= token %>">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||||
|
</masked-input>
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
|
<% else %>
|
||||||
|
<a id="access_token_container" href="<%= settings_reveal_access_token_path %>" data-turbo-frame="modal" class="flex w-full space-x-4">
|
||||||
|
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||||
|
<div class="base-button">
|
||||||
|
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
|
||||||
|
<span class="hidden md:inline"><%= t('copy') %></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
|
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,19 +54,29 @@
|
|||||||
<p class="text-2xl font-bold mt-8 mb-4">
|
<p class="text-2xl font-bold mt-8 mb-4">
|
||||||
<%= t('change_password') %>
|
<%= t('change_password') %>
|
||||||
</p>
|
</p>
|
||||||
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off' } do |f| %>
|
||||||
<div class="form-control">
|
<%= f.label :password, t('new_password'), class: 'label' %>
|
||||||
<%= f.label :password, t('new_password'), class: 'label' %>
|
<%= f.password_field :password, autocomplete: 'off', class: 'base-input peer w-full', required: true %>
|
||||||
<%= f.password_field :password, autocomplete: 'off', class: 'base-input' %>
|
<div class="<%= 'peer-invalid:hidden' if current_user.errors.blank? %> space-y-4 mt-4">
|
||||||
</div>
|
<div class="form-control">
|
||||||
<div class="form-control">
|
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
|
||||||
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
|
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
||||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
</div>
|
||||||
</div>
|
<div class="form-control">
|
||||||
<div class="form-control pt-2">
|
<%= f.label :current_password, t('current_password'), class: 'label' %>
|
||||||
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
<%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
|
||||||
|
<% if Accounts.can_send_emails?(current_account) %>
|
||||||
|
<span class="label-text-alt mt-1">
|
||||||
|
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= button_to nil, user_send_reset_password_path(current_user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_') } %>
|
||||||
<p class="text-2xl font-bold mt-8 mb-4">
|
<p class="text-2xl font-bold mt-8 mb-4">
|
||||||
<%= t('two_factor_authentication') %>
|
<%= t('two_factor_authentication') %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"orientation": "natural",
|
"orientation": "any",
|
||||||
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
|
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
|
||||||
"categories": ["productivity", "utilities"],
|
"categories": ["productivity", "utilities"],
|
||||||
"theme_color": "#FAF7F4",
|
"theme_color": "#FAF7F4",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<input id="api_key" type="text" value="<%= token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: t('reveal_api_key') do %>
|
||||||
|
<%= form_tag settings_reveal_access_token_path, enctype: 'multipart/form-data', data: { turbo_frame: :_top } do %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= label_tag :password, t('enter_your_password_to_reveal_the_api_key'), class: 'label' %>
|
||||||
|
<%= password_field_tag :password, nil, class: 'base-input', autocomplete: 'current-password', required: true, autofocus: true, placeholder: t('password') %>
|
||||||
|
<% if local_assigns[:error_message].present? %>
|
||||||
|
<span class="label-text-alt text-red-400 mt-1"><%= local_assigns[:error_message] %></span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= submit_tag t('submit'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -56,6 +56,8 @@
|
|||||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
|
||||||
<% if can?(:read, WebhookUrl) %>
|
<% if can?(:read, WebhookUrl) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||||
@@ -70,7 +72,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% 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: { prefetch: false } do %>
|
||||||
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% 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['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? %>
|
<% next if value.blank? %>
|
||||||
<% submitter = submitters_index[field['submitter_uuid']] %>
|
<% submitter = submitters_index[field['submitter_uuid']] %>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %>
|
||||||
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
|
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx }, method: :get, data: { turbo_frame: :_top } do %>
|
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||||
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
||||||
<div class="enabled">
|
<div class="enabled">
|
||||||
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv }, method: :get, data: { turbo_frame: :_top } do %>
|
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||||
<div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
<div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
||||||
<div class="enabled">
|
<div class="enabled">
|
||||||
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<% if params[:q].present? || params[:status].present? || filter_params.present? || @pagy.pages > 1 %>
|
<% if params[:q].present? || params[:status].present? || filter_params.present? || @pagy.pages > 1 %>
|
||||||
<%= render 'shared/search_input', title_selector: 'h2' %>
|
<%= render 'shared/search_input', title_selector: 'h2' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
|
<%= link_to new_template_submissions_export_path(@template, params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS)), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
|
||||||
<span><%= t('export') %></span>
|
<span><%= t('export') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,22 +1,37 @@
|
|||||||
<%= form_for user, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for user, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="form-control">
|
<div class="flex space-x-4">
|
||||||
<%= f.label :first_name, t('first_name'), class: 'label' %>
|
<div class="w-full">
|
||||||
<%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
|
<%= f.label :first_name, t('first_name'), class: 'label' %>
|
||||||
</div>
|
<%= f.text_field :first_name, required: true, class: 'base-input w-full', dir: 'auto' %>
|
||||||
<div class="form-control">
|
</div>
|
||||||
<%= f.label :last_name, t('last_name'), class: 'label' %>
|
<div class="w-full">
|
||||||
<%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
|
<%= f.label :last_name, t('last_name'), class: 'label' %>
|
||||||
|
<%= f.text_field :last_name, required: true, class: 'base-input w-full', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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.persisted? && Accounts.can_send_emails?(current_account) %>
|
||||||
|
<span class="label-text-alt mt-2 mx-1">
|
||||||
|
<%= t('click_here_to_send_a_reset_password_email_html') %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<% if user.new_record? && !Docuseal.multitenant? %>
|
||||||
<%= f.label :password, t('password'), class: 'label' %>
|
<div class="form-control">
|
||||||
<%= f.password_field :password, required: user.new_record?, class: 'base-input' %>
|
<%= f.label :password, t('password'), class: 'label' %>
|
||||||
</div>
|
<%= f.password_field :password, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if f.object != current_user %>
|
<% if f.object != current_user %>
|
||||||
|
<% if user.otp_required_for_login %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :otp_required_for_login, t('two_factor_authentication'), class: 'label' %>
|
||||||
|
<%= f.select :otp_required_for_login, [[t('enabled'), true], [t('disabled'), false]], { include_blank: false }, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<%= render 'role_select', f: %>
|
<%= render 'role_select', f: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if local_assigns[:extra_fields_html].present? %>
|
<% if local_assigns[:extra_fields_html].present? %>
|
||||||
@@ -27,3 +42,6 @@
|
|||||||
<%= f.button button_title, class: 'base-button' %>
|
<%= f.button button_title, class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if user.persisted? %>
|
||||||
|
<%= button_to nil, user_send_reset_password_path(user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_'), turbo_frame: :_top } %>
|
||||||
|
<% end %>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||||
<h1 class="text-4xl font-bold">Webhook</h1>
|
<h1 class="text-4xl font-bold">Webhook</h1>
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||||
<% if params[:action] == 'index' %>
|
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
|
||||||
<%= render 'shared/test_mode_toggle' %>
|
<%= render 'shared/test_mode_toggle' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @webhook_url.persisted? && params[:action] == 'index' %>
|
<% if @webhook_url.persisted? && params[:action] == 'index' %>
|
||||||
|
|||||||
+66
-1
@@ -24,8 +24,12 @@ en: &en
|
|||||||
thanks: Thanks
|
thanks: Thanks
|
||||||
private: Private
|
private: Private
|
||||||
select: Select
|
select: Select
|
||||||
|
enabled: Enabled
|
||||||
|
disabled: Disabled
|
||||||
party: Party
|
party: Party
|
||||||
|
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
|
||||||
edit_order: Edit Order
|
edit_order: Edit Order
|
||||||
|
expirable_file_download_links: Expirable file download links
|
||||||
invite_form_fields: Invite form fields
|
invite_form_fields: Invite form fields
|
||||||
default_parties: Default parties
|
default_parties: Default parties
|
||||||
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
|
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
|
||||||
@@ -36,13 +40,14 @@ en: &en
|
|||||||
bcc_recipients: BCC recipients
|
bcc_recipients: BCC recipients
|
||||||
resend_pending: Re-send pending
|
resend_pending: Re-send pending
|
||||||
always_enforce_signing_order: Always enforce the signing order
|
always_enforce_signing_order: Always enforce the signing order
|
||||||
|
create_templates_with_private_access_by_default: Create templates with private access by default
|
||||||
ensure_unique_recipients: Ensure unique recipients
|
ensure_unique_recipients: Ensure unique recipients
|
||||||
edit_per_party: Edit per party
|
edit_per_party: Edit per party
|
||||||
reply_to: Reply to
|
reply_to: Reply to
|
||||||
pending_by_me: Pending by me
|
pending_by_me: Pending by me
|
||||||
partially_completed: Partially completed
|
partially_completed: Partially completed
|
||||||
require_phone_2fa_to_open: Require phone 2FA to open
|
require_phone_2fa_to_open: Require phone 2FA to open
|
||||||
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested a two factor authentication via one time password sent to your <b>%{phone}</b> phone number.
|
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested two-factor authentication via a one-time password sent to your <b>%{phone}</b> phone number.
|
||||||
send_verification_code: Send verification code
|
send_verification_code: Send verification code
|
||||||
code_has_been_resent: Code has been re-sent
|
code_has_been_resent: Code has been re-sent
|
||||||
invalid_code: Invalid code
|
invalid_code: Invalid code
|
||||||
@@ -794,6 +799,12 @@ en: &en
|
|||||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
|
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
|
||||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
|
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
|
||||||
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
|
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
|
||||||
|
reveal_api_key: Reveal API Key
|
||||||
|
enter_your_password_to_reveal_the_api_key: Enter your password to reveal the API key
|
||||||
|
wrong_password: Wrong password.
|
||||||
|
current_password: Current password
|
||||||
|
dont_remember_your_current_password_click_here_to_reset_it_html: 'Don''t remember your current password? <label class="link" for="resend_password_button">Click here</label> to reset it.'
|
||||||
|
an_email_with_password_reset_instructions_has_been_sent: An email with password reset instructions has been sent.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Bulk Send
|
bulk: Bulk Send
|
||||||
@@ -900,6 +911,10 @@ en: &en
|
|||||||
range_without_total: "%{from}-%{to} events"
|
range_without_total: "%{from}-%{to} events"
|
||||||
|
|
||||||
es: &es
|
es: &es
|
||||||
|
enabled: Habilitado
|
||||||
|
disabled: Deshabilitado
|
||||||
|
expirable_file_download_links: Enlaces de descarga de archivos con vencimiento
|
||||||
|
create_templates_with_private_access_by_default: Crear plantillas con acceso privado por defecto
|
||||||
party: Parte
|
party: Parte
|
||||||
edit_order: Edita Pedido
|
edit_order: Edita Pedido
|
||||||
select: Seleccionar
|
select: Seleccionar
|
||||||
@@ -1674,6 +1689,12 @@ es: &es
|
|||||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
|
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
|
||||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
|
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
|
||||||
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
|
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
|
||||||
|
reveal_api_key: Revelar clave API
|
||||||
|
enter_your_password_to_reveal_the_api_key: Introduce tu contraseña para revelar la clave API
|
||||||
|
wrong_password: Contraseña incorrecta.
|
||||||
|
current_password: Contraseña actual
|
||||||
|
dont_remember_your_current_password_click_here_to_reset_it_html: '¿No recuerdas tu contraseña actual? <label class="link" for="resend_password_button">Haz clic aquí</label> para restablecerla.'
|
||||||
|
an_email_with_password_reset_instructions_has_been_sent: Se enviará un correo electrónico con las instrucciones para restablecer tu contraseña en unos minutos.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envío masivo
|
bulk: Envío masivo
|
||||||
@@ -1780,6 +1801,11 @@ es: &es
|
|||||||
range_without_total: "%{from}-%{to} eventos"
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
it: &it
|
it: &it
|
||||||
|
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Clicca qui</label> per inviare una email per reimpostare la password.'
|
||||||
|
enabled: Abilitato
|
||||||
|
disabled: Disabilitato
|
||||||
|
expirable_file_download_links: Link di download di file con scadenza
|
||||||
|
create_templates_with_private_access_by_default: Crea modelli con accesso privato per impostazione predefinita
|
||||||
party: Parte
|
party: Parte
|
||||||
edit_order: Modifica Ordine
|
edit_order: Modifica Ordine
|
||||||
select: Seleziona
|
select: Seleziona
|
||||||
@@ -2554,6 +2580,12 @@ it: &it
|
|||||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
|
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
|
||||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
|
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
|
||||||
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
|
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
|
||||||
|
reveal_api_key: Mostra chiave API
|
||||||
|
enter_your_password_to_reveal_the_api_key: Inserisci la tua password per mostrare la chiave API
|
||||||
|
wrong_password: Password errata.
|
||||||
|
current_password: Password attuale
|
||||||
|
dont_remember_your_current_password_click_here_to_reset_it_html: 'Non ricordi la tua password attuale? <label class="link" for="resend_password_button">Clicca qui</label> per reimpostarla.'
|
||||||
|
an_email_with_password_reset_instructions_has_been_sent: Un'email con le istruzioni per reimpostare la password ti è stata inviata e arriverà entro pochi minuti.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Invio massivo
|
bulk: Invio massivo
|
||||||
@@ -2660,6 +2692,11 @@ it: &it
|
|||||||
range_without_total: "%{from}-%{to} eventi"
|
range_without_total: "%{from}-%{to} eventi"
|
||||||
|
|
||||||
fr: &fr
|
fr: &fr
|
||||||
|
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Cliquez ici</label> pour envoyer un e-mail de réinitialisation du mot de passe.'
|
||||||
|
enabled: Activé
|
||||||
|
disabled: Désactivé
|
||||||
|
expirable_file_download_links: Liens de téléchargement de fichiers expirables
|
||||||
|
create_templates_with_private_access_by_default: Créer des modèles avec un accès privé par défaut
|
||||||
party: Partie
|
party: Partie
|
||||||
edit_order: Modifier la commande
|
edit_order: Modifier la commande
|
||||||
select: Sélectionner
|
select: Sélectionner
|
||||||
@@ -3437,6 +3474,12 @@ fr: &fr
|
|||||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
|
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
|
||||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de l’e-mail.'
|
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de l’e-mail.'
|
||||||
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
|
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
|
||||||
|
reveal_api_key: Révéler la clé API
|
||||||
|
enter_your_password_to_reveal_the_api_key: Entrez votre mot de passe pour révéler la clé API
|
||||||
|
wrong_password: Mot de passe incorrect.
|
||||||
|
current_password: Mot de passe actuel
|
||||||
|
dont_remember_your_current_password_click_here_to_reset_it_html: 'Vous ne vous souvenez plus de votre mot de passe actuel ? <label class="link" for="resend_password_button">Cliquez ici</label> pour le réinitialiser.'
|
||||||
|
an_email_with_password_reset_instructions_has_been_sent: Un e-mail contenant les instructions pour réinitialiser votre mot de passe vous sera envoyé dans quelques minutes.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envoi en masse
|
bulk: Envoi en masse
|
||||||
@@ -3543,6 +3586,11 @@ fr: &fr
|
|||||||
range_without_total: "%{from} à %{to} événements"
|
range_without_total: "%{from} à %{to} événements"
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
|
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Clique aqui</label> para enviar um e-mail de redefinição de senha.'
|
||||||
|
enabled: Ativado
|
||||||
|
disabled: Desativado
|
||||||
|
expirable_file_download_links: Links de download de arquivos com expiração
|
||||||
|
create_templates_with_private_access_by_default: Criar modelos com acesso privado por padrão
|
||||||
party: Parte
|
party: Parte
|
||||||
edit_order: Edita Pedido
|
edit_order: Edita Pedido
|
||||||
select: Selecionar
|
select: Selecionar
|
||||||
@@ -4318,6 +4366,12 @@ pt: &pt
|
|||||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
|
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
|
||||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
|
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
|
||||||
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
|
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
|
||||||
|
reveal_api_key: Revelar chave API
|
||||||
|
enter_your_password_to_reveal_the_api_key: Insira sua senha para revelar a chave API
|
||||||
|
wrong_password: Senha incorreta.
|
||||||
|
current_password: Senha atual
|
||||||
|
dont_remember_your_current_password_click_here_to_reset_it_html: 'Não se lembra da sua senha atual? <label class="link" for="resend_password_button">Clique aqui</label> para redefini-la.'
|
||||||
|
an_email_with_password_reset_instructions_has_been_sent: Um e-mail com instruções para redefinir sua senha será enviado em alguns minutos.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envio em massa
|
bulk: Envio em massa
|
||||||
@@ -4424,6 +4478,11 @@ pt: &pt
|
|||||||
range_without_total: "%{from}-%{to} eventos"
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
de: &de
|
de: &de
|
||||||
|
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Klicken Sie hier</label>, um eine E-Mail zum Zurücksetzen des Passworts zu senden.'
|
||||||
|
enabled: Aktiviert
|
||||||
|
disabled: Deaktiviert
|
||||||
|
expirable_file_download_links: Ablaufbare Datei-Download-Links
|
||||||
|
create_templates_with_private_access_by_default: Vorlagen standardmäßig mit privatem Zugriff erstellen
|
||||||
party: Partei
|
party: Partei
|
||||||
edit_order: Bestellung bearbeiten
|
edit_order: Bestellung bearbeiten
|
||||||
select: Auswählen
|
select: Auswählen
|
||||||
@@ -5199,6 +5258,12 @@ de: &de
|
|||||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
|
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
|
||||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
|
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
|
||||||
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
|
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
|
||||||
|
reveal_api_key: API-Schlüssel anzeigen
|
||||||
|
enter_your_password_to_reveal_the_api_key: Gib dein Passwort ein, um den API-Schlüssel anzuzeigen
|
||||||
|
wrong_password: Falsches Passwort.
|
||||||
|
current_password: Aktuelles Passwort
|
||||||
|
dont_remember_your_current_password_click_here_to_reset_it_html: 'Sie erinnern sich nicht an Ihr aktuelles Passwort? <label class="link" for="resend_password_button">Klicken Sie hier</label>, um es zurückzusetzen.'
|
||||||
|
an_email_with_password_reset_instructions_has_been_sent: Eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts wurde Ihnen in wenigen Minuten zugesendet.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Massenversand
|
bulk: Massenversand
|
||||||
|
|||||||
+4
-1
@@ -65,7 +65,9 @@ Rails.application.routes.draw do
|
|||||||
resources :setup, only: %i[index create]
|
resources :setup, only: %i[index create]
|
||||||
resource :newsletter, only: %i[show update]
|
resource :newsletter, only: %i[show update]
|
||||||
resources :enquiries, only: %i[create]
|
resources :enquiries, only: %i[create]
|
||||||
resources :users, only: %i[new create edit update destroy]
|
resources :users, only: %i[new create edit update destroy] do
|
||||||
|
resource :send_reset_password, only: %i[update], controller: 'users_send_reset_password'
|
||||||
|
end
|
||||||
resource :user_signature, only: %i[edit update destroy]
|
resource :user_signature, only: %i[edit update destroy]
|
||||||
resource :user_initials, only: %i[edit update destroy]
|
resource :user_initials, only: %i[edit update destroy]
|
||||||
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
||||||
@@ -179,6 +181,7 @@ Rails.application.routes.draw do
|
|||||||
defaults: { status: :integration }
|
defaults: { status: :integration }
|
||||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||||
resources :api, only: %i[index create], controller: 'api_settings'
|
resources :api, only: %i[index create], controller: 'api_settings'
|
||||||
|
resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token'
|
||||||
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
||||||
post :resend
|
post :resend
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PopulateExpireLinkConfigs < ActiveRecord::Migration[8.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class MigrationAccount < ActiveRecord::Base
|
||||||
|
self.table_name = 'accounts'
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationAccountConfig < ActiveRecord::Base
|
||||||
|
self.table_name = 'account_configs'
|
||||||
|
|
||||||
|
serialize :value, coder: JSON
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
MigrationAccount.find_each do |account|
|
||||||
|
config = MigrationAccountConfig.find_or_initialize_by(key: 'download_links_expire', account_id: account.id)
|
||||||
|
|
||||||
|
next if config.persisted?
|
||||||
|
|
||||||
|
config.value = false
|
||||||
|
config.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddVerificationMethodToCompletedSubmitters < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :completed_submitters, :verification_method, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
|
ActiveRecord::Schema[8.0].define(version: 2025_09_01_110606) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -117,6 +117,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
|
|||||||
t.datetime "completed_at", null: false
|
t.datetime "completed_at", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.string "verification_method"
|
||||||
t.index ["account_id"], name: "index_completed_submitters_on_account_id"
|
t.index ["account_id"], name: "index_completed_submitters_on_account_id"
|
||||||
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
|
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Accounts
|
module Accounts
|
||||||
|
LINK_EXPIRES_AT = 40.minutes
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def create_duplicate(account)
|
def create_duplicate(account)
|
||||||
@@ -185,4 +187,11 @@ module Accounts
|
|||||||
rescue TZInfo::InvalidTimezoneIdentifier
|
rescue TZInfo::InvalidTimezoneIdentifier
|
||||||
'UTC'
|
'UTC'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def link_expires_at(account)
|
||||||
|
return if AccountConfig.find_or_initialize_by(account: account,
|
||||||
|
key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY).value == false
|
||||||
|
|
||||||
|
LINK_EXPIRES_AT.from_now
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -102,6 +102,7 @@ module ReplaceEmailVariables
|
|||||||
return unless submitter
|
return unless submitter
|
||||||
|
|
||||||
value = submitter.try(field_name)
|
value = submitter.try(field_name)
|
||||||
|
expires_at = nil
|
||||||
|
|
||||||
if value_name
|
if value_name
|
||||||
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
|
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
|
||||||
@@ -114,7 +115,11 @@ module ReplaceEmailVariables
|
|||||||
|
|
||||||
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
|
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
if attachment
|
||||||
|
expires_at ||= Accounts.link_expires_at(Account.new(id: submission.account_id))
|
||||||
|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
value[field&.dig('uuid')]
|
value[field&.dig('uuid')]
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -239,7 +239,7 @@ module Submissions
|
|||||||
|
|
||||||
item['conditions'].each_with_object([]) do |condition, acc|
|
item['conditions'].each_with_object([]) do |condition, acc|
|
||||||
result =
|
result =
|
||||||
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
|
if fields_index.dig(condition['field_uuid'], 'submitter_uuid') == include_submitter_uuid
|
||||||
submitter_conditions_acc << condition if submitter_conditions_acc
|
submitter_conditions_acc << condition if submitter_conditions_acc
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
field['preferences'] = (field['preferences'] || {}).merge(attrs['preferences']) if attrs['preferences'].present?
|
field['preferences'] = (field['preferences'] || {}).merge(attrs['preferences']) if attrs['preferences'].present?
|
||||||
|
field['validation'] = (field['validation'] || {}).merge(attrs['validation']) if attrs['validation'].present?
|
||||||
|
|
||||||
return field if attrs['validation_pattern'].blank?
|
return field if attrs['validation_pattern'].blank?
|
||||||
|
|
||||||
@@ -331,8 +332,8 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_completed_attributes(submitter)
|
def assign_completed_attributes(submitter, with_verification: true)
|
||||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
submitter.values = Submitters::SubmitValues.merge_default_values(submitter, with_verification:)
|
||||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
|
|
||||||
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
||||||
|
|||||||
@@ -391,8 +391,8 @@ module Submissions
|
|||||||
composer.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
Array.wrap(value).map do |uuid|
|
Array.wrap(value).map do |uuid|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
link =
|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
link = r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||||
|
|
||||||
{ link:, text: "#{attachment.filename}\n", style: :link }
|
{ link:, text: "#{attachment.filename}\n", style: :link }
|
||||||
end,
|
end,
|
||||||
@@ -489,6 +489,10 @@ module Submissions
|
|||||||
padding: [5, 0, 0, 8],
|
padding: [5, 0, 0, 8],
|
||||||
position: :float, text_align: :left)
|
position: :float, text_align: :left)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def r
|
||||||
|
Rails.application.routes.url_helpers
|
||||||
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,8 +6,8 @@ module Submissions
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submissions, format: :csv)
|
def call(submissions, format: :csv, expires_at: nil)
|
||||||
rows = build_table_rows(submissions)
|
rows = build_table_rows(submissions, expires_at:)
|
||||||
|
|
||||||
if format.to_sym == :csv
|
if format.to_sym == :csv
|
||||||
rows_to_csv(rows)
|
rows_to_csv(rows)
|
||||||
@@ -40,7 +40,7 @@ module Submissions
|
|||||||
def rows_to_csv(rows)
|
def rows_to_csv(rows)
|
||||||
headers = build_headers(rows)
|
headers = build_headers(rows)
|
||||||
|
|
||||||
CSV.generate do |csv|
|
CSVSafe.generate do |csv|
|
||||||
csv << headers
|
csv << headers
|
||||||
|
|
||||||
rows.each do |row|
|
rows.each do |row|
|
||||||
@@ -57,7 +57,7 @@ module Submissions
|
|||||||
headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
|
headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_table_rows(submissions)
|
def build_table_rows(submissions, expires_at: nil)
|
||||||
submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
|
submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
|
||||||
.find_each.map do |submission|
|
.find_each.map do |submission|
|
||||||
submission_data = []
|
submission_data = []
|
||||||
@@ -69,7 +69,7 @@ module Submissions
|
|||||||
|
|
||||||
submission_data += build_submission_data(submitter, submitter_name, submitters_count)
|
submission_data += build_submission_data(submitter, submitter_name, submitters_count)
|
||||||
|
|
||||||
submission_data += submitter_formatted_fields(submitter).map do |field|
|
submission_data += submitter_formatted_fields(submitter, expires_at:).map do |field|
|
||||||
{
|
{
|
||||||
name: column_name(field[:name], submitter_name, submitters_count),
|
name: column_name(field[:name], submitter_name, submitters_count),
|
||||||
value: field[:value]
|
value: field[:value]
|
||||||
@@ -81,7 +81,7 @@ module Submissions
|
|||||||
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
||||||
{
|
{
|
||||||
name: "#{I18n.t('document')} #{index}",
|
name: "#{I18n.t('document')} #{index}",
|
||||||
value: ActiveStorage::Blob.proxy_url(attachment.blob)
|
value: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -123,7 +123,7 @@ module Submissions
|
|||||||
submitters_count > 1 ? "#{submitter_name} - #{name}" : name
|
submitters_count > 1 ? "#{submitter_name} - #{name}" : name
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_formatted_fields(submitter)
|
def submitter_formatted_fields(submitter, expires_at: nil)
|
||||||
fields = submitter.submission.template_fields || submitter.submission.template.fields
|
fields = submitter.submission.template_fields || submitter.submission.template.fields
|
||||||
|
|
||||||
template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
|
template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
|
||||||
@@ -142,11 +142,13 @@ module Submissions
|
|||||||
value =
|
value =
|
||||||
if template_field_type.in?(%w[image signature])
|
if template_field_type.in?(%w[image signature])
|
||||||
attachment = attachments_index[submitter_value]
|
attachment = attachments_index[submitter_value]
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||||
elsif template_field_type == 'file'
|
elsif template_field_type == 'file'
|
||||||
Array.wrap(submitter_value).compact_blank.filter_map do |e|
|
Array.wrap(submitter_value).compact_blank.filter_map do |e|
|
||||||
attachment = attachments_index[e]
|
attachment = attachments_index[e]
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
submitter_value
|
submitter_value
|
||||||
|
|||||||
@@ -201,13 +201,15 @@ module Submissions
|
|||||||
|
|
||||||
attachments_data_cache = {}
|
attachments_data_cache = {}
|
||||||
|
|
||||||
return pdfs_index if submitter.submission.template_fields.blank?
|
submission = submitter.submission
|
||||||
|
|
||||||
with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil?
|
return pdfs_index if submission.template_fields.blank?
|
||||||
|
|
||||||
|
with_headings = find_last_submitter(submission, submitter:).blank? if with_headings.nil?
|
||||||
|
|
||||||
locale = submitter.metadata.fetch('lang', account.locale)
|
locale = submitter.metadata.fetch('lang', account.locale)
|
||||||
|
|
||||||
submitter.submission.template_fields.each do |field|
|
submission.template_fields.each do |field|
|
||||||
next if field['type'] == 'heading' && !with_headings
|
next if field['type'] == 'heading' && !with_headings
|
||||||
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
|
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
|
||||||
|
|
||||||
@@ -224,7 +226,7 @@ module Submissions
|
|||||||
|
|
||||||
page[:Annots] ||= []
|
page[:Annots] ||= []
|
||||||
page[:Annots] = page[:Annots].try(:reject) do |e|
|
page[:Annots] = page[:Annots].try(:reject) do |e|
|
||||||
next if e.is_a?(Integer)
|
next if e.is_a?(Integer) || e.is_a?(Symbol)
|
||||||
|
|
||||||
e.present? && e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field')
|
e.present? && e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field')
|
||||||
end || page[:Annots]
|
end || page[:Annots]
|
||||||
@@ -476,7 +478,7 @@ module Submissions
|
|||||||
height_diff - (height_diff.zero? ? diff : 0)
|
height_diff - (height_diff.zero? ? diff : 0)
|
||||||
],
|
],
|
||||||
A: { Type: :Action, S: :URI,
|
A: { Type: :Action, S: :URI,
|
||||||
URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
URI: r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -747,11 +749,15 @@ module Submissions
|
|||||||
def maybe_rotate_pdf(pdf)
|
def maybe_rotate_pdf(pdf)
|
||||||
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
||||||
|
|
||||||
|
is_pages_rotated = pdf.pages.root[:Rotate].present? && pdf.pages.root[:Rotate] != 0
|
||||||
|
|
||||||
|
pdf.pages.root[:Rotate] = 0 if is_pages_rotated
|
||||||
|
|
||||||
is_rotated = pdf.pages.filter_map do |page|
|
is_rotated = pdf.pages.filter_map do |page|
|
||||||
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
||||||
end.present?
|
end.present?
|
||||||
|
|
||||||
return pdf unless is_rotated
|
return pdf if !is_rotated && !is_pages_rotated
|
||||||
|
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
@@ -873,7 +879,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def h
|
def r
|
||||||
Rails.application.routes.url_helpers
|
Rails.application.routes.url_helpers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ module Submissions
|
|||||||
module SerializeForApi
|
module SerializeForApi
|
||||||
SERIALIZE_PARAMS = {
|
SERIALIZE_PARAMS = {
|
||||||
only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at],
|
only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at],
|
||||||
methods: %i[audit_log_url combined_document_url],
|
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id] },
|
submitters: { only: %i[id] },
|
||||||
template: { only: %i[id name external_id created_at updated_at],
|
template: { only: %i[id name external_id created_at updated_at],
|
||||||
@@ -15,11 +14,13 @@ module Submissions
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true)
|
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
|
||||||
|
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
|
||||||
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
|
|
||||||
serialized_submitters = submitters.map do |submitter|
|
serialized_submitters = submitters.map do |submitter|
|
||||||
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:)
|
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:,
|
||||||
|
expires_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
json = submission.as_json(SERIALIZE_PARAMS)
|
json = submission.as_json(SERIALIZE_PARAMS)
|
||||||
@@ -30,8 +31,6 @@ module Submissions
|
|||||||
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
||||||
end
|
end
|
||||||
|
|
||||||
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params)
|
|
||||||
|
|
||||||
if submitters.all?(&:completed_at?)
|
if submitters.all?(&:completed_at?)
|
||||||
last_submitter = submitters.max_by(&:completed_at)
|
last_submitter = submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
@@ -39,10 +38,16 @@ module Submissions
|
|||||||
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json['audit_log_url'] = submission.audit_log_url(expires_at:)
|
||||||
|
json['combined_document_url'] = submission.combined_document_url(expires_at:)
|
||||||
|
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params, expires_at:)
|
||||||
|
|
||||||
json['status'] = 'completed'
|
json['status'] = 'completed'
|
||||||
json['completed_at'] = last_submitter.completed_at
|
json['completed_at'] = last_submitter.completed_at.as_json
|
||||||
else
|
else
|
||||||
json['documents'] = [] if with_documents
|
json['documents'] = [] if with_documents
|
||||||
|
json['audit_log_url'] = nil
|
||||||
|
json['combined_document_url'] = nil
|
||||||
json['status'] = build_status(submission, submitters)
|
json['status'] = build_status(submission, submitters)
|
||||||
json['completed_at'] = nil
|
json['completed_at'] = nil
|
||||||
end
|
end
|
||||||
@@ -60,7 +65,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_build_combined_url(submitters, submission, params)
|
def maybe_build_combined_url(submitters, submission, params, expires_at: nil)
|
||||||
return unless submitters.all?(&:completed_at?)
|
return unless submitters.all?(&:completed_at?)
|
||||||
|
|
||||||
attachment = submission.combined_document_attachment
|
attachment = submission.combined_document_attachment
|
||||||
@@ -71,7 +76,7 @@ module Submissions
|
|||||||
attachment = Submissions::GenerateCombinedAttachment.call(submitter)
|
attachment = Submissions::GenerateCombinedAttachment.call(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -67,25 +67,16 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_text_image(submitter)
|
def build_text_image(submitter)
|
||||||
time = I18n.l(submitter.completed_at.in_time_zone(submitter.submission.account.timezone),
|
if submitter.completed_at
|
||||||
format: :long,
|
time = I18n.l(submitter.completed_at.in_time_zone(submitter.submission.account.timezone),
|
||||||
locale: submitter.submission.account.locale)
|
format: :long,
|
||||||
|
locale: submitter.submission.account.locale)
|
||||||
|
|
||||||
timezone = TimeUtils.timezone_abbr(submitter.submission.account.timezone, submitter.completed_at)
|
timezone = TimeUtils.timezone_abbr(submitter.submission.account.timezone, submitter.completed_at)
|
||||||
|
end
|
||||||
|
|
||||||
name = if submitter.name.present? && submitter.email.present?
|
name = build_name(submitter)
|
||||||
"#{submitter.name} #{submitter.email}"
|
role = build_role(submitter)
|
||||||
else
|
|
||||||
submitter.name || submitter.email || submitter.phone
|
|
||||||
end
|
|
||||||
|
|
||||||
role = if submitter.submission.template_submitters.size > 1
|
|
||||||
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
|
|
||||||
|
|
||||||
"#{I18n.t(:role, locale: submitter.account.locale)}: #{item['name']}\n"
|
|
||||||
else
|
|
||||||
''
|
|
||||||
end
|
|
||||||
|
|
||||||
digitally_signed_by = I18n.t(:digitally_signed_by, locale: submitter.submission.account.locale)
|
digitally_signed_by = I18n.t(:digitally_signed_by, locale: submitter.submission.account.locale)
|
||||||
|
|
||||||
@@ -97,6 +88,24 @@ module Submitters
|
|||||||
Vips::Image.text(text, width: WIDTH, height: HEIGHT, wrap: :'word-char')
|
Vips::Image.text(text, width: WIDTH, height: HEIGHT, wrap: :'word-char')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_name(submitter)
|
||||||
|
if submitter.name.present? && submitter.email.present?
|
||||||
|
"#{submitter.name} #{submitter.email}"
|
||||||
|
else
|
||||||
|
submitter.name || submitter.email || submitter.phone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_role(submitter)
|
||||||
|
if submitter.submission.template_submitters.size > 1
|
||||||
|
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
|
||||||
|
|
||||||
|
"#{I18n.t(:role, locale: submitter.account.locale)}: #{item['name']}\n"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def load_logo(_submitter)
|
def load_logo(_submitter)
|
||||||
PdfIcons.stamp_logo_io
|
PdfIcons.stamp_logo_io
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module Submitters
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false,
|
def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false,
|
||||||
with_values: true, params: {})
|
with_values: true, params: {}, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [submitter],
|
records: [submitter],
|
||||||
associations: if with_documents
|
associations: if with_documents
|
||||||
@@ -24,15 +24,19 @@ module Submitters
|
|||||||
additional_attrs = {}
|
additional_attrs = {}
|
||||||
|
|
||||||
if params[:include].to_s.include?('fields')
|
if params[:include].to_s.include?('fields')
|
||||||
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter)
|
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter, expires_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
if with_template
|
if with_template
|
||||||
additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at])
|
additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter) if with_values
|
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter, expires_at:) if with_values
|
||||||
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter) if with_documents
|
|
||||||
|
if with_documents
|
||||||
|
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter, expires_at:)
|
||||||
|
end
|
||||||
|
|
||||||
additional_attrs['preferences'] = submitter.preferences.except('default_values')
|
additional_attrs['preferences'] = submitter.preferences.except('default_values')
|
||||||
additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events
|
additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,18 @@ module Submitters
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter)
|
def call(submitter, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [submitter],
|
records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
||||||
associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
|
||||||
).call
|
).call
|
||||||
|
|
||||||
values = build_values_array(submitter)
|
values = build_values_array(submitter, expires_at:)
|
||||||
documents = build_documents_array(submitter)
|
documents = build_documents_array(submitter, expires_at:)
|
||||||
|
|
||||||
submitter_name = (submitter.submission.template_submitters ||
|
submission = submitter.submission
|
||||||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
|
||||||
|
submitter_name = (submission.template_submitters ||
|
||||||
|
submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
decline_reason =
|
decline_reason =
|
||||||
submitter.declined_at? ? submitter.submission_events.find_by(event_type: :decline_form).data['reason'] : nil
|
submitter.declined_at? ? submitter.submission_events.find_by(event_type: :decline_form).data['reason'] : nil
|
||||||
@@ -31,21 +32,23 @@ module Submitters
|
|||||||
'preferences' => submitter.preferences.except('default_values'),
|
'preferences' => submitter.preferences.except('default_values'),
|
||||||
'values' => values,
|
'values' => values,
|
||||||
'documents' => documents,
|
'documents' => documents,
|
||||||
'audit_log_url' => submitter.submission.audit_log_url,
|
'audit_log_url' => submitter.submission.audit_log_url(expires_at:),
|
||||||
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
|
'submission_url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
|
||||||
**Docuseal.default_url_options),
|
'template' => submission.template.as_json(
|
||||||
'template' => submitter.submission.template.as_json(
|
|
||||||
only: %i[id name external_id created_at updated_at],
|
only: %i[id name external_id created_at updated_at],
|
||||||
methods: %i[folder_name]
|
methods: %i[folder_name]
|
||||||
),
|
),
|
||||||
'submission' => {
|
'submission' => {
|
||||||
**submitter.submission.slice(:id, :audit_log_url, :combined_document_url, :created_at),
|
'id' => submission.id,
|
||||||
status: build_submission_status(submitter.submission),
|
'audit_log_url' => submission.audit_log_url(expires_at:),
|
||||||
url: r.submissions_preview_url(submitter.submission.slug, **Docuseal.default_url_options)
|
'combined_document_url' => submission.combined_document_url(expires_at:),
|
||||||
|
'status' => build_submission_status(submission),
|
||||||
|
'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
|
||||||
|
'created_at' => submission.created_at.as_json
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_values_array(submitter)
|
def build_values_array(submitter, expires_at: nil)
|
||||||
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
|
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
|
||||||
attachments_index = submitter.attachments.index_by(&:uuid)
|
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||||
submitter_field_counters = Hash.new { 0 }
|
submitter_field_counters = Hash.new { 0 }
|
||||||
@@ -61,13 +64,13 @@ module Submitters
|
|||||||
|
|
||||||
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
|
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
|
||||||
|
|
||||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
|
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
|
||||||
|
|
||||||
{ field: field_name, value: }
|
{ 'field' => field_name, 'value' => value }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_fields_array(submitter)
|
def build_fields_array(submitter, expires_at: nil)
|
||||||
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
|
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
|
||||||
attachments_index = submitter.attachments.index_by(&:uuid)
|
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||||
submitter_field_counters = Hash.new { 0 }
|
submitter_field_counters = Hash.new { 0 }
|
||||||
@@ -83,9 +86,9 @@ module Submitters
|
|||||||
|
|
||||||
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
|
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
|
||||||
|
|
||||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
|
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
|
||||||
|
|
||||||
{ name: field_name, uuid: field['uuid'], value:, readonly: field['readonly'] == true }
|
{ 'name' => field_name, 'uuid' => field['uuid'], 'value' => value, 'readonly' => field['readonly'] == true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -101,26 +104,26 @@ module Submitters
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_documents_array(submitter)
|
def build_documents_array(submitter, expires_at: nil)
|
||||||
submitter.documents.map do |attachment|
|
submitter.documents.map do |attachment|
|
||||||
{ name: attachment.filename.base, url: rails_storage_proxy_url(attachment) }
|
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment, expires_at:) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_field_value(field, value, attachments_index)
|
def fetch_field_value(field, value, attachments_index, expires_at: nil)
|
||||||
if field['type'].in?(%w[image signature initials stamp payment])
|
if field['type'].in?(%w[image signature initials stamp payment])
|
||||||
rails_storage_proxy_url(attachments_index[value])
|
rails_storage_proxy_url(attachments_index[value], expires_at:)
|
||||||
elsif field['type'] == 'file'
|
elsif field['type'] == 'file'
|
||||||
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) }
|
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }
|
||||||
else
|
else
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rails_storage_proxy_url(attachment)
|
def rails_storage_proxy_url(attachment, expires_at: nil)
|
||||||
return if attachment.blank?
|
return if attachment.blank?
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def r
|
def r
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_default_values(submitter)
|
def merge_default_values(submitter, with_verification: true)
|
||||||
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ module Submitters
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
if field['type'] == 'verification'
|
if field['type'] == 'verification' && with_verification
|
||||||
acc[field['uuid']] =
|
acc[field['uuid']] =
|
||||||
if submitter.submission_events.exists?(event_type: :complete_verification)
|
if submitter.submission_events.exists?(event_type: :complete_verification)
|
||||||
I18n.t(:verified, locale: :en)
|
I18n.t(:verified, locale: :en)
|
||||||
@@ -189,6 +189,8 @@ module Submitters
|
|||||||
|
|
||||||
next if formula.blank?
|
next if formula.blank?
|
||||||
|
|
||||||
|
formula = normalize_formula(formula, submitter.submission)
|
||||||
|
|
||||||
submission_values ||=
|
submission_values ||=
|
||||||
if submitter.submission.template_submitters.size > 1
|
if submitter.submission.template_submitters.size > 1
|
||||||
merge_submitters_values(submitter)
|
merge_submitters_values(submitter)
|
||||||
@@ -202,6 +204,20 @@ module Submitters
|
|||||||
computed_values.compact_blank
|
computed_values.compact_blank
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_formula(formula, submission, depth = 0)
|
||||||
|
raise ValidationError, 'Formula infinite loop' if depth > 10
|
||||||
|
|
||||||
|
formula.gsub(/{{(.*?)}}/) do |match|
|
||||||
|
uuid = Regexp.last_match(1)
|
||||||
|
|
||||||
|
if (nested_formula = submission.fields_uuid_index.dig(uuid, 'preferences', 'formula').presence)
|
||||||
|
"(#{normalize_formula(nested_formula, submission, depth + 1)})"
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def calculate_formula_value(_formula, _values)
|
def calculate_formula_value(_formula, _values)
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
@@ -321,7 +337,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
def replace_default_variables(value, attrs, submission, with_time: false)
|
||||||
return value if value.in?([true, false]) || value.is_a?(Numeric)
|
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
|
||||||
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ module Templates
|
|||||||
hash
|
hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_assign_access(_template)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def search(current_user, templates, keyword)
|
def search(current_user, templates, keyword)
|
||||||
if Docuseal.fulltext_search?
|
if Docuseal.fulltext_search?
|
||||||
fulltext_search(current_user, templates, keyword)
|
fulltext_search(current_user, templates, keyword)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module Templates
|
|||||||
pdf.pages.flat_map.with_index do |page, index|
|
pdf.pages.flat_map.with_index do |page, index|
|
||||||
(page[:Annots] || []).filter_map do |annot|
|
(page[:Annots] || []).filter_map do |annot|
|
||||||
next if annot.blank?
|
next if annot.blank?
|
||||||
|
next if annot.is_a?(Integer) || annot.is_a?(Symbol)
|
||||||
next if annot[:A].blank? || annot[:A][:URI].blank?
|
next if annot[:A].blank? || annot[:A][:URI].blank?
|
||||||
next unless annot[:Subtype] == :Link
|
next unless annot[:Subtype] == :Link
|
||||||
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ module Templates
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template, schema_documents = template.schema_documents.preload(:blob), preview_image_attachments = nil)
|
def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
|
||||||
|
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
|
||||||
json = template.as_json(SERIALIZE_PARAMS)
|
json = template.as_json(SERIALIZE_PARAMS)
|
||||||
|
|
||||||
preview_image_attachments ||=
|
preview_image_attachments ||=
|
||||||
@@ -25,7 +26,7 @@ module Templates
|
|||||||
name: :preview_images)
|
name: :preview_images)
|
||||||
.preload(:blob)
|
.preload(:blob)
|
||||||
|
|
||||||
json[:documents] = template.schema.filter_map do |item|
|
json['documents'] = template.schema.filter_map do |item|
|
||||||
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
|
||||||
unless attachment
|
unless attachment
|
||||||
@@ -38,11 +39,11 @@ module Templates
|
|||||||
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
||||||
|
|
||||||
{
|
{
|
||||||
id: attachment.id,
|
'id' => attachment.id,
|
||||||
uuid: attachment.uuid,
|
'uuid' => attachment.uuid,
|
||||||
url: ActiveStorage::Blob.proxy_url(attachment.blob),
|
'url' => ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:),
|
||||||
preview_image_url: first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob),
|
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob, expires_at:),
|
||||||
filename: attachment.filename
|
'filename' => attachment.filename
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -6,8 +6,9 @@
|
|||||||
"@babel/plugin-transform-runtime": "7.21.4",
|
"@babel/plugin-transform-runtime": "7.21.4",
|
||||||
"@babel/preset-env": "7.21.5",
|
"@babel/preset-env": "7.21.5",
|
||||||
"@babel/runtime": "7.21.5",
|
"@babel/runtime": "7.21.5",
|
||||||
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@eid-easy/eideasy-widget": "^2.163.4",
|
"@eid-easy/eideasy-widget": "^2.171.0",
|
||||||
"@github/catalyst": "^2.0.0-beta",
|
"@github/catalyst": "^2.0.0-beta",
|
||||||
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
|
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
|
||||||
"@hotwired/turbo-rails": "^7.3.0",
|
"@hotwired/turbo-rails": "^7.3.0",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ describe 'Templates API' do
|
|||||||
let(:folder) { create(:template_folder, account:) }
|
let(:folder) { create(:template_folder, account:) }
|
||||||
let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
|
let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Accounts).to receive(:link_expires_at).and_return(Accounts::LINK_EXPIRES_AT)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET /api/templates' do
|
describe 'GET /api/templates' do
|
||||||
it 'returns a list of templates' do
|
it 'returns a list of templates' do
|
||||||
templates = [
|
templates = [
|
||||||
@@ -211,8 +215,8 @@ describe 'Templates API' do
|
|||||||
{
|
{
|
||||||
id: template.documents.first.id,
|
id: template.documents.first.id,
|
||||||
uuid: template.documents.first.uuid,
|
uuid: template.documents.first.uuid,
|
||||||
url: ActiveStorage::Blob.proxy_url(attachment.blob),
|
url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: Accounts::LINK_EXPIRES_AT),
|
||||||
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob),
|
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob, expires_at: Accounts::LINK_EXPIRES_AT),
|
||||||
filename: 'sample-document.pdf'
|
filename: 'sample-document.pdf'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -235,7 +239,7 @@ describe 'Templates API' do
|
|||||||
folder_name: folder.name,
|
folder_name: folder.name,
|
||||||
source: 'native',
|
source: 'native',
|
||||||
external_id: template.external_id,
|
external_id: template.external_id,
|
||||||
application_key: template.external_id # Backward compatibility
|
application_key: template.external_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,26 @@ RSpec.describe 'API Settings' do
|
|||||||
token = user.access_token.token
|
token = user.access_token.token
|
||||||
expect(page).to have_field('X-Auth-Token', with: token.sub(token[5..], '*' * token[5..].size))
|
expect(page).to have_field('X-Auth-Token', with: token.sub(token[5..], '*' * token[5..].size))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'reveals API key with correct password' do
|
||||||
|
find('#api_key').click
|
||||||
|
|
||||||
|
within('.modal') do
|
||||||
|
fill_in 'password', with: user.password
|
||||||
|
click_button 'Submit'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_field('X-Auth-Token', with: user.access_token.token)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows error with incorrect password' do
|
||||||
|
find('#api_key').click
|
||||||
|
|
||||||
|
within('.modal') do
|
||||||
|
fill_in 'password', with: 'wrong_password'
|
||||||
|
click_button 'Submit'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content('Wrong password')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -5,6 +5,9 @@ RSpec.describe 'Profile Settings' do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
||||||
|
allow(Accounts).to receive(:can_send_emails?).and_return(true)
|
||||||
|
|
||||||
visit settings_profile_index_path
|
visit settings_profile_index_path
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -16,7 +19,6 @@ RSpec.describe 'Profile Settings' do
|
|||||||
|
|
||||||
expect(page).to have_content('Change Password')
|
expect(page).to have_content('Change Password')
|
||||||
expect(page).to have_field('user[password]')
|
expect(page).to have_field('user[password]')
|
||||||
expect(page).to have_field('user[password_confirmation]')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when changes contact information' do
|
context 'when changes contact information' do
|
||||||
@@ -47,6 +49,7 @@ RSpec.describe 'Profile Settings' do
|
|||||||
it 'updates password' do
|
it 'updates password' do
|
||||||
fill_in 'New password', with: 'newpassword'
|
fill_in 'New password', with: 'newpassword'
|
||||||
fill_in 'Confirm new password', with: 'newpassword'
|
fill_in 'Confirm new password', with: 'newpassword'
|
||||||
|
fill_in 'Current password', with: 'password'
|
||||||
|
|
||||||
all(:button, 'Update')[1].click
|
all(:button, 'Update')[1].click
|
||||||
|
|
||||||
@@ -56,10 +59,51 @@ RSpec.describe 'Profile Settings' do
|
|||||||
it 'does not update if password confirmation does not match' do
|
it 'does not update if password confirmation does not match' do
|
||||||
fill_in 'New password', with: 'newpassword'
|
fill_in 'New password', with: 'newpassword'
|
||||||
fill_in 'Confirm new password', with: 'newpassword1'
|
fill_in 'Confirm new password', with: 'newpassword1'
|
||||||
|
fill_in 'Current password', with: 'password'
|
||||||
|
|
||||||
all(:button, 'Update')[1].click
|
all(:button, 'Update')[1].click
|
||||||
|
|
||||||
expect(page).to have_content("Password confirmation doesn't match Password")
|
expect(page).to have_content("Password confirmation doesn't match Password")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not update if current password is incorrect' do
|
||||||
|
fill_in 'New password', with: 'newpassword'
|
||||||
|
fill_in 'Confirm new password', with: 'newpassword'
|
||||||
|
fill_in 'Current password', with: 'wrongpassword'
|
||||||
|
|
||||||
|
all(:button, 'Update')[1].click
|
||||||
|
|
||||||
|
expect(page).to have_content('Current password is invalid')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets password and signs in with new password', sidekiq: :inline do
|
||||||
|
fill_in 'New password', with: 'newpassword'
|
||||||
|
accept_confirm('Are you sure?') do
|
||||||
|
find('label', text: 'Click here').click
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content('An email with password reset instructions has been sent.')
|
||||||
|
|
||||||
|
email = ActionMailer::Base.deliveries.last
|
||||||
|
reset_password_url = email.body
|
||||||
|
.encoded[/href="([^"]+)"/, 1]
|
||||||
|
.sub(%r{https?://(.*?)/}, "#{Capybara.current_session.server.base_url}/")
|
||||||
|
|
||||||
|
visit reset_password_url
|
||||||
|
|
||||||
|
fill_in 'New password', with: 'new_strong_password'
|
||||||
|
fill_in 'Confirm new password', with: 'new_strong_password'
|
||||||
|
click_button 'Change my password'
|
||||||
|
|
||||||
|
expect(page).to have_content('Your password has been changed successfully. You are now signed in.')
|
||||||
|
|
||||||
|
visit new_user_session_path
|
||||||
|
|
||||||
|
fill_in 'Email', with: user.email
|
||||||
|
fill_in 'Password', with: 'new_strong_password'
|
||||||
|
click_button 'Sign In'
|
||||||
|
|
||||||
|
expect(page).to have_content('Signed in successfully')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -115,7 +115,6 @@ RSpec.describe 'Team Settings' do
|
|||||||
fill_in 'First name', with: 'Adam'
|
fill_in 'First name', with: 'Adam'
|
||||||
fill_in 'Last name', with: 'Meier'
|
fill_in 'Last name', with: 'Meier'
|
||||||
fill_in 'Email', with: 'adam.meier@example.com'
|
fill_in 'Email', with: 'adam.meier@example.com'
|
||||||
fill_in 'Password', with: 'new_password'
|
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
click_button 'Submit'
|
click_button 'Submit'
|
||||||
|
|||||||
@@ -1030,6 +1030,11 @@
|
|||||||
"@babel/helper-string-parser" "^7.25.9"
|
"@babel/helper-string-parser" "^7.25.9"
|
||||||
"@babel/helper-validator-identifier" "^7.25.9"
|
"@babel/helper-validator-identifier" "^7.25.9"
|
||||||
|
|
||||||
|
"@braintree/sanitize-url@^7.1.1":
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e"
|
||||||
|
integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==
|
||||||
|
|
||||||
"@codemirror/autocomplete@^6.0.0":
|
"@codemirror/autocomplete@^6.0.0":
|
||||||
version "6.18.6"
|
version "6.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb"
|
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb"
|
||||||
@@ -1141,22 +1146,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||||
|
|
||||||
"@eid-easy/eideasy-browser-client@2.127.0":
|
"@eid-easy/eideasy-browser-client@2.135.0":
|
||||||
version "2.127.0"
|
version "2.135.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.127.0.tgz#d1b09e3634e94d7e32f593209a63f962fd326731"
|
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.135.0.tgz#ade3cd72210aba2bdef99ec8ffdabbefdd5e6480"
|
||||||
integrity sha512-2iosVqkF1C0hQWc6TVBe1SK7b/z8ZnvHnulGXfQf6VrrpEJMgnzK95h6LFDqDQyetfIwEGGoeOiUij2hYA1ZPA==
|
integrity sha512-QaFMxdZaEzN/MdQ/ZhJBDN2v+6XJL0l9vK3zDgJFDyaUYvMvWcjFpHvtMiQEOivTD3sB4cM7/sJdwzA01APvsw==
|
||||||
dependencies:
|
dependencies:
|
||||||
axios "1.8.2"
|
axios "1.8.2"
|
||||||
jsencrypt "3.2.1"
|
jsencrypt "3.2.1"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
serialize-error "^9.1.1"
|
serialize-error "^9.1.1"
|
||||||
|
|
||||||
"@eid-easy/eideasy-widget@^2.163.4":
|
"@eid-easy/eideasy-widget@^2.171.0":
|
||||||
version "2.163.4"
|
version "2.171.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.163.4.tgz#4a8ada2c61f032527a08f9f8d9d1adf3fa7b5334"
|
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.171.0.tgz#2072e50a187ef36dd89d0bc5102d5996657ea180"
|
||||||
integrity sha512-7OQ1bm+KSG99wUI6szXT25Icq67CEQRK38VGj8fynW0ctqJTiFXRop7dqgR9JXLlJ1s1Z++El7igYxph7Dq5Aw==
|
integrity sha512-zDQgq2JhGR+omomU+4PU9vyWXJbMutklWCzZ74YVXzP1LmV0SD/X9Vy/bqofSZ1iRwgT+A+lCqLSmkoztaPC3A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eid-easy/eideasy-browser-client" "2.127.0"
|
"@eid-easy/eideasy-browser-client" "2.135.0"
|
||||||
core-js "^3.8.3"
|
core-js "^3.8.3"
|
||||||
i18n-iso-countries "^6.7.0"
|
i18n-iso-countries "^6.7.0"
|
||||||
lodash.defaultsdeep "^4.6.1"
|
lodash.defaultsdeep "^4.6.1"
|
||||||
|
|||||||
Reference in new issue
Block a user