Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14c84eff93 | ||
|
|
01d982b093 | ||
|
|
35852ef6ab | ||
|
|
47b235dc5f | ||
|
|
0e11d2a581 | ||
|
|
e57e11f206 | ||
|
|
51f639065c | ||
|
|
6c1d4b466f | ||
|
|
b940c597dc | ||
|
|
1ce67bcb39 | ||
|
|
5050a67d08 | ||
|
|
76321cef3c | ||
|
|
8f3a203994 | ||
|
|
ea24562861 | ||
|
|
4a888d048e | ||
|
|
36331bb993 | ||
|
|
6f0e7c0ca8 | ||
|
|
2491b7b7e3 | ||
|
|
7965c17f46 | ||
|
|
5519459519 | ||
|
|
d8bf2cc101 | ||
|
|
2aece98d67 | ||
|
|
f602497255 | ||
|
|
11db68cdc7 | ||
|
|
bed98f4344 | ||
|
|
055f7e2bc1 | ||
|
|
dae3b4b6b5 | ||
|
|
f2888a08dc | ||
|
|
95627c5963 | ||
|
|
4bb3721db0 | ||
|
|
b12111383b | ||
|
|
263c0ff939 | ||
|
|
efc8ba6b0a | ||
|
|
4c530b3f88 | ||
|
|
f6358c9d47 | ||
|
|
b3baea2f6f | ||
|
|
c0594a8b74 | ||
|
|
1403401b03 | ||
|
|
700184f776 | ||
|
|
b6a1a3ee4d | ||
|
|
8cbf5802b5 | ||
|
|
ab9706afef | ||
|
|
f07f9d8cf9 | ||
|
|
591caecb92 | ||
|
|
ef4c2b7213 | ||
|
|
18999255a9 | ||
|
|
0a2a1de6b9 | ||
|
|
1b857b6eb3 | ||
|
|
e5e6b11c95 | ||
|
|
3ec3f58d55 | ||
|
|
3bfce862ec | ||
|
|
c01b67f7ec | ||
|
|
ea0efe9314 | ||
|
|
28b583691f | ||
|
|
33811945be | ||
|
|
9d1860f038 | ||
|
|
f03166b4ea | ||
|
|
c59d948d41 | ||
|
|
57d63dd2f2 | ||
|
|
c88715daec | ||
|
|
0375dad7c9 | ||
|
|
de3f83db96 | ||
|
|
a7bb96464c | ||
|
|
1d48fe9621 | ||
|
|
9544bb3d76 | ||
|
|
4e94664008 | ||
|
|
e41fe5fbca | ||
|
|
24d713dec6 | ||
|
|
f464e1c46a | ||
|
|
85d52d1f28 | ||
|
|
e77b5fa2c9 | ||
|
|
c91b4a765b | ||
|
|
2a031c14c8 | ||
|
|
f7dfc0b2ed | ||
|
|
3b894f5281 | ||
|
|
9645332e48 | ||
|
|
5670390b37 | ||
|
|
260bd6d5d5 | ||
|
|
fa0733f480 | ||
|
|
8b9678a434 | ||
|
|
43669bf095 | ||
|
|
2ef8df3f0c | ||
|
|
1c9d15aa7e | ||
|
|
9fcd8955d3 | ||
|
|
5e44ba49b8 | ||
|
|
ac1f90e711 | ||
|
|
c6eae66127 | ||
|
|
08aa902e07 | ||
|
|
1690337699 | ||
|
|
f78cf8cc6e | ||
|
|
343118d00d | ||
|
|
a3bd37c9a2 | ||
|
|
8bc13a5900 | ||
|
|
21138c1d02 | ||
|
|
38674a9fc9 | ||
|
|
a7ed0b86a4 | ||
|
|
91a96f06c8 | ||
|
|
23631b75f2 | ||
|
|
0fb9c2bbf6 | ||
|
|
14fa8fc8e7 | ||
|
|
047c192988 | ||
|
|
bfa244c8fa | ||
|
|
019222ae40 | ||
|
|
8a10852fe2 | ||
|
|
01232aed5a | ||
|
|
82f3ff4824 | ||
|
|
7be3f5a931 | ||
|
|
9d013a7384 | ||
|
|
0c71d7bff8 | ||
|
|
fdd030652e | ||
|
|
f2bf1461f0 | ||
|
|
fc6f796f30 | ||
|
|
5f44806287 | ||
|
|
318b65af18 | ||
|
|
91b182b04a | ||
|
|
15b886d4ff | ||
|
|
0122a237e3 | ||
|
|
2dca7d8a8e | ||
|
|
87265e2f22 | ||
|
|
3166defa1a | ||
|
|
93ba17f42e | ||
|
|
751be1dae5 | ||
|
|
3f8d2831e2 | ||
|
|
4d8942803e | ||
|
|
46d9bb2c87 | ||
|
|
34433c0575 | ||
|
|
544306eac4 | ||
|
|
0eeebfd3cf | ||
|
|
050224a10d | ||
|
|
7b6fc7d06f | ||
|
|
7fb148c85c | ||
|
|
dffa59fecd | ||
|
|
24fe2a1192 | ||
|
|
97b8ac4444 | ||
|
|
e34a763dd9 | ||
|
|
298b1d3e1e | ||
|
|
d9978519cc | ||
|
|
b4a68cf79e | ||
|
|
5663e60f92 | ||
|
|
19e8835809 | ||
|
|
330fae927f | ||
|
|
c5a5212947 | ||
|
|
21d81e38bf | ||
|
|
5b1a4ebffd | ||
|
|
5f5dabd0e5 | ||
|
|
ff46993de6 | ||
|
|
7676db2196 | ||
|
|
3ef0d28ffe | ||
|
|
231177f057 | ||
|
|
e5f7739952 | ||
|
|
4a8e2a82e5 | ||
|
|
6efe8027f1 | ||
|
|
96bb4eb6e5 | ||
|
|
38e68814cf |
No files matched your search
@@ -157,7 +157,10 @@ jobs:
|
|||||||
bundle install --jobs 4 --retry 4
|
bundle install --jobs 4 --retry 4
|
||||||
yarn install
|
yarn install
|
||||||
sudo apt-get update
|
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
|
- name: Run
|
||||||
env:
|
env:
|
||||||
RAILS_ENV: test
|
RAILS_ENV: test
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
FROM ruby:3.4.2-alpine AS fonts
|
FROM ruby:3.4.2-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
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")'
|
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
|
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\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -70,8 +79,10 @@ COPY ./tmp ./tmp
|
|||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
COPY .version ./public/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=download /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/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
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ GEM
|
|||||||
puma (6.5.0)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.12)
|
rack (3.1.16)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.0.0)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountConfigsController < ApplicationController
|
class AccountConfigsController < ApplicationController
|
||||||
before_action :load_account_config
|
before_action :load_account_config, only: :create
|
||||||
authorize_resource :account_config
|
authorize_resource :account_config, only: :create
|
||||||
|
|
||||||
|
load_and_authorize_resource :account_config, only: :destroy
|
||||||
|
|
||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
@@ -30,6 +32,14 @@ class AccountConfigsController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||||
|
|
||||||
|
@account_config.destroy!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_account_config
|
def load_account_config
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
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?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -37,9 +41,7 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(blob)
|
def authorization_check!(attachment)
|
||||||
attachment = blob.attachments.take
|
|
||||||
|
|
||||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(current_user, @submissions, params[:q])
|
||||||
submissions = filter_submissions(submissions, params)
|
submissions = filter_submissions(submissions, params)
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
@@ -80,6 +80,8 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submissions)
|
||||||
|
|
||||||
render json: build_create_json(submissions)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||||
DownloadUtils::UnableToDownload => e
|
DownloadUtils::UnableToDownload => e
|
||||||
@@ -110,7 +112,9 @@ module Api
|
|||||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:template_folder].present?
|
if params[:template_folder].present?
|
||||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:template_folder])
|
||||||
|
|
||||||
|
submissions = folder ? submissions.joins(:template).where(template: { folder_id: folder.id }) : submissions.none
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.key?(:archived)
|
if params.key?(:archived)
|
||||||
@@ -183,14 +187,14 @@ module Api
|
|||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
:expire_at,
|
:expire_at, :name,
|
||||||
{
|
{
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [], value: [], preferences: {} }] }]]
|
{ default_value: [], value: [], preferences: {} }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
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')
|
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
submitter.opened_at = Time.current
|
@submitter.opened_at = Time.current
|
||||||
submitter.save
|
@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|
|
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||||
'webhook_url_id' => webhook_url.id)
|
'webhook_url_id' => webhook_url.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :submitter
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitters = Submitters.search(@submitters, params[:q])
|
submitters = Submitters.search(current_user, @submitters, params[:q])
|
||||||
|
|
||||||
submitters = filter_submitters(submitters, params)
|
submitters = filter_submitters(submitters, params)
|
||||||
|
|
||||||
@@ -37,14 +37,16 @@ module Api
|
|||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||||
end
|
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 =
|
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
||||||
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
|
submitter_params.merge(role:),
|
||||||
for_submitter: @submitter)
|
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
||||||
|
for_submitter: @submitter
|
||||||
|
)
|
||||||
|
|
||||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||||
[normalized_params],
|
|
||||||
default_submitter_uuid: @submitter.uuid)
|
default_submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
assign_submitter_attrs(@submitter, normalized_params)
|
assign_submitter_attrs(@submitter, normalized_params)
|
||||||
@@ -65,10 +67,10 @@ module Api
|
|||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
|
SearchEntries.enqueue_reindex(@submitter)
|
||||||
with_urls: true,
|
|
||||||
with_events: false,
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
||||||
params:)
|
with_events: false, params:)
|
||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
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,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ 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,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [], value: [], preferences: {} }]] }
|
{ default_value: [], value: [], preferences: {} }]] }
|
||||||
)
|
)
|
||||||
@@ -162,7 +164,7 @@ module Api
|
|||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
if params[:template_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
|
end
|
||||||
|
|
||||||
maybe_filder_by_completed_at(submitters, params)
|
maybe_filder_by_completed_at(submitters, params)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:manage, @template)
|
authorize!(:create, @template)
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
@@ -21,17 +21,20 @@ module Api
|
|||||||
)
|
)
|
||||||
|
|
||||||
cloned_template.source = :api
|
cloned_template.source = :api
|
||||||
cloned_template.save!
|
|
||||||
|
|
||||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
||||||
original_template: @template,
|
original_template: @template,
|
||||||
documents: params[:documents])
|
documents: params[:documents])
|
||||||
|
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||||
'webhook_url_id' => webhook_url.id)
|
'webhook_url_id' => webhook_url.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ module Api
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||||
'webhook_url_id' => webhook_url.id)
|
'webhook_url_id' => webhook_url.id)
|
||||||
@@ -86,12 +88,17 @@ module Api
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filter_templates(templates, params)
|
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 = 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[:application_key]) if params[:application_key].present?
|
||||||
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
|
||||||
|
if params[:folder].present?
|
||||||
|
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:folder])
|
||||||
|
|
||||||
|
templates = folder ? templates.where(folder:) : templates.none
|
||||||
|
end
|
||||||
|
|
||||||
templates
|
templates
|
||||||
end
|
end
|
||||||
@@ -100,6 +107,7 @@ module Api
|
|||||||
permitted_params = [
|
permitted_params = [
|
||||||
:name,
|
:name,
|
||||||
:external_id,
|
:external_id,
|
||||||
|
:shared_link,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base
|
|||||||
request.session[:impersonated_user_id] = user.uuid
|
request.session[:impersonated_user_id] = user.uuid
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def with_locale(&)
|
def with_locale(&)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(email_configs)
|
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')
|
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
|
|||||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||||
|
|
||||||
ENTERPRISE_PATHS = [
|
ENTERPRISE_PATHS = [
|
||||||
|
'/submissions/html',
|
||||||
|
'/api/submissions/html',
|
||||||
'/templates/html',
|
'/templates/html',
|
||||||
'/api/templates/html',
|
'/api/templates/html',
|
||||||
|
'/submissions/pdf',
|
||||||
|
'/api/submissions/pdf',
|
||||||
'/templates/pdf',
|
'/templates/pdf',
|
||||||
'/api/templates/pdf',
|
'/api/templates/pdf',
|
||||||
'/templates/doc',
|
'/templates/doc',
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
@template = attachment.record
|
||||||
|
|
||||||
preview_image = attachment.preview_images.joins(:blob)
|
preview_image = attachment.preview_images.joins(:blob)
|
||||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SearchEntriesReindexController < ApplicationController
|
||||||
|
def create
|
||||||
|
authorize!(:manage, EncryptedConfig)
|
||||||
|
|
||||||
|
ReindexAllSearchEntriesJob.perform_async
|
||||||
|
|
||||||
|
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
|
||||||
|
.update!(value: true)
|
||||||
|
|
||||||
|
Docuseal.instance_variable_set(:@fulltext_search, nil)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_account_path,
|
||||||
|
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,27 +10,37 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
SEND_DURATION = 30.minutes
|
SEND_DURATION = 30.minutes
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@submitter =
|
if params[:template_slug]
|
||||||
if params[:template_slug]
|
template = Template.find_by!(slug: params[:template_slug])
|
||||||
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
|
||||||
template: { slug: params[:template_slug] })
|
@submitter =
|
||||||
elsif params[:submission_slug]
|
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||||
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
elsif params[:submission_slug]
|
||||||
submission: { slug: params[:submission_slug] })
|
submission = Submission.find_by(slug: params[:submission_slug])
|
||||||
else
|
|
||||||
Submitter.find_by!(slug: params[:submitter_slug])
|
if submission
|
||||||
|
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
||||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
|
||||||
end
|
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
f.json { head :ok }
|
f.json { head :ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def already_sent?(submitter)
|
||||||
|
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
|
|||||||
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
||||||
]
|
]
|
||||||
@account.encrypted_configs.create!(encrypted_configs)
|
@account.encrypted_configs.create!(encrypted_configs)
|
||||||
|
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
|
||||||
|
|
||||||
Docuseal.refresh_default_url_options!
|
Docuseal.refresh_default_url_options!
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,28 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed]
|
around_action :with_browser_locale, only: %i[show completed]
|
||||||
before_action :maybe_redirect_com, 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 :load_template
|
||||||
|
before_action :authorize_start!, only: :update
|
||||||
|
|
||||||
def show
|
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)
|
if @template.shared_link?
|
||||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
@template.submitters.first)['uuid'])
|
.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
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
|
||||||
|
|
||||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
@@ -42,9 +51,13 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
if is_new_record
|
if is_new_record
|
||||||
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
|
enqueue_submission_create_webhooks(@submitter)
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
SearchEntries.enqueue_reindex(@submitter)
|
||||||
|
|
||||||
|
if @submitter.submission.expire_at?
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
||||||
|
'submission_id' => @submitter.submission_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,6 +69,8 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
|
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
|
||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions)
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.find_by!(email: params[:email])
|
.find_by!(email: params[:email])
|
||||||
@@ -63,32 +78,55 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
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)
|
def find_or_initialize_submitter(template, submitter_params)
|
||||||
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
Submitter
|
||||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||||
.order(id: :desc)
|
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||||
.where(declined_at: nil)
|
.order(id: :desc)
|
||||||
.where(external_id: nil)
|
.where(declined_at: nil)
|
||||||
.where(ip: [nil, request.remote_ip])
|
.where(external_id: nil)
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.where(ip: [nil, request.remote_ip])
|
||||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
.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
|
end
|
||||||
|
|
||||||
def assign_submission_attributes(submitter, template)
|
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(
|
submitter.assign_attributes(
|
||||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
||||||
ip: request.remote_ip,
|
ip: request.remote_ip,
|
||||||
ua: request.user_agent,
|
ua: request.user_agent,
|
||||||
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||||
metadata: resubmit_submitter&.metadata.presence || {}
|
metadata: @resubmit_submitter&.metadata.presence || {}
|
||||||
)
|
)
|
||||||
|
|
||||||
if submitter.values.present?
|
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)
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -96,6 +134,7 @@ class StartFormController < ApplicationController
|
|||||||
submitter.submission ||= Submission.new(template:,
|
submitter.submission ||= Submission.new(template:,
|
||||||
account_id: template.account_id,
|
account_id: template.account_id,
|
||||||
template_submitters: template.submitters,
|
template_submitters: template.submitters,
|
||||||
|
expire_at: Templates.build_default_expire_at(template),
|
||||||
submitters: [submitter],
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
@@ -105,19 +144,25 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_undefined_submitters(template)
|
def filter_undefined_submitters(template)
|
||||||
Templates.filter_undefined_submitters(template)
|
Templates.filter_undefined_submitters(template.submitters)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
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|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
slug = params[:slug] || params[:start_form_slug]
|
@template =
|
||||||
|
if @resubmit_submitter
|
||||||
@template = Template.find_by!(slug:)
|
@resubmit_submitter.template
|
||||||
|
else
|
||||||
|
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def multiple_submitters_error_message
|
def multiple_submitters_error_message
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, parent: false
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.joins(:template)
|
@submissions = @submissions.left_joins(:template)
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
.or(@submissions.where.not(templates: { 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(current_user, @submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@@ -20,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,6 +8,10 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||||
|
|
||||||
|
before_action only: :create do
|
||||||
|
authorize!(:create, Submission)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
@@ -26,8 +30,6 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
|
||||||
|
|
||||||
save_template_message(@template, params) if params[:save_message] == '1'
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
if params[:is_custom_message] != '1'
|
if params[:is_custom_message] != '1'
|
||||||
@@ -56,6 +58,8 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||||
rescue Submissions::CreateFromSubmitters::BaseError => e
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
render turbo_stream: turbo_stream.replace(:submitters_error,
|
render turbo_stream: turbo_stream.replace(:submitters_error,
|
||||||
@@ -81,7 +85,7 @@ class SubmissionsController < ApplicationController
|
|||||||
I18n.t('submission_has_been_archived')
|
I18n.t('submission_has_been_archived')
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, parent: false
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.joins(:template)
|
@submissions = @submissions.left_joins(:template)
|
||||||
|
|
||||||
@submissions = @submissions.where(archived_at: nil)
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
.where(templates: { 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(current_user, @submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@@ -21,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,20 +8,20 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
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 =
|
signature_valid =
|
||||||
if submitter&.slug == params[:submitter_slug]
|
if @submitter&.slug == params[:submitter_slug]
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
submitter = nil
|
@submitter = nil
|
||||||
end
|
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
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(submitter)
|
url = build_combined_url(@submitter)
|
||||||
|
|
||||||
if url
|
if url
|
||||||
render json: [url]
|
render json: [url]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsPreviewController < ApplicationController
|
class SubmissionsPreviewController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :load_submitter, only: %i[show update completed]
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
|
before_action :maybe_render_locked_page, only: :show
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
@@ -15,19 +16,14 @@ class SubmitFormController < ApplicationController
|
|||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
return render :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)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
return render :awaiting if (@form_configs[:enforce_signing_order] ||
|
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.current_submitter_order?(@submitter)
|
||||||
|
|
||||||
Submitters.preload_with_pages(@submitter)
|
Submissions.preload_with_pages(submission)
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
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
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||||
end
|
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
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -84,6 +80,15 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
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
|
def load_submitter
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
submitter.completed_at? ||
|
submitter.completed_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
submitter.submission.expired? ||
|
||||||
submitter.submission.template.archived_at?
|
submitter.submission.template&.archived_at?
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
submitter.update!(declined_at: Time.current)
|
submitter.update!(declined_at: Time.current)
|
||||||
|
|||||||
@@ -7,25 +7,28 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
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? ||
|
return head :unprocessable_entity if @submitter.declined_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
submitter.submission.template.archived_at?
|
@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
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.max_by(&:completed_at)
|
.max_by(&:completed_at)
|
||||||
|
|
||||||
attachments =
|
attachments =
|
||||||
if last_completed_submitter
|
if last_completed_submitter
|
||||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||||
else
|
else
|
||||||
submitter.submission.template.schema_documents.preload(:blob)
|
@submitter.submission.schema_documents.preload(:blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
urls = attachments.map do |attachment|
|
urls = attachments.map do |attachment|
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
|
|
||||||
def search_submitters(submitters)
|
def search_submitters(submitters)
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
if Docuseal.fulltext_search?
|
||||||
|
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
|
else
|
||||||
Submitters.search(submitters, params[:q])
|
Submitters.search(current_user, submitters, params[:q])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -38,6 +38,8 @@ class SubmittersController < ApplicationController
|
|||||||
if @submitter.save
|
if @submitter.save
|
||||||
maybe_resend_email_sms(@submitter, params)
|
maybe_resend_email_sms(@submitter, params)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@submitter)
|
||||||
|
|
||||||
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ class SubmittersResubmitController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
|
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,
|
submission = @submitter.account.submissions.new(created_by_user: current_user,
|
||||||
submitters_order: :preserved,
|
submitters_order: :preserved,
|
||||||
**@submitter.submission.slice(:template_fields,
|
**@submitter.submission.slice(:template_fields,
|
||||||
:account_id,
|
:account_id,
|
||||||
:template_schema,
|
:name,
|
||||||
:template_submitters,
|
:template_id,
|
||||||
:preferences))
|
:template_schema,
|
||||||
|
:template_submitters,
|
||||||
|
:preferences))
|
||||||
|
|
||||||
@submitter.submission.submitters.each do |submitter|
|
@submitter.submission.submitters.each do |submitter|
|
||||||
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
|
||||||
@@ -27,6 +29,10 @@ class SubmittersResubmitController < ApplicationController
|
|||||||
|
|
||||||
submission.save!
|
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)
|
redirect_to submit_form_path(slug: @new_submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ class TemplateFoldersAutocompleteController < ApplicationController
|
|||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
def index
|
def index
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
|
||||||
|
|
||||||
|
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ class TemplateFoldersController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||||
.preload(:author, :template_accesses)
|
.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])
|
@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
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
@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
|
||||||
end
|
end
|
||||||
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@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 = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
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
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
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
|
||||||
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
@@ -21,9 +21,7 @@ class TemplatesController < ApplicationController
|
|||||||
submissions.order(id: :desc)
|
submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
@@ -74,6 +72,8 @@ class TemplatesController < ApplicationController
|
|||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
enqueue_template_created_webhooks(@template)
|
enqueue_template_created_webhooks(@template)
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
@@ -83,7 +83,13 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
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)
|
enqueue_template_updated_webhooks(@template)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
items: FOLDERS_PER_PAGE,
|
limit: FOLDERS_PER_PAGE,
|
||||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
@templates = @templates.none
|
@templates = @templates.none
|
||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
@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])
|
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
limit =
|
limit =
|
||||||
@@ -35,27 +35,38 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
(@template_folders.size < 7 ? 9 : 6)
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, limit:)
|
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||||
|
|
||||||
|
load_related_submissions if params[:q].present? && @templates.blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_templates(templates)
|
def filter_templates(templates)
|
||||||
rel = templates.active.preload(:author, :template_accesses)
|
rel = templates.active
|
||||||
|
|
||||||
if params[:q].blank?
|
if params[:q].blank?
|
||||||
if Docuseal.multitenant? && !current_account.testing?
|
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
shared_account_ids = [current_user.account_id]
|
||||||
else
|
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
||||||
shared_template_ids =
|
|
||||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
|
||||||
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
Templates.search(current_user, rel, params[:q])
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_template_folders(template_folders, current_user, order)
|
def sort_template_folders(template_folders, current_user, order)
|
||||||
@@ -91,4 +102,19 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
template_folders.order(id: :desc)
|
template_folders.order(id: :desc)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
@related_submissions = Submission.accessible_by(current_ability)
|
||||||
|
.left_joins(:template)
|
||||||
|
.where(archived_at: nil)
|
||||||
|
.where(templates: { archived_at: nil })
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -13,7 +13,7 @@ class TemplatesFormPreviewController < ApplicationController
|
|||||||
|
|
||||||
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
|
@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)
|
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
|
|||||||
@@ -25,13 +25,21 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa
|
||||||
|
default_expire_at_duration
|
||||||
|
default_expire_at
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
submitters: [%i[uuid request_email_subject request_email_body]]]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
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|
|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||||
if %w[true false].include?(value)
|
if %w[true false].include?(value)
|
||||||
value == 'true'
|
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)
|
enqueue_template_created_webhooks(@template)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
|
|||||||
@users.active.where.not(role: 'integration')
|
@users.active.where.not(role: 'integration')
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form'
|
|||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
import CheckOnClick from './elements/check_on_click'
|
||||||
import PasswordInput from './elements/password_input'
|
import PasswordInput from './elements/password_input'
|
||||||
import SearchInput from './elements/search_input'
|
import SearchInput from './elements/search_input'
|
||||||
import ToggleAttribute from './elements/toggle_attribute'
|
import ToggleAttribute from './elements/toggle_attribute'
|
||||||
@@ -33,6 +34,7 @@ import MaskedInput from './elements/masked_input'
|
|||||||
import SetDateButton from './elements/set_date_button'
|
import SetDateButton from './elements/set_date_button'
|
||||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
import AppTour from './elements/app_tour'
|
import AppTour from './elements/app_tour'
|
||||||
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -101,6 +103,8 @@ safeRegisterElement('masked-input', MaskedInput)
|
|||||||
safeRegisterElement('set-date-button', SetDateButton)
|
safeRegisterElement('set-date-button', SetDateButton)
|
||||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||||
safeRegisterElement('app-tour', AppTour)
|
safeRegisterElement('app-tour', AppTour)
|
||||||
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
|
safeRegisterElement('check-on-click', CheckOnClick)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -125,6 +129,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withReplaceAndCloneUpload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
|
|||||||
@@ -0,0 +1,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.clearChecked()
|
||||||
|
|
||||||
this.addEventListener('click', (e) => {
|
this.addEventListener('click', (e) => {
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
const text = this.dataset.text || this.innerText.trim()
|
const text = this.dataset.text || this.innerText.trim()
|
||||||
|
|
||||||
if (navigator.clipboard) {
|
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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
'loading',
|
'loading',
|
||||||
'icon',
|
'icon',
|
||||||
'input'
|
'input',
|
||||||
|
'area'
|
||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.addEventListener('drop', this.onDrop)
|
|
||||||
|
|
||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
|
this.addEventListener('drop', this.onDrop)
|
||||||
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
||||||
|
this.area?.addEventListener('dragover', this.onDragover)
|
||||||
|
this.area?.addEventListener('dragleave', this.onDragleave)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('drop', this.onDrop)
|
||||||
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
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) {
|
onDrop (e) {
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const q = text.split(/[;,\s]+/).pop().trim()
|
const q = this.dataset.field === 'email'
|
||||||
|
? text.split(/[;,\s]+/).pop().trim()
|
||||||
|
: text
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||||
|
|
||||||
elementIds.forEach((elementId) => {
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
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">
|
<template v-if="area.option_uuid && !option.value">
|
||||||
{{ optionValue(option) }}
|
{{ optionValue(option) }}
|
||||||
@@ -55,10 +55,12 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
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
|
<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%"
|
style="min-height: 50%"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -67,8 +69,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withSignatureId"
|
v-if="withSignatureId || field.preferences?.reason_field_uuid"
|
||||||
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||||
|
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||||
>
|
>
|
||||||
<div class="truncate uppercase">
|
<div class="truncate uppercase">
|
||||||
ID: {{ signature.uuid }}
|
ID: {{ signature.uuid }}
|
||||||
@@ -444,6 +447,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
|
},
|
||||||
|
isNarrow () {
|
||||||
|
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<div
|
<div
|
||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
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 }"
|
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
@@ -1133,7 +1133,7 @@ export default {
|
|||||||
parent.style.overflow = 'hidden'
|
parent.style.overflow = 'hidden'
|
||||||
|
|
||||||
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
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'
|
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1390,6 +1390,7 @@ export default {
|
|||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
|
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||||
}
|
}
|
||||||
|
|
||||||
let saveStepRequest
|
let saveStepRequest
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
computedPreviousValue () {
|
computedPreviousValue () {
|
||||||
if (this.isUsePreviousValue) {
|
if (this.isUsePreviousValue && this.field.required === true) {
|
||||||
return this.previousValue
|
return this.previousValue
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||||
>
|
>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
v-if="format === 'upload' && !modelValue"
|
v-if="format === 'upload' && !modelValue && !computedPreviousValue"
|
||||||
:message="`${t('upload')} ${field.name || t('signature')}`"
|
:message="`${t('upload')} ${field.name || t('signature')}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
@@ -414,7 +414,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computedPreviousValue () {
|
computedPreviousValue () {
|
||||||
if (this.isUsePreviousValue) {
|
if (this.isUsePreviousValue && this.field.required === true) {
|
||||||
return this.previousValue
|
return this.previousValue
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -151,8 +151,10 @@ export default {
|
|||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
textarea.style.height = 'auto'
|
if (textarea) {
|
||||||
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toggleTextArea () {
|
toggleTextArea () {
|
||||||
this.isTextArea = true
|
this.isTextArea = true
|
||||||
|
|||||||
@@ -5,7 +5,19 @@
|
|||||||
class="mx-auto pl-3 h-full"
|
class="mx-auto pl-3 h-full"
|
||||||
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
||||||
@dragover="onDragover"
|
@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
|
<DragPlaceholder
|
||||||
ref="dragPlaceholder"
|
ref="dragPlaceholder"
|
||||||
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
||||||
@@ -67,13 +79,52 @@
|
|||||||
name="buttons"
|
name="buttons"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<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
|
<a
|
||||||
v-if="withSignYourselfButton"
|
v-else-if="withSignYourselfButton"
|
||||||
id="sign_yourself_button"
|
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"
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
:target="template.submitters.length > 1 ? '' : '_blank'"
|
data-turbo-frame="modal"
|
||||||
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
|
||||||
@click="maybeShowErrorTemplateAlert"
|
@click="maybeShowErrorTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconWritingSign
|
<IconWritingSign
|
||||||
@@ -433,6 +484,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import Dropzone from './dropzone'
|
import Dropzone from './dropzone'
|
||||||
|
import HoverDropzone from './hover_dropzone'
|
||||||
import DragPlaceholder from './drag_placeholder'
|
import DragPlaceholder from './drag_placeholder'
|
||||||
import Fields from './fields'
|
import Fields from './fields'
|
||||||
import MobileDrawField from './mobile_draw_field'
|
import MobileDrawField from './mobile_draw_field'
|
||||||
@@ -462,6 +514,7 @@ export default {
|
|||||||
MobileFields,
|
MobileFields,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
|
HoverDropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
DocumentControls,
|
DocumentControls,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
@@ -665,6 +718,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withReplaceAndCloneUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -724,7 +782,8 @@ export default {
|
|||||||
copiedArea: null,
|
copiedArea: null,
|
||||||
drawFieldType: null,
|
drawFieldType: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null
|
dragField: null,
|
||||||
|
isDragFile: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -836,6 +895,7 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
window.addEventListener('dragleave', this.onWindowDragLeave)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (document.location.search?.includes('stripe_connect_success')) {
|
if (document.location.search?.includes('stripe_connect_success')) {
|
||||||
@@ -854,6 +914,7 @@ export default {
|
|||||||
window.removeEventListener('keydown', this.onKeyDown)
|
window.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
|
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
@@ -868,6 +929,13 @@ export default {
|
|||||||
|
|
||||||
ref.x = e.clientX - ref.offsetX
|
ref.x = e.clientX - ref.offsetX
|
||||||
ref.y = e.clientY - ref.offsetY
|
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) {
|
reorderFields (item) {
|
||||||
@@ -984,6 +1052,12 @@ export default {
|
|||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'datenow') {
|
||||||
|
field.type = 'date'
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: this.defaultDateFormat
|
format: this.defaultDateFormat
|
||||||
@@ -1250,7 +1324,7 @@ export default {
|
|||||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDraw (area) {
|
onDraw ({ area, isTooSmall }) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
if (this.drawOption) {
|
if (this.drawOption) {
|
||||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
@@ -1345,7 +1419,7 @@ export default {
|
|||||||
area.y -= area.h / 2
|
area.y -= area.h / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if (area.w) {
|
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
|
||||||
this.addField(type, area)
|
this.addField(type, area)
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
@@ -1382,6 +1456,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'datenow') {
|
||||||
|
field.type = 'date'
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
if (['stamp', 'heading'].includes(field.type)) {
|
if (['stamp', 'heading'].includes(field.type)) {
|
||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
@@ -1529,6 +1609,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}, 'image/png')
|
}, 'image/png')
|
||||||
},
|
},
|
||||||
|
onUploadFailed (error) {
|
||||||
|
if (error) alert(error)
|
||||||
|
},
|
||||||
updateFromUpload (data) {
|
updateFromUpload (data) {
|
||||||
this.template.schema.push(...data.schema)
|
this.template.schema.push(...data.schema)
|
||||||
this.template.documents.push(...data.documents)
|
this.template.documents.push(...data.documents)
|
||||||
@@ -1649,6 +1732,29 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
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) {
|
moveDocument (item, direction) {
|
||||||
const currentIndex = this.template.schema.indexOf(item)
|
const currentIndex = this.template.schema.indexOf(item)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||||
@remove-area="$emit('remove-area', $event)"
|
@remove-area="$emit('remove-area', $event)"
|
||||||
@scroll-to="scrollToArea"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,34 +2,42 @@
|
|||||||
<div
|
<div
|
||||||
class="flex h-60 w-full"
|
class="flex h-60 w-full"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
|
@dragenter="isDragEntering = true"
|
||||||
|
@dragleave="isDragEntering = false"
|
||||||
@drop.prevent="onDropFiles"
|
@drop.prevent="onDropFiles"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
id="document_dropzone"
|
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"
|
: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">
|
<div class="flex flex-col items-center">
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isLoading || isProcessing"
|
v-if="isLoading"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
/>
|
/>
|
||||||
<IconCloudUpload
|
<component
|
||||||
|
:is="icon"
|
||||||
v-else
|
v-else
|
||||||
|
class="stroke-[1.5px]"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="message"
|
v-if="message"
|
||||||
class="font-medium text-lg mb-1"
|
class="text-lg mb-1"
|
||||||
|
:class="{ 'mt-1': !withDescription, 'font-medium': withDescription }"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</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') }}
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,13 +62,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileDropzone',
|
name: 'FileDropzone',
|
||||||
components: {
|
components: {
|
||||||
|
IconFilePlus,
|
||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop,
|
||||||
|
IconFileSymlink,
|
||||||
|
IconFiles
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 't'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
@@ -68,35 +79,70 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
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: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success', 'error', 'loading'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isProcessing: false
|
isDragEntering: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
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 () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading')
|
return this.t('uploading')
|
||||||
} else if (this.isProcessing) {
|
|
||||||
return this.t('processing_')
|
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
} 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 {
|
} 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: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
|
|||||||
@@ -424,7 +424,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</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
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
: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
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -101,6 +101,7 @@ export default {
|
|||||||
signature: this.t('signature'),
|
signature: this.t('signature'),
|
||||||
initials: this.t('initials'),
|
initials: this.t('initials'),
|
||||||
date: this.t('date'),
|
date: this.t('date'),
|
||||||
|
datenow: this.t('date_signed'),
|
||||||
number: this.t('number'),
|
number: this.t('number'),
|
||||||
image: this.t('image'),
|
image: this.t('image'),
|
||||||
file: this.t('file'),
|
file: this.t('file'),
|
||||||
@@ -142,6 +143,7 @@ export default {
|
|||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
|
datenow: IconCalendarCheck,
|
||||||
number: IconSquareNumber1,
|
number: IconSquareNumber1,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
@@ -164,7 +166,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<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`"
|
:id="`${type}_type_field_button`"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
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')"
|
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.com/contact"
|
href="https://www.docuseal.com/qualified-electronic-signature"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
@@ -347,7 +347,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
font: 'Font',
|
font: 'Font',
|
||||||
party: 'Party',
|
party: 'Party',
|
||||||
|
date_signed: 'Date Signed',
|
||||||
method: 'Method',
|
method: 'Method',
|
||||||
reorder_fields: 'Reorder fields',
|
reorder_fields: 'Reorder fields',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
@@ -63,6 +64,9 @@ const en = {
|
|||||||
processing_: 'Processing...',
|
processing_: 'Processing...',
|
||||||
add_pdf_documents_or_images: 'Add PDF documents or images',
|
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||||
add_documents_or_images: 'Add 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',
|
required: 'Required',
|
||||||
default_value: 'Default value',
|
default_value: 'Default value',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -164,6 +168,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
date_signed: 'Fecha actual',
|
||||||
fuente: 'Fuente',
|
fuente: 'Fuente',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
@@ -225,6 +230,9 @@ const es = {
|
|||||||
processing_: 'Procesando...',
|
processing_: 'Procesando...',
|
||||||
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
||||||
add_documents_or_images: 'Agregar documentos 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',
|
required: 'Requerido',
|
||||||
default_value: 'Valor predeterminado',
|
default_value: 'Valor predeterminado',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -329,6 +337,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
date_signed: 'Data attuale',
|
||||||
font: 'Carattere',
|
font: 'Carattere',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Metodo',
|
method: 'Metodo',
|
||||||
@@ -393,6 +402,9 @@ const it = {
|
|||||||
processing_: 'Elaborazione...',
|
processing_: 'Elaborazione...',
|
||||||
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
||||||
add_documents_or_images: 'Aggiungi documenti 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',
|
required: 'Obbligatorio',
|
||||||
default_value: 'Valore predefinito',
|
default_value: 'Valore predefinito',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -494,6 +506,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
date_signed: 'Data atual',
|
||||||
fonte: 'Fonte',
|
fonte: 'Fonte',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
@@ -555,6 +568,9 @@ const pt = {
|
|||||||
processing_: 'Processando...',
|
processing_: 'Processando...',
|
||||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||||
add_documents_or_images: 'Adicionar documentos 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',
|
required: 'Obrigatório',
|
||||||
default_value: 'Valor padrão',
|
default_value: 'Valor padrão',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -659,6 +675,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
date_signed: 'Date actuelle',
|
||||||
font: 'Police',
|
font: 'Police',
|
||||||
party: 'Partie',
|
party: 'Partie',
|
||||||
method: 'Méthode',
|
method: 'Méthode',
|
||||||
@@ -719,6 +736,9 @@ const fr = {
|
|||||||
processing_: 'Traitement en cours...',
|
processing_: 'Traitement en cours...',
|
||||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
||||||
add_documents_or_images: 'Ajoutez des documents 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',
|
required: 'Requis',
|
||||||
default_value: 'Valeur par défaut',
|
default_value: 'Valeur par défaut',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -824,6 +844,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
date_now: 'Akt. Datum',
|
||||||
font: 'Schriftart',
|
font: 'Schriftart',
|
||||||
party: 'Partei',
|
party: 'Partei',
|
||||||
method: 'Verfahren',
|
method: 'Verfahren',
|
||||||
@@ -884,6 +905,9 @@ const de = {
|
|||||||
processing_: 'Verarbeitung...',
|
processing_: 'Verarbeitung...',
|
||||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||||
add_documents_or_images: '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',
|
required: 'Erforderlich',
|
||||||
default_value: 'Standardwert',
|
default_value: 'Standardwert',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
: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
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
|
|||||||
@@ -290,7 +290,12 @@ export default {
|
|||||||
area.cell_w = this.newArea.cell_w
|
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
|
this.showMask = false
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<label
|
<label
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
|
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
|
||||||
:class="{ 'opacity-100': isLoading || isProcessing }"
|
:class="{ 'opacity-100': isLoading }"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<form
|
<form
|
||||||
@@ -41,19 +41,19 @@ export default {
|
|||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
isProcessing: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
},
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading_')
|
return this.t('uploading_')
|
||||||
} else if (this.isProcessing) {
|
|
||||||
return this.t('processing_')
|
|
||||||
} else {
|
} else {
|
||||||
return this.t('replace')
|
return this.t('replace')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
id="add_document_button"
|
id="add_document_button"
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="btn btn-outline w-full add-document-button"
|
class="btn btn-outline w-full add-document-button"
|
||||||
:class="{ 'btn-disabled': isLoading || isProcessing }"
|
:class="{ 'btn-disabled': isLoading }"
|
||||||
>
|
>
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isLoading || isProcessing"
|
v-if="isLoading"
|
||||||
width="20"
|
width="20"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
/>
|
/>
|
||||||
@@ -18,9 +18,6 @@
|
|||||||
<span v-if="isLoading">
|
<span v-if="isLoading">
|
||||||
{{ t('uploading_') }}
|
{{ t('uploading_') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="isProcessing">
|
|
||||||
{{ t('processing_') }}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ t('add_document') }}
|
{{ t('add_document') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -63,30 +60,34 @@ export default {
|
|||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success', 'error'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
isProcessing: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async upload () {
|
async upload () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
this.baseFetch(this.uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then((resp) => {
|
}).then((resp) => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
resp.json().then((data) => {
|
resp.json().then((data) => {
|
||||||
this.$emit('success', data)
|
this.$emit('success', data)
|
||||||
this.$refs.input.value = ''
|
this.$refs.input.value = ''
|
||||||
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
} else if (resp.status === 422) {
|
} else if (resp.status === 422) {
|
||||||
resp.json().then((data) => {
|
resp.json().then((data) => {
|
||||||
@@ -95,21 +96,33 @@ export default {
|
|||||||
|
|
||||||
formData.append('password', prompt(this.t('enter_pdf_password')))
|
formData.append('password', prompt(this.t('enter_pdf_password')))
|
||||||
|
|
||||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
this.baseFetch(this.uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
this.$emit('success', await resp.json())
|
this.$emit('success', await resp.json())
|
||||||
this.$refs.input.value = ''
|
this.$refs.input.value = ''
|
||||||
|
this.isLoading = false
|
||||||
} else {
|
} else {
|
||||||
alert(this.t('wrong_password'))
|
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
|
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
|
||||||
@@ -7,7 +7,7 @@ class ProcessSubmissionExpiredJob
|
|||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find(params['submission_id'])
|
||||||
|
|
||||||
return if submission.archived_at?
|
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 if submission.submitters.where.not(declined_at: nil).exists?
|
||||||
return unless submission.submitters.exists?(completed_at: nil)
|
return unless submission.submitters.exists?(completed_at: nil)
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
user = submission.created_by_user || submitter.template.author
|
user = submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
|
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) &&
|
if submission.submitters.map(&:email).exclude?(user.email) &&
|
||||||
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
|
||||||
user.role != 'integration'
|
user.role != 'integration'
|
||||||
@@ -98,7 +98,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def maybe_enqueue_copy_emails(submitter)
|
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,
|
configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
@@ -119,7 +119,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
|
|
||||||
def build_bcc_addresses(submission)
|
def build_bcc_addresses(submission)
|
||||||
bcc = submission.preferences['bcc_completed'].presence ||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
submission.template.preferences['bcc_completed'].presence ||
|
submission.template&.preferences&.dig('bcc_completed').presence ||
|
||||||
submission.account.account_configs
|
submission.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ReindexAllSearchEntriesJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
def perform
|
||||||
|
SearchEntries.reindex_all
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -20,7 +20,7 @@ class ApplicationMailer < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_message_metadata
|
def set_message_metadata
|
||||||
message.instance_variable_set(:@message_metadata, @message_metadata)
|
message.instance_variable_set(:@message_metadata, @message_metadata || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_message_uuid
|
def set_message_uuid
|
||||||
@@ -28,10 +28,14 @@ class ApplicationMailer < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assign_message_metadata(tag, record)
|
def assign_message_metadata(tag, record)
|
||||||
@message_metadata = {
|
@message_metadata = (@message_metadata || {}).merge(
|
||||||
'tag' => tag,
|
'tag' => tag,
|
||||||
'record_id' => record.id,
|
'record_id' => record.id,
|
||||||
'record_type' => record.class.name
|
'record_type' => record.class.name
|
||||||
}
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_metadata(attrs)
|
||||||
|
@message_metadata = (@message_metadata || {}).merge(attrs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -23,11 +23,11 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
@body = @email_message&.body.presence ||
|
@body = @email_message&.body.presence ||
|
||||||
template_submitters_index.dig(@submitter.uuid, 'request_email_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 ||
|
@subject = @email_message&.subject.presence ||
|
||||||
template_submitters_index.dig(@submitter.uuid, 'request_email_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)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
@@ -53,6 +53,8 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@submission = submitter.submission
|
@submission = submitter.submission
|
||||||
@user = user
|
@user = user
|
||||||
|
|
||||||
|
template_preferences = @submission.template&.preferences || {}
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
@@ -60,15 +62,15 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
add_completed_email_attachments!(
|
add_completed_email_attachments!(
|
||||||
submitter,
|
submitter,
|
||||||
with_documents: @email_config&.value&.dig('attach_documents') != false &&
|
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 &&
|
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
|
@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
|
@body ||= @email_config.value['body'] if @email_config
|
||||||
|
|
||||||
assign_message_metadata('submitter_completed', @submitter)
|
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,
|
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
|
||||||
reply_to: @submitter.friendly_name,
|
reply_to: @submitter.friendly_name,
|
||||||
subject: I18n.t(:name_declined_by_submitter,
|
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))
|
submitter: @submitter.name || @submitter.email || @submitter.phone))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -107,22 +109,24 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
|
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
|
||||||
|
|
||||||
|
template_preferences = @submitter.template&.preferences || {}
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
add_completed_email_attachments!(
|
add_completed_email_attachments!(
|
||||||
submitter,
|
submitter,
|
||||||
with_documents: @submitter.template.preferences['documents_copy_email_attach_documents'] != false &&
|
with_documents: template_preferences['documents_copy_email_attach_documents'] != false &&
|
||||||
(@email_config.nil? || @email_config.value['attach_documents'] != false),
|
(@email_config.nil? || @email_config.value['attach_documents'] != false),
|
||||||
with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false &&
|
with_audit_log: template_preferences['documents_copy_email_attach_audit'] != false &&
|
||||||
(@email_config.nil? || @email_config.value['attach_audit_log'] != 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
|
@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
|
@body ||= @email_config.value['body'] if @email_config
|
||||||
|
|
||||||
assign_message_metadata('submitter_documents_copy', @submitter)
|
assign_message_metadata('submitter_documents_copy', @submitter)
|
||||||
@@ -130,11 +134,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
I18n.with_locale(@current_account.locale) do
|
I18n.with_locale(@current_account.locale) do
|
||||||
subject =
|
subject =
|
||||||
if @subject.present?
|
@subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy)
|
||||||
ReplaceEmailVariables.call(@subject, submitter:)
|
|
||||||
else
|
|
||||||
I18n.t(:your_document_copy)
|
|
||||||
end
|
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: to || @submitter.friendly_name,
|
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)
|
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
|
||||||
reply_to = submitter.preferences['reply_to'].presence
|
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
|
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
|
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
|
||||||
@@ -212,7 +212,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_submitter_preferences_index(submitter)
|
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
|
end
|
||||||
|
|
||||||
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
|
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)
|
def from_address_for_submitter(submitter)
|
||||||
if submitter.submission.source.in?(%w[api embed]) &&
|
if submitter.submission.source.in?(%w[api embed]) &&
|
||||||
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
|
(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
|
from_email
|
||||||
else
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :account_linked_accounts, dependent: :destroy
|
has_many :account_linked_accounts, dependent: :destroy
|
||||||
has_many :email_events, dependent: :destroy
|
has_many :email_events, dependent: :destroy
|
||||||
has_many :webhook_urls, dependent: :destroy
|
has_many :webhook_urls, dependent: :destroy
|
||||||
|
has_many :account_accesses, dependent: :destroy
|
||||||
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||||
class_name: 'AccountLinkedAccount',
|
class_name: 'AccountLinkedAccount',
|
||||||
inverse_of: :account
|
inverse_of: :account
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -29,6 +29,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
||||||
|
ALLOW_TO_PARTIAL_DOWNLOAD_KEY = 'allow_to_partial_download'
|
||||||
SUBMITTER_REMINDERS = 'submitter_reminders'
|
SUBMITTER_REMINDERS = 'submitter_reminders'
|
||||||
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
|
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
@@ -41,6 +42,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||||
|
WITH_AUDIT_SUBMITTER_TIMEZONE_KEY = 'with_audit_submitter_timezone'
|
||||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||||
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# account_id :bigint not null
|
# account_id :bigint not null
|
||||||
# submission_id :bigint not null
|
# submission_id :bigint not null
|
||||||
# submitter_id :bigint not null
|
# submitter_id :bigint not null
|
||||||
# template_id :bigint not null
|
# template_id :bigint
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -24,7 +24,7 @@ class CompletedSubmitter < ApplicationRecord
|
|||||||
belongs_to :submitter
|
belongs_to :submitter
|
||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :template
|
belongs_to :template, optional: true
|
||||||
|
|
||||||
has_many :completed_documents, dependent: :destroy,
|
has_many :completed_documents, dependent: :destroy,
|
||||||
primary_key: :submitter_id,
|
primary_key: :submitter_id,
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_email_events_on_account_id (account_id)
|
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
||||||
# index_email_events_on_email (email)
|
# index_email_events_on_email (email)
|
||||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
|
||||||
# index_email_events_on_message_id (message_id)
|
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||||
|
# index_email_events_on_message_id (message_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# archived_at :datetime
|
# archived_at :datetime
|
||||||
# expire_at :datetime
|
# expire_at :datetime
|
||||||
|
# name :text
|
||||||
# preferences :text not null
|
# preferences :text not null
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# source :text not null
|
# source :text not null
|
||||||
@@ -18,14 +19,16 @@
|
|||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint not null
|
# account_id :bigint not null
|
||||||
# created_by_user_id :bigint
|
# created_by_user_id :bigint
|
||||||
# template_id :bigint not null
|
# template_id :bigint
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submissions_on_account_id_and_id (account_id,id)
|
# index_submissions_on_account_id_and_id (account_id,id)
|
||||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
|
||||||
# index_submissions_on_slug (slug) UNIQUE
|
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
|
||||||
# index_submissions_on_template_id (template_id)
|
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||||
|
# index_submissions_on_slug (slug) UNIQUE
|
||||||
|
# index_submissions_on_template_id (template_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
@@ -33,10 +36,12 @@
|
|||||||
# fk_rails_... (template_id => templates.id)
|
# fk_rails_... (template_id => templates.id)
|
||||||
#
|
#
|
||||||
class Submission < ApplicationRecord
|
class Submission < ApplicationRecord
|
||||||
belongs_to :template
|
belongs_to :template, optional: true
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
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 :submitters, dependent: :destroy
|
||||||
has_many :submission_events, dependent: :destroy
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|
||||||
@@ -56,6 +61,7 @@ class Submission < ApplicationRecord
|
|||||||
has_one_attached :combined_document
|
has_one_attached :combined_document
|
||||||
|
|
||||||
has_many_attached :preview_documents
|
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
|
has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false
|
||||||
|
|
||||||
@@ -65,13 +71,19 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
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 {
|
scope :completed, lambda {
|
||||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
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)
|
.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 :declined, lambda {
|
||||||
scope :expired, -> { where(expire_at: ..Time.current) }
|
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, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
@@ -90,6 +102,14 @@ class Submission < ApplicationRecord
|
|||||||
expire_at && expire_at <= Time.current
|
expire_at && expire_at <= Time.current
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def schema_documents
|
||||||
|
if template_id?
|
||||||
|
template_schema_documents
|
||||||
|
else
|
||||||
|
documents_attachments
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fields_uuid_index
|
def fields_uuid_index
|
||||||
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
|
|
||||||
enum :event_type, {
|
enum :event_type, {
|
||||||
send_email: 'send_email',
|
send_email: 'send_email',
|
||||||
|
bounce_email: 'bounce_email',
|
||||||
|
complaint_email: 'complaint_email',
|
||||||
send_reminder_email: 'send_reminder_email',
|
send_reminder_email: 'send_reminder_email',
|
||||||
send_sms: 'send_sms',
|
send_sms: 'send_sms',
|
||||||
send_2fa_sms: 'send_2fa_sms',
|
send_2fa_sms: 'send_2fa_sms',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
# preferences :text not null
|
# preferences :text not null
|
||||||
# sent_at :datetime
|
# sent_at :datetime
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
|
# timezone :string
|
||||||
# ua :string
|
# ua :string
|
||||||
# uuid :string not null
|
# uuid :string not null
|
||||||
# values :text not null
|
# values :text not null
|
||||||
@@ -27,11 +28,12 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submitters_on_account_id_and_id (account_id,id)
|
# index_submitters_on_account_id_and_id (account_id,id)
|
||||||
# index_submitters_on_email (email)
|
# index_submitters_on_completed_at_and_account_id (completed_at,account_id)
|
||||||
# index_submitters_on_external_id (external_id)
|
# index_submitters_on_email (email)
|
||||||
# index_submitters_on_slug (slug) UNIQUE
|
# index_submitters_on_external_id (external_id)
|
||||||
# index_submitters_on_submission_id (submission_id)
|
# index_submitters_on_slug (slug) UNIQUE
|
||||||
|
# index_submitters_on_submission_id (submission_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
@@ -41,6 +43,7 @@ class Submitter < ApplicationRecord
|
|||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :template, through: :submission
|
has_one :template, through: :submission
|
||||||
|
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
attribute :preferences, :string, default: -> { {} }
|
attribute :preferences, :string, default: -> { {} }
|
||||||
@@ -54,7 +57,7 @@ class Submitter < ApplicationRecord
|
|||||||
has_many_attached :documents
|
has_many_attached :documents
|
||||||
has_many_attached :attachments
|
has_many_attached :attachments
|
||||||
has_many_attached :preview_documents
|
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 :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
|
||||||
|
|
||||||
has_many :document_generation_events, dependent: :destroy
|
has_many :document_generation_events, dependent: :destroy
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
# name :string not null
|
# name :string not null
|
||||||
# preferences :text not null
|
# preferences :text not null
|
||||||
# schema :text not null
|
# schema :text not null
|
||||||
|
# shared_link :boolean default(FALSE), not null
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# source :text not null
|
# source :text not null
|
||||||
# submitters :text not null
|
# submitters :text not null
|
||||||
@@ -22,11 +23,13 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_templates_on_account_id (account_id)
|
# index_templates_on_account_id (account_id)
|
||||||
# index_templates_on_author_id (author_id)
|
# index_templates_on_account_id_and_folder_id_and_id (account_id,folder_id,id) WHERE (archived_at IS NULL)
|
||||||
# index_templates_on_external_id (external_id)
|
# index_templates_on_account_id_and_id_archived (account_id,id) WHERE (archived_at IS NOT NULL)
|
||||||
# index_templates_on_folder_id (folder_id)
|
# index_templates_on_author_id (author_id)
|
||||||
# index_templates_on_slug (slug) UNIQUE
|
# index_templates_on_external_id (external_id)
|
||||||
|
# index_templates_on_folder_id (folder_id)
|
||||||
|
# index_templates_on_slug (slug) UNIQUE
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
@@ -41,6 +44,8 @@ class Template < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :folder, class_name: 'TemplateFolder'
|
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
|
before_validation :maybe_set_default_folder, on: :create
|
||||||
|
|
||||||
attribute :preferences, :string, default: -> { {} }
|
attribute :preferences, :string, default: -> { {} }
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ class User < ApplicationRecord
|
|||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
scope :admins, -> { where(role: ADMIN_ROLE) }
|
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
|
def access_token
|
||||||
super || build_access_token.tap(&:save!)
|
super || build_access_token.tap(&:save!)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -165,6 +165,15 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= render 'extra_preferences' %>
|
||||||
|
<% if !Docuseal.multitenant? && SearchEntry.table_exists? && (!Docuseal.fulltext_search? || params[:reindex] == 'true') && can?(:manage, EncryptedConfig) %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Efficient search with search index
|
||||||
|
</span>
|
||||||
|
<%= button_to params[:reindex] == 'true' ? 'Reindex' : 'Build Search Index', settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'compliances' %>
|
<%= render 'compliances' %>
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
<div class="text-center">
|
<% eu_server = request.host == 'docuseal.eu' %>
|
||||||
<div class="join">
|
<server-selector>
|
||||||
<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' %>">
|
<div id="global_server_selector" class="text-center hidden">
|
||||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
<div class="join">
|
||||||
Global
|
<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 %>">
|
||||||
</a>
|
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||||
<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' %>">
|
Global
|
||||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
</a>
|
||||||
Europe
|
<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 %>">
|
||||||
</a>
|
<%= 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>
|
<div id="server_selector" class="flex justify-center <%= 'hidden' unless eu_server %>">
|
||||||
|
<div class="dropdown">
|
||||||
|
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
||||||
|
<% if eu_server %>
|
||||||
|
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||||
|
<span>EU Cloud</span>
|
||||||
|
<% else %>
|
||||||
|
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||||
|
<span>US Cloud</span>
|
||||||
|
<% end %>
|
||||||
|
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
|
||||||
|
</label>
|
||||||
|
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
|
||||||
|
<li>
|
||||||
|
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
|
||||||
|
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||||
|
US Cloud
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
|
||||||
|
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||||
|
EU Cloud
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</server-selector>
|
||||||
|
<%= render 'scripts/server_selector' %>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
<%= t('preferences') %>
|
<%= t('preferences') %>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<div class="flex items-center justify-between py-2.5">
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
|||||||
@@ -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 stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M3 9l4 -4l4 4m-4 -4v14" />
|
<path d="M3 9l4 -4l4 4m-4 -4v14" />
|
||||||
<path d="M21 15l-4 4l-4 -4m4 4v-14" />
|
<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 |
@@ -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 |
@@ -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,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">
|
<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 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" />
|
||||||
<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>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 619 B After Width: | Height: | Size: 559 B |
@@ -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">
|
<div class="w-full">
|
||||||
<%= f.fields_for :value, record do |ff| %>
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
<%= ff.label :first_duration, t('first_reminder_in'), class: 'label truncate' %>
|
<%= 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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<%= f.fields_for :value, record do |ff| %>
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
<%= ff.label :second_duration, t('second_reminder_in'), class: 'label truncate' %>
|
<%= 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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<%= f.fields_for :value, record do |ff| %>
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
<%= ff.label :third_duration, t('third_reminder_in'), class: 'label truncate' %>
|
<%= 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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
if (!window.customElements.get('server-selector')) {
|
||||||
|
customElements.define('server-selector', class extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
const serverSelector = this.querySelector('#server_selector');
|
||||||
|
const globalServerSelector = this.querySelector('#global_server_selector');
|
||||||
|
const euServerAlert = this.querySelector('#eu_server_alert');
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
||||||
|
|
||||||
|
if (!serverSelector.classList.contains('hidden')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usTimezones.test(timezone)) {
|
||||||
|
serverSelector.classList.remove('hidden');
|
||||||
|
} else if (timezone.includes('Europe')) {
|
||||||
|
globalServerSelector.classList.remove('hidden');
|
||||||
|
euServerAlert?.classList?.remove('hidden');
|
||||||
|
} else {
|
||||||
|
globalServerSelector.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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' %>
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
<% link = pagy_anchor(@pagy) %>
|
<% link = pagy_anchor(pagy) %>
|
||||||
<% if @pagy.pages > 1 %>
|
<% if pagy.pages > 1 %>
|
||||||
<div class="flex my-6 justify-center md:justify-between">
|
<div class="flex my-6 justify-center md:justify-between">
|
||||||
<div class="hidden md:block text-sm">
|
<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] %>
|
<%= local_assigns[:left_additional_html] %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1.5">
|
<div class="flex items-center space-x-1.5">
|
||||||
<%= local_assigns[:right_additional_html] %>
|
<%= local_assigns[:right_additional_html] %>
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<% if @pagy.prev %>
|
<% if pagy.prev %>
|
||||||
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
<%== link.call(pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
||||||
<%= t('page_number', number: @pagy.page) %>
|
<%= t('page_number', number: pagy.page) %>
|
||||||
</span>
|
</span>
|
||||||
<% if @pagy.next %>
|
<% if local_assigns[:next_page_path].present? %>
|
||||||
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
<%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %>
|
||||||
|
<% elsif pagy.next %>
|
||||||
|
<%== link.call(pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||