Compare commits

...

129 Commits

Author SHA1 Message Date
Pete Matsyburka 055f7e2bc1 Merge from docusealco/wip 2025-06-15 23:53:35 +03:00
Pete Matsyburka dae3b4b6b5 add completed at index 2025-06-15 12:26:11 +03:00
Pete Matsyburka f2888a08dc fix query 2025-06-14 08:53:45 +03:00
Pete Matsyburka 95627c5963 add audit timezone 2025-06-13 22:37:04 +03:00
Pete Matsyburka 4bb3721db0 add submitter timezone 2025-06-13 19:15:05 +03:00
Pete Matsyburka b12111383b fix update oneoff 2025-06-11 23:39:19 +03:00
Pete Matsyburka 263c0ff939 fix migration 2025-06-11 18:50:01 +03:00
Pete Matsyburka efc8ba6b0a fix merge 2025-06-11 17:55:10 +03:00
Pete Matsyburka 4c530b3f88 fix query 2025-06-11 12:10:24 +03:00
Pete Matsyburka f6358c9d47 fix brakeman 2025-06-11 09:32:43 +03:00
Pete Matsyburka b3baea2f6f optimize query 2025-06-11 09:28:10 +03:00
Pete Matsyburka c0594a8b74 fix brakeman 2025-06-11 09:16:57 +03:00
Pete Matsyburka 1403401b03 permit field required 2025-06-10 11:26:12 +03:00
Pete Matsyburka 700184f776 add ngram 2025-06-10 09:19:13 +03:00
Pete Matsyburka b6a1a3ee4d fix archived 2025-06-09 21:46:26 +03:00
Pete Matsyburka 8cbf5802b5 fix signature upload 2025-06-09 17:06:20 +03:00
Pete Matsyburka ab9706afef fix template fields 2025-06-09 14:20:17 +03:00
Pete Matsyburka f07f9d8cf9 fix oneoff edit 2025-06-09 14:20:17 +03:00
Pete Matsyburka 591caecb92 fix redirect 2025-06-09 14:20:17 +03:00
Pete Matsyburka ef4c2b7213 update gem 2025-06-08 16:54:59 +03:00
Pete Matsyburka 18999255a9 remove constant 2025-06-08 13:34:41 +03:00
Pete Matsyburka 0a2a1de6b9 brakeman ignore 2025-06-07 21:28:01 +03:00
Pete Matsyburka 1b857b6eb3 fix preview 2025-06-07 21:12:37 +03:00
Pete Matsyburka e5e6b11c95 fix image name 2025-06-07 16:20:57 +03:00
Pete Matsyburka 3ec3f58d55 fix mysql migration 2025-06-07 09:55:45 +03:00
Pete Matsyburka 3bfce862ec fix reindex 2025-06-07 09:52:26 +03:00
Pete Matsyburka c01b67f7ec index template 2025-06-06 23:14:17 +03:00
Pete Matsyburka ea0efe9314 fix form cells 2025-06-06 22:10:00 +03:00
Pete Matsyburka 28b583691f partial download toggle 2025-06-06 16:28:53 +03:00
Pete Matsyburka 33811945be fulltext search 2025-06-06 16:03:16 +03:00
Pete Matsyburka 9d1860f038 fix specs 2025-06-06 08:47:56 +03:00
Pete Matsyburka f03166b4ea one-off submission 2025-06-06 08:47:56 +03:00
Pete Matsyburka c59d948d41 add stamp to all pages 2025-06-04 08:44:55 +03:00
Pete Matsyburka 57d63dd2f2 fix empty option condition 2025-06-03 22:29:14 +03:00
Alex Turchyn c88715daec Merge from docusealco/wip 2025-06-03 12:22:34 +03:00
Pete Matsyburka 0375dad7c9 smaller audit initial 2025-06-03 10:13:49 +03:00
Pete Matsyburka de3f83db96 use metadata lang 2025-06-03 09:52:46 +03:00
Pete Matsyburka a7bb96464c completed action shared link 2025-06-01 22:42:44 +03:00
Pete Matsyburka 1d48fe9621 fix spec 2025-06-01 15:51:43 +03:00
Pete Matsyburka 9544bb3d76 Revert "shared link true default"
This reverts commit e41fe5fbca.
2025-06-01 15:23:20 +03:00
Pete Matsyburka 4e94664008 refactor start form 2025-06-01 15:23:19 +03:00
Pete Matsyburka e41fe5fbca shared link true default 2025-06-01 13:21:11 +03:00
Pete Matsyburka 24d713dec6 resubmit shared template 2025-06-01 12:27:36 +03:00
Pete Matsyburka f464e1c46a add log 2025-06-01 12:22:16 +03:00
Pete Matsyburka 85d52d1f28 refactor policy 2025-06-01 11:38:08 +03:00
Alex Turchyn e77b5fa2c9 add optional shared link to templates 2025-06-01 11:07:52 +03:00
Alex Turchyn c91b4a765b add Privacy Policy and eSignature Disclosure links 2025-06-01 09:20:42 +03:00
Pete Matsyburka 2a031c14c8 fix js error 2025-05-30 16:58:20 +03:00
Pete Matsyburka f7dfc0b2ed add email event date index 2025-05-30 11:12:33 +03:00
Pete Matsyburka 3b894f5281 fix 2025-05-30 10:36:08 +03:00
Alex Turchyn 9645332e48 add countless pagination 2025-05-30 09:12:42 +03:00
Pete Matsyburka 5670390b37 fix signature reason 2025-05-29 17:31:09 +03:00
Pete Matsyburka 260bd6d5d5 fix signature field size 2025-05-29 17:24:03 +03:00
Alex Turchyn fa0733f480 improve user email validation 2025-05-29 14:24:17 +03:00
Pete Matsyburka 8b9678a434 fix heif 2025-05-28 23:01:34 +03:00
Pete Matsyburka 43669bf095 do not prefill optional signature 2025-05-28 21:51:17 +03:00
Pete Matsyburka 2ef8df3f0c optimize query 2025-05-28 10:23:36 +03:00
Alex Turchyn 1c9d15aa7e add translations for pagination 2025-05-28 10:06:06 +03:00
Alex Turchyn 9fcd8955d3 add 'field-area-active-label' theme class 2025-05-27 21:10:58 +03:00
Pete Matsyburka 5e44ba49b8 refactor 2025-05-26 22:26:14 +03:00
Pete Matsyburka ac1f90e711 do not email smtp settings 2025-05-26 14:46:33 +03:00
Alex Turchyn c6eae66127 Merge from docusealco/wip 2025-05-26 13:26:19 +03:00
Pete Matsyburka 08aa902e07 set from user id 2025-05-26 11:58:47 +03:00
Pete Matsyburka 1690337699 fix pdf image result 2025-05-26 10:11:01 +03:00
Pete Matsyburka f78cf8cc6e pdfium 2025-05-26 10:01:27 +03:00
Pete Matsyburka 343118d00d fix bmp 2025-05-24 11:54:18 +03:00
Alex Turchyn a3bd37c9a2 Improve error message when does not match in completed form 2025-05-24 11:25:49 +03:00
Alex Turchyn 8bc13a5900 fix german translations 2025-05-23 10:52:47 +03:00
Pete Matsyburka 21138c1d02 remove log 2025-05-22 15:41:19 +03:00
Alex Turchyn 38674a9fc9 fix accidental checkbox addition 2025-05-21 22:18:25 +03:00
Pete Matsyburka a7ed0b86a4 dot rspec 2025-05-21 10:20:43 +03:00
Pete Matsyburka 91a96f06c8 remove account config action 2025-05-20 22:50:43 +03:00
Alex Turchyn 23631b75f2 add template_accesses factory 2025-05-20 19:36:45 +03:00
Pete Matsyburka 0fb9c2bbf6 fix gmail normalize 2025-05-20 19:32:23 +03:00
Pete Matsyburka 14fa8fc8e7 fix expired 2025-05-20 11:18:08 +03:00
Pete Matsyburka 047c192988 fix typo 2025-05-20 09:49:17 +03:00
Pete Matsyburka bfa244c8fa add ico and bmp support 2025-05-20 09:49:16 +03:00
Alex Turchyn 019222ae40 Merge from docusealco/wip 2025-05-19 13:07:42 +03:00
Pete Matsyburka 8a10852fe2 docs 2025-05-18 17:38:51 +03:00
Pete Matsyburka 01232aed5a add account access 2025-05-18 15:16:53 +03:00
Alex Turchyn 82f3ff4824 fix narrow signatures 2025-05-18 09:44:55 +03:00
Pete Matsyburka 7be3f5a931 fix android 2025-05-16 18:28:55 +03:00
Pete Matsyburka 9d013a7384 fix email normalize 2025-05-15 22:16:06 +03:00
Alex Turchyn 0c71d7bff8 fix EU server selection note 2025-05-15 18:05:07 +03:00
Pete Matsyburka fdd030652e adjust border 2025-05-15 14:39:25 +03:00
Pete Matsyburka f2bf1461f0 adjust selector 2025-05-15 13:50:02 +03:00
Alex Turchyn fc6f796f30 adjust server selector 2025-05-15 13:16:43 +03:00
Alex Turchyn 5f44806287 adjust dropoze border style 2025-05-15 13:16:40 +03:00
Pete Matsyburka 318b65af18 fix i18n 2025-05-14 21:28:52 +03:00
Pete Matsyburka 91b182b04a adjust email validate 2025-05-14 20:40:59 +03:00
Pete Matsyburka 15b886d4ff update reminder durations 2025-05-14 20:22:34 +03:00
Pete Matsyburka 0122a237e3 remove request helper 2025-05-14 13:21:57 +03:00
Alex Turchyn 2dca7d8a8e add builder 'replace' button specs 2025-05-14 13:06:03 +03:00
Pete Matsyburka 87265e2f22 fix prefill same name field 2025-05-14 13:04:35 +03:00
Pete Matsyburka 3166defa1a add submitter value var 2025-05-13 12:27:27 +03:00
Pete Matsyburka 93ba17f42e expire at webhook 2025-05-12 18:23:17 +03:00
Pete Matsyburka 751be1dae5 fix template filter 2025-05-12 15:38:13 +03:00
Pete Matsyburka 3f8d2831e2 allow double dash email 2025-05-12 13:22:11 +03:00
Alex Turchyn 4d8942803e Merge from docusealco/wip 2025-05-12 12:31:27 +03:00
Pete Matsyburka 46d9bb2c87 fix optimize query 2025-05-12 11:17:26 +03:00
Pete Matsyburka 34433c0575 adjust conditions 2025-05-12 11:05:48 +03:00
Pete Matsyburka 544306eac4 fix replace 2025-05-11 18:02:24 +03:00
Pete Matsyburka 0eeebfd3cf clear cache 2025-05-10 20:03:35 +03:00
Pete Matsyburka 050224a10d fix refactor expire at 2025-05-10 17:39:53 +03:00
Alex Turchyn 7b6fc7d06f add expiration duration to template preferences 2025-05-10 12:12:15 +03:00
Pete Matsyburka 7fb148c85c drag folders 2025-05-10 12:11:57 +03:00
Alex Turchyn dffa59fecd add translations 2025-05-09 22:20:20 +03:00
Pete Matsyburka 24fe2a1192 fix refactor dropzone 2025-05-09 22:12:20 +03:00
Alex Turchyn 97b8ac4444 add builder dashboard dropzone 2025-05-09 21:35:31 +03:00
Pete Matsyburka e34a763dd9 fix field id 2025-05-09 10:04:22 +03:00
Pete Matsyburka 298b1d3e1e update gem 2025-05-08 22:41:25 +03:00
Pete Matsyburka d9978519cc adjust qes link 2025-05-08 12:32:10 +03:00
Pete Matsyburka b4a68cf79e adjust field types prop 2025-05-08 12:22:18 +03:00
Pete Matsyburka 5663e60f92 fix log 2025-05-06 22:18:53 +03:00
Pete Matsyburka 19e8835809 fix timestamp path 2025-05-06 18:44:29 +03:00
Pete Matsyburka 330fae927f skip bounce from audit 2025-05-06 16:48:50 +03:00
Pete Matsyburka c5a5212947 disable archived template actions 2025-05-06 15:50:58 +03:00
Pete Matsyburka 21d81e38bf add email events 2025-05-06 15:10:02 +03:00
Pete Matsyburka 5b1a4ebffd improve log 2025-05-03 16:30:11 +03:00
Pete Matsyburka 5f5dabd0e5 advanced formats 2025-05-02 19:17:19 +03:00
Pete Matsyburka ff46993de6 fix spec 2025-05-02 12:54:00 +03:00
Pete Matsyburka 7676db2196 hex colors 2025-05-02 12:36:33 +03:00
Alex Turchyn 3ef0d28ffe adjust app tour 2025-05-02 10:27:57 +03:00
Pete Matsyburka 231177f057 add log 2025-05-01 00:14:04 +03:00
Pete Matsyburka e5f7739952 use sha 256 for tsa 2025-04-30 23:48:03 +03:00
Pete Matsyburka 4a8e2a82e5 tsa fallback 2025-04-30 22:58:50 +03:00
Pete Matsyburka 6efe8027f1 disable multiple sign 2025-04-30 16:54:56 +03:00
Pete Matsyburka 96bb4eb6e5 fix standalone radio 2025-04-29 20:15:32 +03:00
Pete Matsyburka 38e68814cf fix color 2025-04-29 18:02:19 +03:00
242 changed files with 39150 additions and 864 deletions
+4 -1
View File
@@ -157,7 +157,10 @@ jobs:
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
sudo apt-get install -y libvips
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
rm -f pdfium-linux.tgz
- name: Run
env:
RAILS_ENV: test
+1
View File
@@ -0,0 +1 @@
--require rails_helper
+16 -5
View File
@@ -1,8 +1,17 @@
FROM ruby:3.4.2-alpine AS fonts
FROM ruby:3.4.2-alpine AS download
WORKDIR /fonts
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN apk --no-cache add fontforge wget && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
@@ -41,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
@@ -70,8 +79,10 @@ COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
+1 -1
View File
@@ -387,7 +387,7 @@ GEM
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.12)
rack (3.1.16)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
+12 -2
View File
@@ -1,8 +1,10 @@
# frozen_string_literal: true
class AccountConfigsController < ApplicationController
before_action :load_account_config
authorize_resource :account_config
before_action :load_account_config, only: :create
authorize_resource :account_config, only: :create
load_and_authorize_resource :account_config, only: :destroy
ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE,
@@ -30,6 +32,14 @@ class AccountConfigsController < ApplicationController
head :ok
end
def destroy
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
@account_config.destroy!
redirect_back(fallback_location: root_path)
end
private
def load_account_config
@@ -21,7 +21,11 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
authorization_check!(blob) if exp.blank?
attachment = blob.attachments.take
@record = attachment.record
authorization_check!(attachment) if exp.blank?
if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range']
@@ -37,9 +41,7 @@ module Api
private
def authorization_check!(blob)
attachment = blob.attachments.take
def authorization_check!(attachment)
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
@@ -10,7 +10,7 @@ module Api
end
def index
submissions = Submissions.search(@submissions, params[:q])
submissions = Submissions.search(current_user, @submissions, params[:q])
submissions = filter_submissions(submissions, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters,
@@ -80,6 +80,8 @@ module Api
end
end
SearchEntries.enqueue_reindex(submissions)
render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
DownloadUtils::UnableToDownload => e
@@ -183,14 +185,14 @@ module Api
def submissions_params
permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:expire_at,
:expire_at, :name,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message,
:readonly, :required, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]]
}
]
@@ -6,10 +6,10 @@ module Api
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
end
render json: {}
@@ -6,15 +6,15 @@ module Api
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
submitter.opened_at = Time.current
submitter.save
@submitter.opened_at = Time.current
@submitter.save
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
'webhook_url_id' => webhook_url.id)
end
+15 -13
View File
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :submitter
def index
submitters = Submitters.search(@submitters, params[:q])
submitters = Submitters.search(current_user, @submitters, params[:q])
submitters = filter_submitters(submitters, params)
@@ -37,14 +37,16 @@ module Api
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
end
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
submission = @submitter.submission
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
normalized_params, new_attachments =
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
for_submitter: @submitter)
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
submitter_params.merge(role:),
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
for_submitter: @submitter
)
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
[normalized_params],
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params)
@@ -65,10 +67,10 @@ module Api
Submitters.send_signature_requests([@submitter])
end
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
with_urls: true,
with_events: false,
params:)
SearchEntries.enqueue_reindex(@submitter)
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
with_events: false, params:)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
@@ -82,7 +84,7 @@ module Api
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [[:name, :uuid, :default_value, :value,
fields: [[:name, :uuid, :default_value, :value, :required,
:readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }]] }
)
@@ -162,7 +164,7 @@ module Api
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filder_by_completed_at(submitters, params)
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :template
def create
authorize!(:manage, @template)
authorize!(:create, @template)
ActiveRecord::Associations::Preloader.new(
records: [@template],
@@ -21,17 +21,20 @@ module Api
)
cloned_template.source = :api
cloned_template.save!
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
original_template: @template,
documents: params[:documents])
cloned_template.save!
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id)
end
SearchEntries.enqueue_reindex(cloned_template)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end
end
+4 -1
View File
@@ -65,6 +65,8 @@ module Api
@template.update!(template_params)
SearchEntries.enqueue_reindex(@template)
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
'webhook_url_id' => webhook_url.id)
@@ -86,7 +88,7 @@ module Api
private
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = Templates.search(current_user, templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
@@ -100,6 +102,7 @@ module Api
permitted_params = [
:name,
:external_id,
:shared_link,
{
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
@@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base
request.session[:impersonated_user_id] = user.uuid
end
def pagy_auto(collection, **keyword_args)
if current_ability.can?(:manage, :countless)
pagy_countless(collection, **keyword_args)
else
pagy(collection, **keyword_args)
end
end
private
def with_locale(&)
@@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create
if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
unless Docuseal.multitenant?
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
end
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
@@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API
return head :not_found unless attachment
@template = attachment.record
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
@@ -10,16 +10,17 @@ class SendSubmissionEmailController < ApplicationController
SEND_DURATION = 30.minutes
def create
@submitter =
if params[:template_slug]
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
template: { slug: params[:template_slug] })
elsif params[:submission_slug]
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
submission: { slug: params[:submission_slug] })
else
Submitter.find_by!(slug: params[:submitter_slug])
end
if params[:template_slug]
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
template: { slug: params[:template_slug] })
elsif params[:submission_slug]
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
submission: { slug: params[:submission_slug] })
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
else
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
end
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
+73 -28
View File
@@ -8,19 +8,28 @@ class StartFormController < ApplicationController
around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template
before_action :authorize_start!, only: :update
def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
return render :private if current_user && current_ability.can?(:read, @template)
raise ActionController::RoutingError, I18n.t('not_found')
end
end
def update
return redirect_to start_form_path(@template.slug) if @template.archived_at?
@submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at?
@@ -42,9 +51,13 @@ class StartFormController < ApplicationController
if @submitter.save
if is_new_record
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
'webhook_url_id' => webhook_url.id)
enqueue_submission_create_webhooks(@submitter)
SearchEntries.enqueue_reindex(@submitter)
if @submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
'submission_id' => @submitter.submission_id)
end
end
@@ -56,6 +69,8 @@ class StartFormController < ApplicationController
end
def completed
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
@submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil)
.find_by!(email: params[:email])
@@ -63,32 +78,55 @@ class StartFormController < ApplicationController
private
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
Submitter.find_by(slug: params[:resubmit])
end
end
def authorize_start!
return redirect_to start_form_path(@template.slug) if @template.archived_at?
return if @resubmit_submitter
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
redirect_to start_form_path(@template.slug)
end
def enqueue_submission_create_webhooks(submitter)
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'webhook_url_id' => webhook_url.id)
end
end
def find_or_initialize_submitter(template, submitter_params)
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
Submitter
.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
end
def assign_submission_attributes(submitter, template)
resubmit_submitter =
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
ip: request.remote_ip,
ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {}
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: @resubmit_submitter&.metadata.presence || {}
)
if submitter.values.present?
resubmit_submitter.attachments.each do |attachment|
@resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
@@ -96,6 +134,7 @@ class StartFormController < ApplicationController
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
expire_at: Templates.build_default_expire_at(template),
submitters: [submitter],
source: :link)
@@ -105,19 +144,25 @@ class StartFormController < ApplicationController
end
def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template.submitters)
end
def submitter_params
return current_user.slice(:email) if params[:selfsign]
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
def load_template
slug = params[:slug] || params[:start_form_slug]
@template = Template.find_by!(slug:)
@template =
if @resubmit_submitter
@resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
end
def multiple_submitters_error_message
@@ -4,14 +4,12 @@ class SubmissionsArchivedController < ApplicationController
load_and_authorize_resource :submission, parent: false
def index
@submissions = @submissions.joins(:template)
@submissions = @submissions.left_joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
.preload(:template_accesses, :created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@@ -20,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end
end
+7 -3
View File
@@ -8,6 +8,10 @@ class SubmissionsController < ApplicationController
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action only: :create do
authorize!(:create, Submission)
end
def show
@submission = Submissions.preload_with_pages(@submission)
@@ -26,8 +30,6 @@ class SubmissionsController < ApplicationController
end
def create
authorize!(:create, Submission)
save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1'
@@ -56,6 +58,8 @@ class SubmissionsController < ApplicationController
Submissions.send_signature_requests(submissions)
SearchEntries.enqueue_reindex(submissions)
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error,
@@ -81,7 +85,7 @@ class SubmissionsController < ApplicationController
I18n.t('submission_has_been_archived')
end
redirect_back(fallback_location: template_path(@submission.template), notice:)
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:)
end
private
@@ -4,15 +4,13 @@ class SubmissionsDashboardController < ApplicationController
load_and_authorize_resource :submission, parent: false
def index
@submissions = @submissions.joins(:template)
@submissions = @submissions.left_joins(:template)
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author)
.preload(:template_accesses, :created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@@ -21,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end
end
@@ -8,20 +8,20 @@ class SubmissionsDownloadController < ApplicationController
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if submitter&.slug == params[:submitter_slug]
if @submitter&.slug == params[:submitter_slug]
true
else
submitter = nil
@submitter = nil
end
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(submitter)
Submissions::EnsureResultGenerated.call(@submitter)
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
end
if params[:combined] == 'true'
url = build_combined_url(submitter)
url = build_combined_url(@submitter)
if url
render json: [url]
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class SubmissionsPreviewController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
+13 -8
View File
@@ -8,6 +8,7 @@ class SubmitFormController < ApplicationController
skip_authorization_check
before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show
CONFIG_KEYS = [].freeze
@@ -15,19 +16,14 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if submission.template.archived_at? ||
submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template.preferences['submitters_order'] == 'preserved') &&
submission.template&.preferences&.dig('submitters_order') == 'preserved') &&
!Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter)
Submissions.preload_with_pages(submission)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@@ -54,7 +50,7 @@ class SubmitFormController < ApplicationController
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end
if @submitter.template.archived_at? || @submitter.submission.archived_at?
if @submitter.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end
@@ -84,6 +80,15 @@ class SubmitFormController < ApplicationController
private
def maybe_render_locked_page
return render :archived if @submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if @submitter.submission.expired?
render :declined if @submitter.declined_at?
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
end
@@ -11,7 +11,7 @@ class SubmitFormDeclineController < ApplicationController
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
submitter.submission.template&.archived_at?
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)
@@ -7,25 +7,28 @@ class SubmitFormDownloadController < ApplicationController
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if submitter.declined_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
return head :unprocessable_entity if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
last_completed_submitter = submitter.submission.submitters
.where.not(id: submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
attachments =
if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter)
else
submitter.submission.template.schema_documents.preload(:blob)
@submitter.submission.schema_documents.preload(:blob)
end
urls = attachments.map do |attachment|
@@ -22,13 +22,17 @@ class SubmittersAutocompleteController < ApplicationController
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
if Docuseal.fulltext_search?(current_user)
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
else
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%"
term = "#{params[:q].downcase}%"
submitters.where(column.matches(term))
submitters.where(column.matches(term))
end
else
Submitters.search(submitters, params[:q])
Submitters.search(current_user, submitters, params[:q])
end
end
end
+2
View File
@@ -38,6 +38,8 @@ class SubmittersController < ApplicationController
if @submitter.save
maybe_resend_email_sms(@submitter, params)
SearchEntries.enqueue_reindex(@submitter)
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
@@ -6,13 +6,15 @@ class SubmittersResubmitController < ApplicationController
def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
submission = @submitter.template.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
**@submitter.submission.slice(:template_fields,
:account_id,
:template_schema,
:template_submitters,
:preferences))
submission = @submitter.account.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
**@submitter.submission.slice(:template_fields,
:account_id,
:name,
:template_id,
:template_schema,
:template_submitters,
:preferences))
@submitter.submission.submitters.each do |submitter|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
@@ -27,6 +29,10 @@ class SubmittersResubmitController < ApplicationController
submission.save!
@submitter.submission.documents_attachments.each do |attachment|
submission.documents_attachments.create!(uuid: attachment.uuid, blob_id: attachment.blob_id)
end
redirect_to submit_form_path(slug: @new_submitter.slug)
end
@@ -6,10 +6,10 @@ class TemplateFoldersController < ApplicationController
def show
@templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses)
@templates = Templates.search(@templates, params[:q])
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
@pagy, @templates = pagy(@templates, limit: 12)
@pagy, @templates = pagy_auto(@templates, limit: 12)
end
def edit; end
@@ -5,8 +5,8 @@ class TemplatesArchivedController < ApplicationController
def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
@templates = Templates.search(current_user, @templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12)
@pagy, @templates = pagy_auto(@templates, limit: 12)
end
end
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index
@submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:files].blank?
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
).call
cloned_template = Templates::Clone.call(@template, author: current_user)
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
cloned_template.save!
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
excluded_attachment_uuids: documents.map(&:uuid))
SearchEntries.enqueue_reindex(cloned_template)
respond_to do |f|
f.html { redirect_to edit_template_path(cloned_template) }
f.json { render json: { id: cloned_template.id } }
end
rescue Templates::CreateAttachments::PdfEncrypted
respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
end
end
end
+11 -5
View File
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
def show
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions
@@ -21,9 +21,7 @@ class TemplatesController < ApplicationController
submissions.order(id: :desc)
end
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
@@ -74,6 +72,8 @@ class TemplatesController < ApplicationController
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SearchEntries.enqueue_reindex(@template)
enqueue_template_created_webhooks(@template)
maybe_redirect_to_template(@template)
@@ -83,7 +83,13 @@ class TemplatesController < ApplicationController
end
def update
@template.update!(template_params)
@template.assign_attributes(template_params)
is_name_changed = @template.name_changed?
@template.save!
SearchEntries.enqueue_reindex(@template) if is_name_changed
enqueue_template_updated_webhooks(@template)
@@ -17,7 +17,7 @@ class TemplatesDashboardController < ApplicationController
@pagy, @template_folders = pagy(
@template_folders,
items: FOLDERS_PER_PAGE,
limit: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
@@ -25,7 +25,7 @@ class TemplatesDashboardController < ApplicationController
@templates = @templates.none
else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates)
@templates = filter_templates(@templates).preload(:author, :template_accesses)
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
limit =
@@ -35,27 +35,36 @@ class TemplatesDashboardController < ApplicationController
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy(@templates, limit:)
@pagy, @templates = pagy_auto(@templates, limit:)
end
end
private
def filter_templates(templates)
rel = templates.active.preload(:author, :template_accesses)
rel = templates.active
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
rel = rel.where(folder_id: current_account.default_template_folder.id)
else
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
shared_account_ids = [current_user.account_id]
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
rel = Template.where(
Template.arel_table[:id].in(
Arel::Nodes::Union.new(
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel,
shared_template_ids.arel
)
)
)
else
rel = rel.where(folder_id: current_account.default_template_folder.id)
end
end
Templates.search(rel, params[:q])
Templates.search(current_user, rel, params[:q])
end
def sort_template_folders(template_folders, current_user, order)
@@ -13,7 +13,7 @@ class TemplatesFormPreviewController < ApplicationController
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submitters.preload_with_pages(@submitter)
Submissions.preload_with_pages(@submitter.submission)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@@ -26,12 +26,20 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
submitters_order require_phone_2fa
default_expire_at_duration
default_expire_at
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]]
).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
attrs[:preferences][:default_expire_at] =
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
end
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
value == 'true'
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class TemplatesShareLinkController < ApplicationController
load_and_authorize_resource :template
def show; end
def create
authorize!(:update, @template)
@template.update!(template_params)
head :ok
end
private
def template_params
params.require(:template).permit(:shared_link)
end
end
@@ -25,6 +25,8 @@ class TemplatesUploadsController < ApplicationController
enqueue_template_created_webhooks(@template)
SearchEntries.enqueue_reindex(@template)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
+1 -1
View File
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
@users.active.where.not(role: 'integration')
end
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
end
def new; end
+5
View File
@@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit'
import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute'
@@ -33,6 +34,7 @@ import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -101,6 +103,8 @@ safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
@@ -125,6 +129,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true',
withReplaceAndCloneUpload: true,
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true'
+14
View File
@@ -0,0 +1,14 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', () => {
if (!this.element.checked) {
this.element.checked = true
this.element.dispatchEvent(new Event('change', { bubbles: true }))
}
})
}
get element () {
return document.getElementById(this.dataset.elementId)
}
}
@@ -3,8 +3,6 @@ export default class extends HTMLElement {
this.clearChecked()
this.addEventListener('click', (e) => {
e.stopPropagation()
const text = this.dataset.text || this.innerText.trim()
if (navigator.clipboard) {
@@ -0,0 +1,222 @@
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3a9 9 0 1 0 9 9" />
</svg>`
export default targetable(class extends HTMLElement {
static [targets.static] = [
'hiddenOnDrag',
'folderCards',
'templateCards'
]
static [target.static] = [
'form',
'fileDropzone',
'fileDropzoneLoading'
]
connectedCallback () {
document.addEventListener('drop', this.onWindowDragdrop)
document.addEventListener('dragover', this.onWindowDropover)
window.addEventListener('dragleave', this.onWindowDragleave)
this.fileDropzone?.addEventListener('drop', this.onDropFile)
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
el?.addEventListener('dragover', this.onDragover)
el?.addEventListener('dragleave', this.onDragleave)
})
}
disconnectedCallback () {
document.removeEventListener('drop', this.onWindowDragdrop)
document.removeEventListener('dragover', this.onWindowDropover)
window.removeEventListener('dragleave', this.onWindowDragleave)
}
onTemplateDragStart = (e) => {
const id = e.target.href.split('/').pop()
e.dataTransfer.effectAllowed = 'move'
if (id) {
e.dataTransfer.setData('template_id', id)
const dragPreview = e.target.cloneNode(true)
const rect = e.target.getBoundingClientRect()
const height = e.target.children[0].getBoundingClientRect().height + 50
dragPreview.children[1].remove()
dragPreview.style.width = `${rect.width}px`
dragPreview.style.height = `${height}px`
dragPreview.style.position = 'absolute'
dragPreview.style.top = '-1000px'
dragPreview.style.pointerEvents = 'none'
dragPreview.style.opacity = '0.9'
document.body.appendChild(dragPreview)
e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2)
setTimeout(() => document.body.removeChild(dragPreview), 0)
}
}
onDropFile = (e) => {
e.preventDefault()
this.fileDropzoneLoading.classList.remove('hidden')
this.fileDropzoneLoading.previousElementSibling.classList.add('hidden')
this.fileDropzoneLoading.classList.add('opacity-50')
this.uploadFiles(e.dataTransfer.files, '/templates_upload')
}
onDropFolder = (e, el) => {
e.preventDefault()
const templateId = e.dataTransfer.getData('template_id')
if (e.dataTransfer.files.length || templateId) {
const loading = document.createElement('div')
const svg = el.querySelector('svg')
loading.innerHTML = loadingIconHtml
loading.children[0].classList.add(...svg.classList)
el.replaceChild(loading.children[0], svg)
el.classList.add('opacity-50')
if (e.dataTransfer.files.length) {
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
} else {
const formData = new FormData()
formData.append('name', el.innerText)
fetch(`/templates/${templateId}/folder`, {
method: 'PUT',
redirect: 'manual',
body: formData,
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
}).finally(() => {
window.Turbo.cache.clear()
window.Turbo.visit(location.href)
})
}
}
}
onDropTemplate = (e) => {
e.preventDefault()
if (e.dataTransfer.files.length) {
const loading = document.createElement('div')
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
loading.innerHTML = loadingIconHtml
e.target.appendChild(loading)
e.target.classList.add('opacity-50')
const id = e.target.href.split('/').pop()
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
}
}
onWindowDragdrop = (e) => {
e.preventDefault()
if (!this.isLoading) this.hideDraghover()
}
uploadFiles (files, url) {
this.isLoading = true
this.form.action = url
this.form.querySelector('[type="file"]').files = files
this.form.querySelector('[type="submit"]').click()
}
onWindowDropover = (e) => {
e.preventDefault()
if (e.dataTransfer?.types?.includes('Files')) {
this.showDraghover()
}
}
onDragover (e) {
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
this.style.backgroundColor = '#F7F3F0'
if (this.classList.contains('before:border-base-300')) {
this.classList.remove('before:border-base-300')
this.classList.add('before:border-base-content/30')
} else if (this.classList.contains('border-base-300')) {
this.classList.remove('border-base-300')
this.classList.add('border-base-content/30')
}
}
}
onDragleave () {
this.style.backgroundColor = null
if (this.classList.contains('before:border-base-content/30')) {
this.classList.remove('before:border-base-content/30')
this.classList.add('before:border-base-300')
} else if (this.classList.contains('border-base-content/30')) {
this.classList.remove('border-base-content/30')
this.classList.add('border-base-300')
}
}
onWindowDragleave = (e) => {
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
this.hideDraghover()
}
}
showDraghover = () => {
if (this.isDrag) return
this.isDrag = true
this.fileDropzone?.classList?.remove('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
return [...this.folderCards, ...this.templateCards].forEach((el) => {
el.classList.remove('bg-base-200', 'before:hidden')
})
}
hideDraghover = () => {
this.isDrag = false
this.fileDropzone?.classList?.add('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
return [...this.folderCards, ...this.templateCards].forEach((el) => {
el.classList.add('bg-base-200', 'before:hidden')
})
}
})
+22 -4
View File
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'loading',
'icon',
'input'
'input',
'area'
]
connectedCallback () {
this.addEventListener('drop', this.onDrop)
this.addEventListener('dragover', (e) => e.preventDefault())
this.addEventListener('drop', this.onDrop)
document.addEventListener('turbo:submit-end', this.toggleLoading)
this.area?.addEventListener('dragover', this.onDragover)
this.area?.addEventListener('dragleave', this.onDragleave)
}
disconnectedCallback () {
this.removeEventListener('drop', this.onDrop)
document.removeEventListener('turbo:submit-end', this.toggleLoading)
this.area?.removeEventListener('dragover', this.onDragover)
this.area?.removeEventListener('dragleave', this.onDragleave)
}
onDragover (e) {
if (e.dataTransfer?.types?.includes('Files')) {
this.style.backgroundColor = '#F7F3F0'
this.classList.remove('border-base-300', 'hover:bg-base-200/30')
this.classList.add('border-base-content/30')
}
}
onDragleave () {
this.style.backgroundColor = null
this.classList.remove('border-base-content/30')
this.classList.add('border-base-300', 'hover:bg-base-200/30')
}
onDrop (e) {
@@ -36,7 +36,9 @@ export default class extends HTMLElement {
}
fetch = (text, resolve) => {
const q = text.split(/[;,\s]+/).pop().trim()
const q = this.dataset.field === 'email'
? text.split(/[;,\s]+/).pop().trim()
: text
if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
+1 -1
View File
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
const elementIds = JSON.parse(this.dataset.elementIds)
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
})
}
})
+11 -5
View File
@@ -23,7 +23,7 @@
</div>
<div
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none field-area-active-label"
>
<template v-if="area.option_uuid && !option.value">
{{ optionValue(option) }}
@@ -55,10 +55,12 @@
>
<div
v-else-if="field.type === 'signature' && signature"
class="flex flex-col justify-between h-full overflow-hidden"
class="flex justify-between h-full gap-1 overflow-hidden w-full"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
>
<div
class="flex-grow flex overflow-hidden"
class="flex overflow-hidden"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
style="min-height: 50%"
>
<img
@@ -67,8 +69,9 @@
>
</div>
<div
v-if="withSignatureId"
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
v-if="withSignatureId || field.preferences?.reason_field_uuid"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
:class="isNarrow ? 'w-1/2' : 'w-full'"
>
<div class="truncate uppercase">
ID: {{ signature.uuid }}
@@ -444,6 +447,9 @@ export default {
}
return style
},
isNarrow () {
return this.area.h > 0 && (this.area.w / this.area.h) > 6
}
},
watch: {
+3 -2
View File
@@ -93,7 +93,7 @@
<div
v-show="isFormVisible"
id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }"
>
@@ -1133,7 +1133,7 @@ export default {
parent.style.overflow = 'hidden'
scrollbox.classList.add('h-full', 'overflow-y-auto')
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
scrollbox.parentNode.classList.add('h-screen', 'h-[100dvh]', 'overflow-y-auto')
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
})
}
@@ -1390,6 +1390,7 @@ export default {
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
formData.append('completed', 'true')
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone)
}
let saveStepRequest
@@ -211,7 +211,7 @@ export default {
},
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
if (this.isUsePreviousValue && this.field.required === true) {
return this.previousValue
} else {
return null
@@ -133,7 +133,7 @@
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<FileDropzone
v-if="format === 'upload' && !modelValue"
v-if="format === 'upload' && !modelValue && !computedPreviousValue"
:message="`${t('upload')} ${field.name || t('signature')}`"
:submitter-slug="submitterSlug"
:dry-run="dryRun"
@@ -414,7 +414,7 @@ export default {
}
},
computedPreviousValue () {
if (this.isUsePreviousValue) {
if (this.isUsePreviousValue && this.field.required === true) {
return this.previousValue
} else {
return null
+4 -2
View File
@@ -151,8 +151,10 @@ export default {
resizeTextarea () {
const textarea = this.$refs.textarea
textarea.style.height = 'auto'
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
if (textarea) {
textarea.style.height = 'auto'
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
}
},
toggleTextArea () {
this.isTextArea = true
+101 -7
View File
@@ -5,7 +5,19 @@
class="mx-auto pl-3 h-full"
:class="isMobile ? 'pl-4' : 'md:pl-4'"
@dragover="onDragover"
@drop="isDragFile = false"
>
<HoverDropzone
v-if="sortedDocuments.length && withUploadButton && editable"
:is-dragging="isDragFile"
:template-id="template.id"
:accept-file-types="acceptFileTypes"
:with-replace-and-clone="withReplaceAndCloneUpload"
@add="[updateFromUpload($event), isDragFile = false]"
@replace="[onDocumentsReplace($event), isDragFile = false]"
@replace-and-clone="onDocumentsReplaceAndTemplateClone($event)"
@error="[onUploadFailed($event), isDragFile = false]"
/>
<DragPlaceholder
ref="dragPlaceholder"
:field="fieldsDragFieldRef.value || toRaw(dragField)"
@@ -67,13 +79,52 @@
name="buttons"
/>
<template v-else>
<form
v-if="withSignYourselfButton && template.submitters.length < 2"
target="_blank"
data-turbo="false"
class="inline"
method="post"
:action="`/d/${template.slug}`"
@submit="maybeShowErrorTemplateAlert"
>
<input
type="hidden"
name="_method"
value="put"
autocomplete="off"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
autocomplete="off"
>
<input
type="hidden"
name="selfsign"
value="true"
autocomplete="off"
>
<button
class="btn btn-primary btn-ghost text-base hidden md:flex"
type="submit"
>
<IconWritingSign
width="22"
class="inline"
/>
<span class="hidden md:inline">
{{ t('sign_yourself') }}
</span>
</button>
</form>
<a
v-if="withSignYourselfButton"
v-else-if="withSignYourselfButton"
id="sign_yourself_button"
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
:href="`/templates/${template.id}/submissions/new?selfsign=true`"
class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
data-turbo-frame="modal"
@click="maybeShowErrorTemplateAlert"
>
<IconWritingSign
@@ -433,6 +484,7 @@
<script>
import Upload from './upload'
import Dropzone from './dropzone'
import HoverDropzone from './hover_dropzone'
import DragPlaceholder from './drag_placeholder'
import Fields from './fields'
import MobileDrawField from './mobile_draw_field'
@@ -462,6 +514,7 @@ export default {
MobileFields,
Logo,
Dropzone,
HoverDropzone,
DocumentPreview,
DocumentControls,
IconInnerShadowTop,
@@ -665,6 +718,11 @@ export default {
required: false,
default: true
},
withReplaceAndCloneUpload: {
type: Boolean,
required: false,
default: false
},
withPhone: {
type: Boolean,
required: false,
@@ -724,7 +782,8 @@ export default {
copiedArea: null,
drawFieldType: null,
drawOption: null,
dragField: null
dragField: null,
isDragFile: false
}
},
computed: {
@@ -836,6 +895,7 @@ export default {
window.addEventListener('keydown', this.onKeyDown)
window.addEventListener('resize', this.onWindowResize)
window.addEventListener('dragleave', this.onWindowDragLeave)
this.$nextTick(() => {
if (document.location.search?.includes('stripe_connect_success')) {
@@ -854,6 +914,7 @@ export default {
window.removeEventListener('keydown', this.onKeyDown)
window.removeEventListener('resize', this.onWindowResize)
window.removeEventListener('dragleave', this.onWindowDragLeave)
},
beforeUpdate () {
this.documentRefs = []
@@ -868,6 +929,13 @@ export default {
ref.x = e.clientX - ref.offsetX
ref.y = e.clientY - ref.offsetY
} else if (e.dataTransfer?.types?.includes('Files')) {
this.isDragFile = true
}
},
onWindowDragLeave (event) {
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
this.isDragFile = false
}
},
reorderFields (item) {
@@ -1250,7 +1318,7 @@ export default {
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
}
},
onDraw (area) {
onDraw ({ area, isTooSmall }) {
if (this.drawField) {
if (this.drawOption) {
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
@@ -1345,7 +1413,7 @@ export default {
area.y -= area.h / 2
}
if (area.w) {
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
this.addField(type, area)
this.selectedAreaRef.value = area
@@ -1529,6 +1597,9 @@ export default {
})
}, 'image/png')
},
onUploadFailed (error) {
if (error) alert(error)
},
updateFromUpload (data) {
this.template.schema.push(...data.schema)
this.template.documents.push(...data.documents)
@@ -1649,6 +1720,29 @@ export default {
this.save()
},
onDocumentsReplace (data) {
data.schema.forEach((schemaItem, index) => {
const existingSchemaItem = this.template.schema[index]
if (this.template.schema[index]) {
this.onDocumentReplace({
replaceSchemaItem: existingSchemaItem,
schema: [schemaItem],
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)]
})
} else {
this.updateFromUpload({
schema: [schemaItem],
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)],
fields: data.fields,
submitters: data.submitters
})
}
})
},
onDocumentsReplaceAndTemplateClone (template) {
window.Turbo.visit(`/templates/${template.id}/edit`)
},
moveDocument (item, direction) {
const currentIndex = this.template.schema.indexOf(item)
+1 -1
View File
@@ -23,7 +23,7 @@
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)"
@scroll-to="scrollToArea"
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
/>
</div>
</template>
+61 -15
View File
@@ -2,34 +2,42 @@
<div
class="flex h-60 w-full"
@dragover.prevent
@dragenter="isDragEntering = true"
@dragleave="isDragEntering = false"
@drop.prevent="onDropFiles"
>
<label
id="document_dropzone"
class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
class="w-full relative rounded-md border-2 border-base-content/10 border-dashed"
:for="inputId"
:class="{ 'opacity-50': isLoading || isProcessing }"
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
<div class="flex flex-col items-center">
<IconInnerShadowTop
v-if="isLoading || isProcessing"
v-if="isLoading"
class="animate-spin"
:width="40"
:height="40"
/>
<IconCloudUpload
<component
:is="icon"
v-else
class="stroke-[1.5px]"
:width="40"
:height="40"
/>
<div
v-if="message"
class="font-medium text-lg mb-1"
class="text-lg mb-1"
:class="{ 'mt-1': !withDescription, 'font-medium': withDescription }"
>
{{ message }}
</div>
<div class="text-sm">
<div
v-if="withDescription"
class="text-sm"
>
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
</div>
</div>
@@ -54,13 +62,16 @@
<script>
import Upload from './upload'
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'FileDropzone',
components: {
IconFilePlus,
IconCloudUpload,
IconInnerShadowTop
IconInnerShadowTop,
IconFileSymlink,
IconFiles
},
inject: ['baseFetch', 't'],
props: {
@@ -68,35 +79,70 @@ export default {
type: [Number, String],
required: true
},
withHoverClass: {
type: Boolean,
required: false,
default: true
},
icon: {
type: String,
required: false,
default: 'IconCloudUpload'
},
cloneTemplateOnUpload: {
type: Boolean,
required: false,
default: false
},
withDescription: {
type: Boolean,
required: false,
default: true
},
title: {
type: String,
required: false,
default: ''
},
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
}
},
emits: ['success'],
emits: ['success', 'error', 'loading'],
data () {
return {
isLoading: false,
isProcessing: false
isDragEntering: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
uploadUrl () {
if (this.cloneTemplateOnUpload) {
return `/templates/${this.templateId}/clone_and_replace`
} else {
return `/templates/${this.templateId}/documents`
}
},
message () {
if (this.isLoading) {
return this.t('uploading')
} else if (this.isProcessing) {
return this.t('processing_')
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
return this.t('add_pdf_documents_or_images')
return this.title || this.t('add_pdf_documents_or_images')
} else {
return this.t('add_documents_or_images')
return this.title || this.t('add_documents_or_images')
}
}
},
watch: {
isLoading (value) {
this.$emit('loading', value)
}
},
methods: {
upload: Upload.methods.upload,
onDropFiles (e) {
@@ -424,7 +424,7 @@
</a>
</li>
</template>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
<a
href="#"
class="text-sm py-1 px-2"
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
<a
href="#"
class="text-sm py-1 px-2"
+2 -2
View File
@@ -112,7 +112,7 @@
:key="type"
>
<button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
:id="`${type}_type_field_button`"
draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
@@ -168,7 +168,7 @@
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
>
<a
href="https://www.docuseal.com/contact"
href="https://www.docuseal.com/qualified-electronic-signature"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
@@ -0,0 +1,89 @@
<template>
<div
v-if="isDragging || isLoading"
class="modal modal-open"
>
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
<Dropzone
class="flex-1 h-full"
icon="IconFilePlus"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-hover-class="false"
:with-description="false"
:title="t('add_a_new_document')"
type="add_files"
@loading="isLoading = $event"
@success="$emit('add', $event)"
@error="$emit('error', $event)"
/>
<div class="flex-1 flex gap-2 w-full">
<Dropzone
class="flex-1 h-full"
icon="IconFileSymlink"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-hover-class="false"
:with-description="false"
:title="t('replace_existing_document')"
@loading="isLoading = $event"
@success="$emit('replace', $event)"
@error="$emit('error', $event)"
/>
<Dropzone
v-if="withReplaceAndClone"
class="flex-1 h-full"
icon="IconFiles"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-hover-class="false"
:with-description="false"
:clone-template-on-upload="true"
:title="t('clone_and_replace_documents')"
@loading="isLoading = $event"
@success="$emit('replace-and-clone', $event)"
@error="$emit('error', $event)"
/>
</div>
</div>
</div>
</template>
<script>
import Dropzone from './dropzone'
export default {
name: 'HoverDropzone',
components: {
Dropzone
},
inject: ['t'],
props: {
isDragging: {
type: Boolean,
required: true,
default: false
},
templateId: {
type: [Number, String],
required: true
},
withReplaceAndClone: {
type: Boolean,
required: false,
default: true
},
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
}
},
emits: ['add', 'replace', 'replace-and-clone', 'error'],
data () {
return {
isLoading: false
}
}
}
</script>
+18
View File
@@ -63,6 +63,9 @@ const en = {
processing_: 'Processing...',
add_pdf_documents_or_images: 'Add PDF documents or images',
add_documents_or_images: 'Add documents or images',
add_a_new_document: 'Add a new document',
replace_existing_document: 'Replace existing document',
clone_and_replace_documents: 'Clone and replace documents',
required: 'Required',
default_value: 'Default value',
format: 'Format',
@@ -225,6 +228,9 @@ const es = {
processing_: 'Procesando...',
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
add_documents_or_images: 'Agregar documentos o imágenes',
add_a_new_document: 'Agregar un nuevo documento',
replace_existing_document: 'Reemplazar documento existente',
clone_and_replace_documents: 'Clonar y reemplazar documentos',
required: 'Requerido',
default_value: 'Valor predeterminado',
format: 'Formato',
@@ -393,6 +399,9 @@ const it = {
processing_: 'Elaborazione...',
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
add_documents_or_images: 'Aggiungi documenti o immagini',
add_a_new_document: 'Aggiungi un nuovo documento',
replace_existing_document: 'Sostituisci documento esistente',
clone_and_replace_documents: 'Clona e sostituisci documenti',
required: 'Obbligatorio',
default_value: 'Valore predefinito',
format: 'Formato',
@@ -555,6 +564,9 @@ const pt = {
processing_: 'Processando...',
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
add_documents_or_images: 'Adicionar documentos ou imagens',
add_a_new_document: 'Ajouter un nouveau document',
replace_existing_document: 'Substituir documento existente',
clone_and_replace_documents: 'Clonar e substituir documentos',
required: 'Obrigatório',
default_value: 'Valor padrão',
format: 'Formato',
@@ -719,6 +731,9 @@ const fr = {
processing_: 'Traitement en cours...',
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
add_documents_or_images: 'Ajoutez des documents ou des images',
add_a_new_document: 'Adicionar um novo documento',
replace_existing_document: 'Remplacer le document existant',
clone_and_replace_documents: 'Cloner et remplacer les documents',
required: 'Requis',
default_value: 'Valeur par défaut',
format: 'Format',
@@ -884,6 +899,9 @@ const de = {
processing_: 'Verarbeitung...',
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
add_a_new_document: 'Neues Dokument hinzufügen',
replace_existing_document: 'Vorhandenes Dokument ersetzen',
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
required: 'Erforderlich',
default_value: 'Standardwert',
format: 'Format',
@@ -54,7 +54,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
<a
href="#"
class="text-sm py-1 px-2"
+6 -1
View File
@@ -290,7 +290,12 @@ export default {
area.cell_w = this.newArea.cell_w
}
this.$emit('draw', area)
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
const isTooSmall = dx < 8 && dy < 8
this.$emit('draw', { area, isTooSmall })
}
this.showMask = false
+5 -5
View File
@@ -2,7 +2,7 @@
<label
:for="inputId"
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
:class="{ 'opacity-100': isLoading || isProcessing }"
:class="{ 'opacity-100': isLoading }"
>
{{ message }}
<form
@@ -41,19 +41,19 @@ export default {
emits: ['success'],
data () {
return {
isLoading: false,
isProcessing: false
isLoading: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
uploadUrl () {
return `/templates/${this.templateId}/documents`
},
message () {
if (this.isLoading) {
return this.t('uploading_')
} else if (this.isProcessing) {
return this.t('processing_')
} else {
return this.t('replace')
}
+24 -11
View File
@@ -4,10 +4,10 @@
id="add_document_button"
:for="inputId"
class="btn btn-outline w-full add-document-button"
:class="{ 'btn-disabled': isLoading || isProcessing }"
:class="{ 'btn-disabled': isLoading }"
>
<IconInnerShadowTop
v-if="isLoading || isProcessing"
v-if="isLoading"
width="20"
class="animate-spin"
/>
@@ -18,9 +18,6 @@
<span v-if="isLoading">
{{ t('uploading_') }}
</span>
<span v-else-if="isProcessing">
{{ t('processing_') }}
</span>
<span v-else>
{{ t('add_document') }}
</span>
@@ -63,30 +60,34 @@ export default {
default: 'image/*, application/pdf'
}
},
emits: ['success'],
emits: ['success', 'error'],
data () {
return {
isLoading: false,
isProcessing: false
isLoading: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
uploadUrl () {
return `/templates/${this.templateId}/documents`
}
},
methods: {
async upload () {
this.isLoading = true
this.baseFetch(`/templates/${this.templateId}/documents`, {
this.baseFetch(this.uploadUrl, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(this.$refs.form)
}).then((resp) => {
if (resp.ok) {
resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
this.isLoading = false
})
} else if (resp.status === 422) {
resp.json().then((data) => {
@@ -95,21 +96,33 @@ export default {
formData.append('password', prompt(this.t('enter_pdf_password')))
this.baseFetch(`/templates/${this.templateId}/documents`, {
this.baseFetch(this.uploadUrl, {
method: 'POST',
body: formData
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
this.isLoading = false
} else {
alert(this.t('wrong_password'))
this.$emit('error', await resp.json().error)
this.isLoading = false
}
})
} else {
this.$emit('error', data.error)
this.isLoading = false
}
})
} else {
resp.json().then((data) => {
this.$emit('error', data.error)
this.isLoading = false
})
}
}).finally(() => {
}).catch(() => {
this.isLoading = false
})
}
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class GenerateAttachmentPreviewJob
include Sidekiq::Job
InvalidFormat = Class.new(StandardError)
sidekiq_options queue: :images
def perform(params = {})
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
if attachment.content_type == Templates::ProcessDocument::PDF_CONTENT_TYPE
Templates::ProcessDocument.generate_pdf_preview_images(attachment, attachment.download)
elsif attachment.image?
Templates::ProcessDocument.generate_preview_image(attachment, attachment.download)
else
raise InvalidFormat, attachment.id
end
end
end
+1 -1
View File
@@ -7,7 +7,7 @@ class ProcessSubmissionExpiredJob
submission = Submission.find(params['submission_id'])
return if submission.archived_at?
return if submission.template.archived_at?
return if submission.template&.archived_at?
return if submission.submitters.where.not(declined_at: nil).exists?
return unless submission.submitters.exists?(completed_at: nil)
+3 -3
View File
@@ -82,7 +82,7 @@ class ProcessSubmitterCompletionJob
user = submission.created_by_user || submitter.template.author
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
submitter.template.preferences['completed_notification_email_enabled'] != false
submitter.template&.preferences&.dig('completed_notification_email_enabled') != false
if submission.submitters.map(&:email).exclude?(user.email) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
user.role != 'integration'
@@ -98,7 +98,7 @@ class ProcessSubmitterCompletionJob
end
def maybe_enqueue_copy_emails(submitter)
return if submitter.template.preferences['documents_copy_email_enabled'] == false
return if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false
configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@@ -119,7 +119,7 @@ class ProcessSubmitterCompletionJob
def build_bcc_addresses(submission)
bcc = submission.preferences['bcc_completed'].presence ||
submission.template.preferences['bcc_completed'].presence ||
submission.template&.preferences&.dig('bcc_completed').presence ||
submission.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class ReindexSearchEntryJob
include Sidekiq::Job
InvalidFormat = Class.new(StandardError)
def perform(params = {})
entry = SearchEntry.find_or_initialize_by(params.slice('record_type', 'record_id'))
SearchEntries.reindex_record(entry.record)
end
end
+7 -3
View File
@@ -20,7 +20,7 @@ class ApplicationMailer < ActionMailer::Base
end
def set_message_metadata
message.instance_variable_set(:@message_metadata, @message_metadata)
message.instance_variable_set(:@message_metadata, @message_metadata || {})
end
def set_message_uuid
@@ -28,10 +28,14 @@ class ApplicationMailer < ActionMailer::Base
end
def assign_message_metadata(tag, record)
@message_metadata = {
@message_metadata = (@message_metadata || {}).merge(
'tag' => tag,
'record_id' => record.id,
'record_type' => record.class.name
}
)
end
def put_metadata(attrs)
@message_metadata = (@message_metadata || {}).merge(attrs)
end
end
+29 -21
View File
@@ -23,11 +23,11 @@ class SubmitterMailer < ApplicationMailer
@body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@submitter.template.preferences['request_email_body'].presence
@submitter.template&.preferences&.dig('request_email_body').presence
@subject = @email_message&.subject.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence ||
@submitter.template.preferences['request_email_subject'].presence
@submitter.template&.preferences&.dig('request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@@ -53,6 +53,8 @@ class SubmitterMailer < ApplicationMailer
@submission = submitter.submission
@user = user
template_preferences = @submission.template&.preferences || {}
Submissions::EnsureResultGenerated.call(submitter)
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
@@ -60,15 +62,15 @@ class SubmitterMailer < ApplicationMailer
add_completed_email_attachments!(
submitter,
with_documents: @email_config&.value&.dig('attach_documents') != false &&
@submitter.template.preferences['completed_notification_email_attach_documents'] != false,
template_preferences['completed_notification_email_attach_documents'] != false,
with_audit_log: @email_config&.value&.dig('attach_audit_log') != false &&
@submitter.template.preferences['completed_notification_email_attach_audit'] != false
template_preferences['completed_notification_email_attach_audit'] != false
)
@subject = @submitter.template.preferences['completed_notification_email_subject'].presence
@subject = template_preferences['completed_notification_email_subject'].presence
@subject ||= @email_config.value['subject'] if @email_config
@body = @submitter.template.preferences['completed_notification_email_body'].presence
@body = template_preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_completed', @submitter)
@@ -97,7 +99,7 @@ class SubmitterMailer < ApplicationMailer
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
reply_to: @submitter.friendly_name,
subject: I18n.t(:name_declined_by_submitter,
name: @submission.template.name.truncate(20),
name: (@submission.name || @submission.template.name).truncate(20),
submitter: @submitter.name || @submitter.email || @submitter.phone))
end
end
@@ -107,22 +109,24 @@ class SubmitterMailer < ApplicationMailer
@submitter = submitter
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
template_preferences = @submitter.template&.preferences || {}
Submissions::EnsureResultGenerated.call(@submitter)
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
add_completed_email_attachments!(
submitter,
with_documents: @submitter.template.preferences['documents_copy_email_attach_documents'] != false &&
(@email_config.nil? || @email_config.value['attach_documents'] != false),
with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false &&
(@email_config.nil? || @email_config.value['attach_audit_log'] != false)
with_documents: template_preferences['documents_copy_email_attach_documents'] != false &&
(@email_config.nil? || @email_config.value['attach_documents'] != false),
with_audit_log: template_preferences['documents_copy_email_attach_audit'] != false &&
(@email_config.nil? || @email_config.value['attach_audit_log'] != false)
)
@subject = @submitter.template.preferences['documents_copy_email_subject'].presence
@subject = template_preferences['documents_copy_email_subject'].presence
@subject ||= @email_config.value['subject'] if @email_config
@body = @submitter.template.preferences['documents_copy_email_body'].presence
@body = template_preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_documents_copy', @submitter)
@@ -130,11 +134,7 @@ class SubmitterMailer < ApplicationMailer
I18n.with_locale(@current_account.locale) do
subject =
if @subject.present?
ReplaceEmailVariables.call(@subject, submitter:)
else
I18n.t(:your_document_copy)
end
@subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy)
mail(from: from_address_for_submitter(submitter),
to: to || @submitter.friendly_name,
@@ -147,7 +147,7 @@ class SubmitterMailer < ApplicationMailer
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
reply_to = submitter.preferences['reply_to'].presence
reply_to ||= submitter.template.preferences['documents_copy_email_reply_to'].presence if documents_copy_email
reply_to ||= submitter.template&.preferences&.dig('documents_copy_email_reply_to').presence if documents_copy_email
reply_to ||= email_config.value['reply_to'].presence if email_config
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
@@ -212,7 +212,7 @@ class SubmitterMailer < ApplicationMailer
end
def build_submitter_preferences_index(submitter)
submitter.template.preferences['submitters'].to_a.index_by { |e| e['uuid'] }
submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] }
end
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
@@ -233,9 +233,17 @@ class SubmitterMailer < ApplicationMailer
def from_address_for_submitter(submitter)
if submitter.submission.source.in?(%w[api embed]) &&
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
user = submitter.account.users.find_by(email: from_email)
put_metadata('from_user_id' => user.id)
from_email
else
(submitter.submission.created_by_user || submitter.submission.template.author).friendly_name
user = submitter.submission.created_by_user || submitter.submission.template.author
put_metadata('from_user_id' => user.id)
user.friendly_name
end
end
end
+1
View File
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
has_many :account_linked_accounts, dependent: :destroy
has_many :email_events, dependent: :destroy
has_many :webhook_urls, dependent: :destroy
has_many :account_accesses, dependent: :destroy
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
class_name: 'AccountLinkedAccount',
inverse_of: :account
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_accesses
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_account_accesses_on_account_id_and_user_id (account_id,user_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class AccountAccess < ApplicationRecord
belongs_to :account
belongs_to :user
end
+2
View File
@@ -29,6 +29,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
ALLOW_TO_PARTIAL_DOWNLOAD_KEY = 'allow_to_partial_download'
SUBMITTER_REMINDERS = 'submitter_reminders'
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
@@ -41,6 +42,7 @@ class AccountConfig < ApplicationRecord
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id'
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SUBMITTER_TIMEZONE_KEY = 'with_audit_submitter_timezone'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
+2 -2
View File
@@ -13,7 +13,7 @@
# account_id :bigint not null
# submission_id :bigint not null
# submitter_id :bigint not null
# template_id :bigint not null
# template_id :bigint
#
# Indexes
#
@@ -24,7 +24,7 @@ class CompletedSubmitter < ApplicationRecord
belongs_to :submitter
belongs_to :submission
belongs_to :account
belongs_to :template
belongs_to :template, optional: true
has_many :completed_documents, dependent: :destroy,
primary_key: :submitter_id,
+4 -4
View File
@@ -18,10 +18,10 @@
#
# Indexes
#
# index_email_events_on_account_id (account_id)
# index_email_events_on_email (email)
# index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id)
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
# index_email_events_on_email (email)
# index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id)
#
# Foreign Keys
#
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: search_entries
#
# id :bigint not null, primary key
# ngram :tsvector
# record_type :string not null
# tsvector :tsvector not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# record_id :bigint not null
#
# Indexes
#
# index_search_entries_on_account_id_ngram_submission (account_id,ngram) WHERE ((record_type)::text = 'Submission'::text) USING gin
# index_search_entries_on_account_id_ngram_submitter (account_id,ngram) WHERE ((record_type)::text = 'Submitter'::text) USING gin
# index_search_entries_on_account_id_ngram_template (account_id,ngram) WHERE ((record_type)::text = 'Template'::text) USING gin
# index_search_entries_on_account_id_tsvector_submission (account_id,tsvector) WHERE ((record_type)::text = 'Submission'::text) USING gin
# index_search_entries_on_account_id_tsvector_submitter (account_id,tsvector) WHERE ((record_type)::text = 'Submitter'::text) USING gin
# index_search_entries_on_account_id_tsvector_template (account_id,tsvector) WHERE ((record_type)::text = 'Template'::text) USING gin
# index_search_entries_on_record_id_and_record_type (record_id,record_type) UNIQUE
#
class SearchEntry < ApplicationRecord
belongs_to :record, polymorphic: true
belongs_to :account
end
+23 -5
View File
@@ -7,6 +7,7 @@
# id :bigint not null, primary key
# archived_at :datetime
# expire_at :datetime
# name :text
# preferences :text not null
# slug :string not null
# source :text not null
@@ -18,7 +19,7 @@
# updated_at :datetime not null
# account_id :bigint not null
# created_by_user_id :bigint
# template_id :bigint not null
# template_id :bigint
#
# Indexes
#
@@ -33,10 +34,12 @@
# fk_rails_... (template_id => templates.id)
#
class Submission < ApplicationRecord
belongs_to :template
belongs_to :template, optional: true
belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
has_many :submitters, dependent: :destroy
has_many :submission_events, dependent: :destroy
@@ -56,6 +59,7 @@ class Submission < ApplicationRecord
has_one_attached :combined_document
has_many_attached :preview_documents
has_many_attached :documents
has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false
@@ -65,13 +69,19 @@ class Submission < ApplicationRecord
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
scope :pending, lambda {
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) }
scope :expired, -> { where(expire_at: ..Time.current) }
scope :declined, lambda {
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
}
scope :expired, -> { pending.where(expire_at: ..Time.current) }
enum :source, {
invite: 'invite',
@@ -90,6 +100,14 @@ class Submission < ApplicationRecord
expire_at && expire_at <= Time.current
end
def schema_documents
if template_id?
template_schema_documents
else
documents_attachments
end
end
def fields_uuid_index
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end
+2
View File
@@ -38,6 +38,8 @@ class SubmissionEvent < ApplicationRecord
enum :event_type, {
send_email: 'send_email',
bounce_email: 'bounce_email',
complaint_email: 'complaint_email',
send_reminder_email: 'send_reminder_email',
send_sms: 'send_sms',
send_2fa_sms: 'send_2fa_sms',
+9 -6
View File
@@ -16,6 +16,7 @@
# preferences :text not null
# sent_at :datetime
# slug :string not null
# timezone :string
# ua :string
# uuid :string not null
# values :text not null
@@ -27,11 +28,12 @@
#
# Indexes
#
# index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_email (email)
# index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
# index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_completed_at_and_account_id (completed_at,account_id)
# index_submitters_on_email (email)
# index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
#
# Foreign Keys
#
@@ -41,6 +43,7 @@ class Submitter < ApplicationRecord
belongs_to :submission
belongs_to :account
has_one :template, through: :submission
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} }
@@ -54,7 +57,7 @@ class Submitter < ApplicationRecord
has_many_attached :documents
has_many_attached :attachments
has_many_attached :preview_documents
has_many :template_accesses, through: :template
has_many :template_accesses, through: :submission
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
has_many :document_generation_events, dependent: :destroy
+3
View File
@@ -10,6 +10,7 @@
# name :string not null
# preferences :text not null
# schema :text not null
# shared_link :boolean default(FALSE), not null
# slug :string not null
# source :text not null
# submitters :text not null
@@ -41,6 +42,8 @@ class Template < ApplicationRecord
belongs_to :account
belongs_to :folder, class_name: 'TemplateFolder'
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
before_validation :maybe_set_default_folder, on: :create
attribute :preferences, :string, default: -> { {} }
+2
View File
@@ -73,6 +73,8 @@ class User < ApplicationRecord
scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) }
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
def access_token
super || build_access_token.tap(&:save!)
end
+51 -11
View File
@@ -1,12 +1,52 @@
<div class="text-center">
<div class="join">
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
<%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
</a>
<% eu_server = request.host == 'docuseal.eu' %>
<server-selector>
<div id="global_server_selector" class="text-center hidden">
<div class="join">
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-40 <%= 'bg-base-300' unless eu_server %>">
<%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="relative btn bg-base-200 join-item w-40 <%= 'bg-base-300' if eu_server %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
<% unless eu_server %>
<span id="eu_server_alert" class="absolute flex space-x-0.5 hidden" style="top: -1.5rem;">
<span class="text-xs font-normal leading-none text-base-content normal-case">
<%= t('eu_data_residency') %>
</span>
<%= svg_icon 'corner_right_down', class: 'w-4 h-5 stroke-1 shrink-0 pt-1' %>
</span>
<% end %>
</a>
</div>
</div>
</div>
<div id="us_server_selector" class="flex justify-center hidden">
<div class="dropdown">
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
<% if eu_server %>
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
<span>EU Cloud</span>
<% else %>
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
<span>US Cloud</span>
<% end %>
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
</label>
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
<li>
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
US Cloud
</a>
</li>
<li>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
EU Cloud
</a>
</li>
</ul>
</div>
</div>
</server-selector>
<%= render 'scripts/server_selector' %>
+1 -1
View File
@@ -141,7 +141,7 @@
<%= t('preferences') %>
</h2>
</div>
<% if can?(:manage, account_config) %>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %>
<%= 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">
+1 -1
View File
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 9l4 -4l4 4m-4 -4v14" />
<path d="M21 15l-4 4l-4 -4m4 4v-14" />

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 362 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M20.986 12.502a9 9 0 1 0 -5.973 7.98" /><path d="M12 7v5l3 3" /><path d="M19 16v3" /><path d="M19 22v.01" />
</svg>

After

Width:  |  Height:  |  Size: 400 B

@@ -0,0 +1,3 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M6 6h6a3 3 0 0 1 3 3v10l-4 -4m8 0l-4 4"></path>
</svg>

After

Width:  |  Height:  |  Size: 285 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /><path d="M12 11v6" /><path d="M9.5 13.5l2.5 -2.5l2.5 2.5" />
</svg>

After

Width:  |  Height:  |  Size: 462 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 3v4a1 1 0 0 0 1 1h4" />
<path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z" />
<path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2" />
</svg>

After

Width:  |  Height:  |  Size: 485 B

+1 -7
View File
@@ -1,9 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 8v-2a2 2 0 0 1 2 -2h2"></path>
<path d="M4 16v2a2 2 0 0 0 2 2h2"></path>
<path d="M16 4h2a2 2 0 0 1 2 2v2"></path>
<path d="M16 20h2a2 2 0 0 0 2 -2v-2"></path>
<path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
<path d="M10 11v-2a2 2 0 1 1 4 0v2"></path>
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 11v-2a2 2 0 1 1 4 0v2" />
</svg>

Before

Width:  |  Height:  |  Size: 619 B

After

Width:  |  Height:  |  Size: 559 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 640 480">
<path fill="#bd3d44" d="M0 0h640v480H0" />
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640" />
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
<marker id="um-a" markerHeight="30" markerWidth="30">
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
</marker>
<path fill="none" marker-mid="url(#um-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z" />
</svg>

After

Width:  |  Height:  |  Size: 671 B

@@ -7,19 +7,19 @@
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, t('first_reminder_in'), class: 'label truncate' %>
<%= ff.select :first_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<%= ff.select :first_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
<% end %>
</div>
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :second_duration, t('second_reminder_in'), class: 'label truncate' %>
<%= ff.select :second_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<%= ff.select :second_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
<% end %>
</div>
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :third_duration, t('third_reminder_in'), class: 'label truncate' %>
<%= ff.select :third_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<%= ff.select :third_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
<% end %>
</div>
</div>
@@ -0,0 +1,22 @@
<script>
if (!window.customElements.get('server-selector')) {
customElements.define('server-selector', class extends HTMLElement {
connectedCallback() {
const usServerSelector = this.querySelector('#us_server_selector');
const globalServerSelector = this.querySelector('#global_server_selector');
const euServerAlert = this.querySelector('#eu_server_alert');
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
if (usTimezones.test(timezone)) {
usServerSelector.classList.remove('hidden');
} else if (timezone.includes('Europe')) {
globalServerSelector.classList.remove('hidden');
euServerAlert?.classList?.remove('hidden');
} else {
globalServerSelector.classList.remove('hidden');
}
}
});
}
</script>
+1 -1
View File
@@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open' %>
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30' %>
+5 -1
View File
@@ -2,7 +2,11 @@
<% if @pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm">
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %>
<% if @pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to)) %>
<% else %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to), count: local_assigns.fetch(:count, @pagy.count)) %>
<% end %>
<%= local_assigns[:left_additional_html] %>
</div>
<div class="flex items-center space-x-1.5">
+5
View File
@@ -0,0 +1,5 @@
<% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %>
<div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2">
<%= auto_link(MarkdownToHtml.call(configs.value)) %>
</div>
<% end %>
+3 -3
View File
@@ -23,12 +23,12 @@
</div>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account) %>
<toggle-submit class="block">
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, class: 'base-button w-full' %>
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
</toggle-submit>
<% end %>
<% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
<toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, class: 'white-button w-full' %>
</toggle-submit>
<% end %>
</div>
+28
View File
@@ -0,0 +1,28 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="text-center w-full space-y-6">
<%= render 'banner' %>
<p class="text-xl font-semibold text-center">
<%= t('share_link_is_currently_disabled') %>
</p>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<% if @template.archived_at? %>
<p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
+1
View File
@@ -42,3 +42,4 @@
</div>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
<%= render 'start_form/policy', account: @template.account %>
@@ -38,6 +38,8 @@
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
<% elsif event.event_type.include?('send_') %>
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
<%= t("submission_event_names.#{event.event_type}_html", submitter_name:) %>
<% else %>
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
<% end %>
+11 -10
View File
@@ -1,4 +1,5 @@
<% submitter_preferences_index = template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } %>
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
@@ -37,18 +38,18 @@
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="message_field" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template.preferences['submitters'].to_a.size > 1 %>
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
@@ -58,7 +59,7 @@
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
@@ -69,10 +70,10 @@
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template.submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
@@ -83,14 +84,14 @@
<% end %>
</ul>
</toggle-visible>
<% template.submitters.each_with_index do |submitter, index| %>
<% template_submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
@@ -100,7 +101,7 @@
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
+5 -4
View File
@@ -5,12 +5,13 @@
<% font_type = field.dig('preferences', 'font_type') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
</div>
<% if local_assigns[:with_signature_id] && attachment = attachments_index[value] %>
<div class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]">
<% if (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) && attachment = attachments_index[value] %>
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
<div class="truncate uppercase">
ID: <%= attachment.uuid %>
</div>

Some files were not shown because too many files have changed in this diff Show More