Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -157,7 +157,10 @@ jobs:
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
sudo apt-get install -y libvips
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
||||
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
|
||||
rm -f pdfium-linux.tgz
|
||||
- name: Run
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
FROM ruby:3.4.2-alpine AS fonts
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||
RUN apk --no-cache add fontforge wget && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||
|
||||
@@ -41,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -70,8 +79,10 @@ COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY .version ./public/version
|
||||
|
||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
|
||||
@@ -387,7 +387,7 @@ GEM
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack (3.1.14)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountConfigsController < ApplicationController
|
||||
before_action :load_account_config
|
||||
authorize_resource :account_config
|
||||
before_action :load_account_config, only: :create
|
||||
authorize_resource :account_config, only: :create
|
||||
|
||||
load_and_authorize_resource :account_config, only: :destroy
|
||||
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
@@ -30,6 +32,14 @@ class AccountConfigsController < ApplicationController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
|
||||
@account_config.destroy!
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
|
||||
@@ -21,7 +21,11 @@ module Api
|
||||
|
||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||
|
||||
authorization_check!(blob) if exp.blank?
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
|
||||
authorization_check!(attachment) if exp.blank?
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
@@ -37,9 +41,7 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(blob)
|
||||
attachment = blob.attachments.take
|
||||
|
||||
def authorization_check!(attachment)
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||
|
||||
@@ -6,10 +6,10 @@ module Api
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
||||
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||
end
|
||||
|
||||
render json: {}
|
||||
|
||||
@@ -6,15 +6,15 @@ module Api
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
submitter.opened_at = Time.current
|
||||
submitter.save
|
||||
@submitter.opened_at = Time.current
|
||||
@submitter.save
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ module Api
|
||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||
|
||||
if params[:template_id].present?
|
||||
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||
end
|
||||
|
||||
maybe_filder_by_completed_at(submitters, params)
|
||||
|
||||
@@ -5,7 +5,7 @@ module Api
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:manage, @template)
|
||||
authorize!(:create, @template)
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
|
||||
@@ -100,6 +100,7 @@ module Api
|
||||
permitted_params = [
|
||||
:name,
|
||||
:external_id,
|
||||
:shared_link,
|
||||
{
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
|
||||
@@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base
|
||||
request.session[:impersonated_user_id] = user.uuid
|
||||
end
|
||||
|
||||
def pagy_auto(collection, **keyword_args)
|
||||
if current_ability.can?(:manage, :countless)
|
||||
pagy_countless(collection, **keyword_args)
|
||||
else
|
||||
pagy(collection, **keyword_args)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_locale(&)
|
||||
|
||||
@@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(email_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||
unless Docuseal.multitenant?
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||
end
|
||||
|
||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
|
||||
@@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API
|
||||
|
||||
return head :not_found unless attachment
|
||||
|
||||
@template = attachment.record
|
||||
|
||||
preview_image = attachment.preview_images.joins(:blob)
|
||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||
|
||||
|
||||
@@ -10,16 +10,17 @@ class SendSubmissionEmailController < ApplicationController
|
||||
SEND_DURATION = 30.minutes
|
||||
|
||||
def create
|
||||
@submitter =
|
||||
if params[:template_slug]
|
||||
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
elsif params[:submission_slug]
|
||||
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
else
|
||||
Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
if params[:template_slug]
|
||||
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
elsif params[:submission_slug]
|
||||
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
|
||||
@@ -8,19 +8,28 @@ class StartFormController < ApplicationController
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed]
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
before_action :load_resubmit_submitter, only: :update
|
||||
before_action :load_template
|
||||
before_action :authorize_start!, only: :update
|
||||
|
||||
def show
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
||||
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
||||
@template.submitters.first)['uuid'])
|
||||
if @template.shared_link?
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(account_id: @template.account_id,
|
||||
uuid: (filter_undefined_submitters(@template).first ||
|
||||
@template.submitters.first)['uuid'])
|
||||
else
|
||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
return render :private if current_user && current_ability.can?(:read, @template)
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||
|
||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||
|
||||
if @submitter.completed_at?
|
||||
@@ -42,9 +51,11 @@ class StartFormController < ApplicationController
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
enqueue_submission_create_webhooks(@submitter)
|
||||
|
||||
if @submitter.submission.expire_at?
|
||||
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
||||
'submission_id' => @submitter.submission_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,6 +67,8 @@ class StartFormController < ApplicationController
|
||||
end
|
||||
|
||||
def completed
|
||||
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
|
||||
|
||||
@submitter = Submitter.where(submission: @template.submissions)
|
||||
.where.not(completed_at: nil)
|
||||
.find_by!(email: params[:email])
|
||||
@@ -63,32 +76,55 @@ class StartFormController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def load_resubmit_submitter
|
||||
@resubmit_submitter =
|
||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||
Submitter.find_by(slug: params[:resubmit])
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||
|
||||
return if @resubmit_submitter
|
||||
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
||||
|
||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
redirect_to start_form_path(@template.slug)
|
||||
end
|
||||
|
||||
def enqueue_submission_create_webhooks(submitter)
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||
.order(id: :desc)
|
||||
.where(declined_at: nil)
|
||||
.where(external_id: nil)
|
||||
.where(ip: [nil, request.remote_ip])
|
||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
||||
Submitter
|
||||
.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||
.order(id: :desc)
|
||||
.where(declined_at: nil)
|
||||
.where(external_id: nil)
|
||||
.where(ip: [nil, request.remote_ip])
|
||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
||||
end
|
||||
|
||||
def assign_submission_attributes(submitter, template)
|
||||
resubmit_submitter =
|
||||
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
|
||||
|
||||
submitter.assign_attributes(
|
||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
||||
ip: request.remote_ip,
|
||||
ua: request.user_agent,
|
||||
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||
metadata: resubmit_submitter&.metadata.presence || {}
|
||||
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||
metadata: @resubmit_submitter&.metadata.presence || {}
|
||||
)
|
||||
|
||||
if submitter.values.present?
|
||||
resubmit_submitter.attachments.each do |attachment|
|
||||
@resubmit_submitter.attachments.each do |attachment|
|
||||
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||
end
|
||||
end
|
||||
@@ -96,6 +132,7 @@ class StartFormController < ApplicationController
|
||||
submitter.submission ||= Submission.new(template:,
|
||||
account_id: template.account_id,
|
||||
template_submitters: template.submitters,
|
||||
expire_at: Templates.build_default_expire_at(template),
|
||||
submitters: [submitter],
|
||||
source: :link)
|
||||
|
||||
@@ -109,15 +146,21 @@ class StartFormController < ApplicationController
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
return current_user.slice(:email) if params[:selfsign]
|
||||
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
|
||||
|
||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
end
|
||||
end
|
||||
|
||||
def load_template
|
||||
slug = params[:slug] || params[:start_form_slug]
|
||||
|
||||
@template = Template.find_by!(slug:)
|
||||
@template =
|
||||
if @resubmit_submitter
|
||||
@resubmit_submitter.template
|
||||
else
|
||||
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||
end
|
||||
end
|
||||
|
||||
def multiple_submitters_error_message
|
||||
|
||||
@@ -7,9 +7,7 @@ class SubmissionsArchivedController < ApplicationController
|
||||
@submissions = @submissions.joins(:template)
|
||||
@submissions = @submissions.where.not(archived_at: nil)
|
||||
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||
.preload(:created_by_user, template: :author)
|
||||
|
||||
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
||||
.preload(:template_accesses, :created_by_user, template: :author)
|
||||
|
||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
@@ -20,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,9 +8,7 @@ class SubmissionsDashboardController < ApplicationController
|
||||
|
||||
@submissions = @submissions.where(archived_at: nil)
|
||||
.where(templates: { archived_at: nil })
|
||||
.preload(:created_by_user, template: :author)
|
||||
|
||||
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
||||
.preload(:template_accesses, :created_by_user, template: :author)
|
||||
|
||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
@@ -21,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,20 +8,20 @@ class SubmissionsDownloadController < ApplicationController
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if submitter&.slug == params[:submitter_slug]
|
||||
if @submitter&.slug == params[:submitter_slug]
|
||||
true
|
||||
else
|
||||
submitter = nil
|
||||
@submitter = nil
|
||||
end
|
||||
|
||||
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(submitter)
|
||||
url = build_combined_url(@submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsPreviewController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
|
||||
@@ -7,25 +7,25 @@ class SubmitFormDownloadController < ApplicationController
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
|
||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
return head :unprocessable_entity if submitter.declined_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template.archived_at?
|
||||
return head :unprocessable_entity if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template.archived_at?
|
||||
|
||||
last_completed_submitter = submitter.submission.submitters
|
||||
.where.not(id: submitter.id)
|
||||
.where.not(completed_at: nil)
|
||||
.max_by(&:completed_at)
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
.where.not(completed_at: nil)
|
||||
.max_by(&:completed_at)
|
||||
|
||||
attachments =
|
||||
if last_completed_submitter
|
||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||
else
|
||||
submitter.submission.template.schema_documents.preload(:blob)
|
||||
@submitter.submission.template.schema_documents.preload(:blob)
|
||||
end
|
||||
|
||||
urls = attachments.map do |attachment|
|
||||
|
||||
@@ -9,7 +9,7 @@ class TemplateFoldersController < ApplicationController
|
||||
@templates = Templates.search(@templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||
|
||||
@pagy, @templates = pagy(@templates, limit: 12)
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
@@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController
|
||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
||||
@templates = Templates.search(@templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy(@templates, limit: 12)
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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))
|
||||
|
||||
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
|
||||
@@ -21,9 +21,7 @@ class TemplatesController < ApplicationController
|
||||
submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
||||
|
||||
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ class TemplatesDashboardController < ApplicationController
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
items: FOLDERS_PER_PAGE,
|
||||
limit: FOLDERS_PER_PAGE,
|
||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class TemplatesDashboardController < ApplicationController
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy(@templates, limit:)
|
||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,13 +45,15 @@ class TemplatesDashboardController < ApplicationController
|
||||
rel = templates.active.preload(:author, :template_accesses)
|
||||
|
||||
if params[:q].blank?
|
||||
if Docuseal.multitenant? && !current_account.testing?
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||
else
|
||||
shared_template_ids =
|
||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
||||
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
||||
shared_account_ids = [current_user.account_id]
|
||||
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
||||
|
||||
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
|
||||
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||
else
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -26,12 +26,20 @@ class TemplatesPreferencesController < ApplicationController
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url validate_unique_submitters
|
||||
submitters_order require_phone_2fa
|
||||
default_expire_at_duration
|
||||
default_expire_at
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||
[completed_message: %i[title body],
|
||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
||||
).tap do |attrs|
|
||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||
|
||||
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
|
||||
attrs[:preferences][:default_expire_at] =
|
||||
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
|
||||
end
|
||||
|
||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||
if %w[true false].include?(value)
|
||||
value == 'true'
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesShareLinkController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(:shared_link)
|
||||
end
|
||||
end
|
||||
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
|
||||
@users.active.where.not(role: 'integration')
|
||||
end
|
||||
|
||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
@@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||
import CheckOnClick from './elements/check_on_click'
|
||||
import PasswordInput from './elements/password_input'
|
||||
import SearchInput from './elements/search_input'
|
||||
import ToggleAttribute from './elements/toggle_attribute'
|
||||
@@ -33,6 +34,7 @@ import MaskedInput from './elements/masked_input'
|
||||
import SetDateButton from './elements/set_date_button'
|
||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||
import AppTour from './elements/app_tour'
|
||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -101,6 +103,8 @@ safeRegisterElement('masked-input', MaskedInput)
|
||||
safeRegisterElement('set-date-button', SetDateButton)
|
||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||
safeRegisterElement('app-tour', AppTour)
|
||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||
safeRegisterElement('check-on-click', CheckOnClick)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
@@ -125,6 +129,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
withSendButton: this.dataset.withSendButton !== 'false',
|
||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||
withConditions: this.dataset.withConditions === 'true',
|
||||
withReplaceAndCloneUpload: true,
|
||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.addEventListener('click', () => {
|
||||
if (!this.element.checked) {
|
||||
this.element.checked = true
|
||||
this.element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get element () {
|
||||
return document.getElementById(this.dataset.elementId)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ export default class extends HTMLElement {
|
||||
this.clearChecked()
|
||||
|
||||
this.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const text = this.dataset.text || this.innerText.trim()
|
||||
|
||||
if (navigator.clipboard) {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
||||
|
||||
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3a9 9 0 1 0 9 9" />
|
||||
</svg>`
|
||||
|
||||
export default targetable(class extends HTMLElement {
|
||||
static [targets.static] = [
|
||||
'hiddenOnDrag',
|
||||
'folderCards',
|
||||
'templateCards'
|
||||
]
|
||||
|
||||
static [target.static] = [
|
||||
'form',
|
||||
'fileDropzone',
|
||||
'fileDropzoneLoading'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
document.addEventListener('drop', this.onWindowDragdrop)
|
||||
document.addEventListener('dragover', this.onWindowDropover)
|
||||
|
||||
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||
|
||||
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||
|
||||
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||
|
||||
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
el?.addEventListener('dragover', this.onDragover)
|
||||
el?.addEventListener('dragleave', this.onDragleave)
|
||||
})
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
document.removeEventListener('drop', this.onWindowDragdrop)
|
||||
document.removeEventListener('dragover', this.onWindowDropover)
|
||||
|
||||
window.removeEventListener('dragleave', this.onWindowDragleave)
|
||||
}
|
||||
|
||||
onTemplateDragStart = (e) => {
|
||||
const id = e.target.href.split('/').pop()
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
|
||||
if (id) {
|
||||
e.dataTransfer.setData('template_id', id)
|
||||
|
||||
const dragPreview = e.target.cloneNode(true)
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
const height = e.target.children[0].getBoundingClientRect().height + 50
|
||||
|
||||
dragPreview.children[1].remove()
|
||||
dragPreview.style.width = `${rect.width}px`
|
||||
dragPreview.style.height = `${height}px`
|
||||
dragPreview.style.position = 'absolute'
|
||||
dragPreview.style.top = '-1000px'
|
||||
dragPreview.style.pointerEvents = 'none'
|
||||
dragPreview.style.opacity = '0.9'
|
||||
|
||||
document.body.appendChild(dragPreview)
|
||||
|
||||
e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2)
|
||||
|
||||
setTimeout(() => document.body.removeChild(dragPreview), 0)
|
||||
}
|
||||
}
|
||||
|
||||
onDropFile = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.fileDropzoneLoading.classList.remove('hidden')
|
||||
this.fileDropzoneLoading.previousElementSibling.classList.add('hidden')
|
||||
this.fileDropzoneLoading.classList.add('opacity-50')
|
||||
|
||||
this.uploadFiles(e.dataTransfer.files, '/templates_upload')
|
||||
}
|
||||
|
||||
onDropFolder = (e, el) => {
|
||||
e.preventDefault()
|
||||
|
||||
const templateId = e.dataTransfer.getData('template_id')
|
||||
|
||||
if (e.dataTransfer.files.length || templateId) {
|
||||
const loading = document.createElement('div')
|
||||
const svg = el.querySelector('svg')
|
||||
|
||||
loading.innerHTML = loadingIconHtml
|
||||
loading.children[0].classList.add(...svg.classList)
|
||||
|
||||
el.replaceChild(loading.children[0], svg)
|
||||
el.classList.add('opacity-50')
|
||||
|
||||
if (e.dataTransfer.files.length) {
|
||||
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
|
||||
|
||||
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
||||
} else {
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('name', el.innerText)
|
||||
|
||||
fetch(`/templates/${templateId}/folder`, {
|
||||
method: 'PUT',
|
||||
redirect: 'manual',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
}).finally(() => {
|
||||
window.Turbo.cache.clear()
|
||||
window.Turbo.visit(location.href)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDropTemplate = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.dataTransfer.files.length) {
|
||||
const loading = document.createElement('div')
|
||||
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
|
||||
loading.innerHTML = loadingIconHtml
|
||||
|
||||
e.target.appendChild(loading)
|
||||
e.target.classList.add('opacity-50')
|
||||
|
||||
const id = e.target.href.split('/').pop()
|
||||
|
||||
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
|
||||
}
|
||||
}
|
||||
|
||||
onWindowDragdrop = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!this.isLoading) this.hideDraghover()
|
||||
}
|
||||
|
||||
uploadFiles (files, url) {
|
||||
this.isLoading = true
|
||||
|
||||
this.form.action = url
|
||||
|
||||
this.form.querySelector('[type="file"]').files = files
|
||||
|
||||
this.form.querySelector('[type="submit"]').click()
|
||||
}
|
||||
|
||||
onWindowDropover = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
this.showDraghover()
|
||||
}
|
||||
}
|
||||
|
||||
onDragover (e) {
|
||||
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
||||
this.style.backgroundColor = '#F7F3F0'
|
||||
|
||||
if (this.classList.contains('before:border-base-300')) {
|
||||
this.classList.remove('before:border-base-300')
|
||||
this.classList.add('before:border-base-content/30')
|
||||
} else if (this.classList.contains('border-base-300')) {
|
||||
this.classList.remove('border-base-300')
|
||||
this.classList.add('border-base-content/30')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDragleave () {
|
||||
this.style.backgroundColor = null
|
||||
|
||||
if (this.classList.contains('before:border-base-content/30')) {
|
||||
this.classList.remove('before:border-base-content/30')
|
||||
this.classList.add('before:border-base-300')
|
||||
} else if (this.classList.contains('border-base-content/30')) {
|
||||
this.classList.remove('border-base-content/30')
|
||||
this.classList.add('border-base-300')
|
||||
}
|
||||
}
|
||||
|
||||
onWindowDragleave = (e) => {
|
||||
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||
this.hideDraghover()
|
||||
}
|
||||
}
|
||||
|
||||
showDraghover = () => {
|
||||
if (this.isDrag) return
|
||||
|
||||
this.isDrag = true
|
||||
|
||||
this.fileDropzone?.classList?.remove('hidden')
|
||||
|
||||
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||
|
||||
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
el.classList.remove('bg-base-200', 'before:hidden')
|
||||
})
|
||||
}
|
||||
|
||||
hideDraghover = () => {
|
||||
this.isDrag = false
|
||||
|
||||
this.fileDropzone?.classList?.add('hidden')
|
||||
|
||||
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||
|
||||
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
el.classList.add('bg-base-200', 'before:hidden')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'loading',
|
||||
'icon',
|
||||
'input'
|
||||
'input',
|
||||
'area'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
this.addEventListener('drop', this.onDrop)
|
||||
|
||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||
|
||||
this.addEventListener('drop', this.onDrop)
|
||||
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
||||
this.area?.addEventListener('dragover', this.onDragover)
|
||||
this.area?.addEventListener('dragleave', this.onDragleave)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.removeEventListener('drop', this.onDrop)
|
||||
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
||||
this.area?.removeEventListener('dragover', this.onDragover)
|
||||
this.area?.removeEventListener('dragleave', this.onDragleave)
|
||||
}
|
||||
|
||||
onDragover (e) {
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
this.style.backgroundColor = '#F7F3F0'
|
||||
this.classList.remove('border-base-300', 'hover:bg-base-200/30')
|
||||
this.classList.add('border-base-content/30')
|
||||
}
|
||||
}
|
||||
|
||||
onDragleave () {
|
||||
this.style.backgroundColor = null
|
||||
this.classList.remove('border-base-content/30')
|
||||
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
||||
}
|
||||
|
||||
onDrop (e) {
|
||||
|
||||
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
|
||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
|
||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
|
||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none field-area-active-label"
|
||||
>
|
||||
<template v-if="area.option_uuid && !option.value">
|
||||
{{ optionValue(option) }}
|
||||
@@ -55,10 +55,12 @@
|
||||
>
|
||||
<div
|
||||
v-else-if="field.type === 'signature' && signature"
|
||||
class="flex flex-col justify-between h-full overflow-hidden"
|
||||
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
||||
>
|
||||
<div
|
||||
class="flex-grow flex overflow-hidden"
|
||||
class="flex overflow-hidden"
|
||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
||||
style="min-height: 50%"
|
||||
>
|
||||
<img
|
||||
@@ -67,8 +69,9 @@
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="withSignatureId"
|
||||
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||
v-if="withSignatureId || field.preferences?.reason_field_uuid"
|
||||
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||
>
|
||||
<div class="truncate uppercase">
|
||||
ID: {{ signature.uuid }}
|
||||
@@ -444,6 +447,9 @@ export default {
|
||||
}
|
||||
|
||||
return style
|
||||
},
|
||||
isNarrow () {
|
||||
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div
|
||||
v-show="isFormVisible"
|
||||
id="form_container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
||||
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
>
|
||||
@@ -1133,7 +1133,7 @@ export default {
|
||||
parent.style.overflow = 'hidden'
|
||||
|
||||
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||
scrollbox.parentNode.classList.add('h-screen', 'h-[100dvh]', 'overflow-y-auto')
|
||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
computedPreviousValue () {
|
||||
if (this.isUsePreviousValue) {
|
||||
if (this.isUsePreviousValue && this.field.required === true) {
|
||||
return this.previousValue
|
||||
} else {
|
||||
return null
|
||||
|
||||
@@ -414,7 +414,7 @@ export default {
|
||||
}
|
||||
},
|
||||
computedPreviousValue () {
|
||||
if (this.isUsePreviousValue) {
|
||||
if (this.isUsePreviousValue && this.field.required === true) {
|
||||
return this.previousValue
|
||||
} else {
|
||||
return null
|
||||
|
||||
@@ -151,8 +151,10 @@ export default {
|
||||
resizeTextarea () {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
||||
}
|
||||
},
|
||||
toggleTextArea () {
|
||||
this.isTextArea = true
|
||||
|
||||
@@ -5,7 +5,19 @@
|
||||
class="mx-auto pl-3 h-full"
|
||||
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
||||
@dragover="onDragover"
|
||||
@drop="isDragFile = false"
|
||||
>
|
||||
<HoverDropzone
|
||||
v-if="sortedDocuments.length && withUploadButton && editable"
|
||||
:is-dragging="isDragFile"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-replace-and-clone="withReplaceAndCloneUpload"
|
||||
@add="[updateFromUpload($event), isDragFile = false]"
|
||||
@replace="[onDocumentsReplace($event), isDragFile = false]"
|
||||
@replace-and-clone="onDocumentsReplaceAndTemplateClone($event)"
|
||||
@error="[onUploadFailed($event), isDragFile = false]"
|
||||
/>
|
||||
<DragPlaceholder
|
||||
ref="dragPlaceholder"
|
||||
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
||||
@@ -67,13 +79,52 @@
|
||||
name="buttons"
|
||||
/>
|
||||
<template v-else>
|
||||
<form
|
||||
v-if="withSignYourselfButton && template.submitters.length < 2"
|
||||
target="_blank"
|
||||
data-turbo="false"
|
||||
class="inline"
|
||||
method="post"
|
||||
:action="`/d/${template.slug}`"
|
||||
@submit="maybeShowErrorTemplateAlert"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="_method"
|
||||
value="put"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="authenticityToken"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="selfsign"
|
||||
value="true"
|
||||
autocomplete="off"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||
type="submit"
|
||||
>
|
||||
<IconWritingSign
|
||||
width="22"
|
||||
class="inline"
|
||||
/>
|
||||
<span class="hidden md:inline">
|
||||
{{ t('sign_yourself') }}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<a
|
||||
v-if="withSignYourselfButton"
|
||||
v-else-if="withSignYourselfButton"
|
||||
id="sign_yourself_button"
|
||||
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
||||
:href="`/templates/${template.id}/submissions/new?selfsign=true`"
|
||||
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
||||
data-turbo-frame="modal"
|
||||
@click="maybeShowErrorTemplateAlert"
|
||||
>
|
||||
<IconWritingSign
|
||||
@@ -433,6 +484,7 @@
|
||||
<script>
|
||||
import Upload from './upload'
|
||||
import Dropzone from './dropzone'
|
||||
import HoverDropzone from './hover_dropzone'
|
||||
import DragPlaceholder from './drag_placeholder'
|
||||
import Fields from './fields'
|
||||
import MobileDrawField from './mobile_draw_field'
|
||||
@@ -462,6 +514,7 @@ export default {
|
||||
MobileFields,
|
||||
Logo,
|
||||
Dropzone,
|
||||
HoverDropzone,
|
||||
DocumentPreview,
|
||||
DocumentControls,
|
||||
IconInnerShadowTop,
|
||||
@@ -665,6 +718,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withReplaceAndCloneUpload: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withPhone: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -724,7 +782,8 @@ export default {
|
||||
copiedArea: null,
|
||||
drawFieldType: null,
|
||||
drawOption: null,
|
||||
dragField: null
|
||||
dragField: null,
|
||||
isDragFile: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -836,6 +895,7 @@ export default {
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
window.addEventListener('dragleave', this.onWindowDragLeave)
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (document.location.search?.includes('stripe_connect_success')) {
|
||||
@@ -854,6 +914,7 @@ export default {
|
||||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||
},
|
||||
beforeUpdate () {
|
||||
this.documentRefs = []
|
||||
@@ -868,6 +929,13 @@ export default {
|
||||
|
||||
ref.x = e.clientX - ref.offsetX
|
||||
ref.y = e.clientY - ref.offsetY
|
||||
} else if (e.dataTransfer?.types?.includes('Files')) {
|
||||
this.isDragFile = true
|
||||
}
|
||||
},
|
||||
onWindowDragLeave (event) {
|
||||
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
|
||||
this.isDragFile = false
|
||||
}
|
||||
},
|
||||
reorderFields (item) {
|
||||
@@ -1250,7 +1318,7 @@ export default {
|
||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||
}
|
||||
},
|
||||
onDraw (area) {
|
||||
onDraw ({ area, isTooSmall }) {
|
||||
if (this.drawField) {
|
||||
if (this.drawOption) {
|
||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||
@@ -1345,7 +1413,7 @@ export default {
|
||||
area.y -= area.h / 2
|
||||
}
|
||||
|
||||
if (area.w) {
|
||||
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
|
||||
this.addField(type, area)
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
@@ -1529,6 +1597,9 @@ export default {
|
||||
})
|
||||
}, 'image/png')
|
||||
},
|
||||
onUploadFailed (error) {
|
||||
if (error) alert(error)
|
||||
},
|
||||
updateFromUpload (data) {
|
||||
this.template.schema.push(...data.schema)
|
||||
this.template.documents.push(...data.documents)
|
||||
@@ -1649,6 +1720,29 @@ export default {
|
||||
|
||||
this.save()
|
||||
},
|
||||
onDocumentsReplace (data) {
|
||||
data.schema.forEach((schemaItem, index) => {
|
||||
const existingSchemaItem = this.template.schema[index]
|
||||
|
||||
if (this.template.schema[index]) {
|
||||
this.onDocumentReplace({
|
||||
replaceSchemaItem: existingSchemaItem,
|
||||
schema: [schemaItem],
|
||||
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)]
|
||||
})
|
||||
} else {
|
||||
this.updateFromUpload({
|
||||
schema: [schemaItem],
|
||||
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)],
|
||||
fields: data.fields,
|
||||
submitters: data.submitters
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onDocumentsReplaceAndTemplateClone (template) {
|
||||
window.Turbo.visit(`/templates/${template.id}/edit`)
|
||||
},
|
||||
moveDocument (item, direction) {
|
||||
const currentIndex = this.template.schema.indexOf(item)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||
@remove-area="$emit('remove-area', $event)"
|
||||
@scroll-to="scrollToArea"
|
||||
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,34 +2,42 @@
|
||||
<div
|
||||
class="flex h-60 w-full"
|
||||
@dragover.prevent
|
||||
@dragenter="isDragEntering = true"
|
||||
@dragleave="isDragEntering = false"
|
||||
@drop.prevent="onDropFiles"
|
||||
>
|
||||
<label
|
||||
id="document_dropzone"
|
||||
class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
|
||||
class="w-full relative rounded-md border-2 border-base-content/10 border-dashed"
|
||||
:for="inputId"
|
||||
:class="{ 'opacity-50': isLoading || isProcessing }"
|
||||
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
|
||||
>
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="flex flex-col items-center">
|
||||
<IconInnerShadowTop
|
||||
v-if="isLoading || isProcessing"
|
||||
v-if="isLoading"
|
||||
class="animate-spin"
|
||||
:width="40"
|
||||
:height="40"
|
||||
/>
|
||||
<IconCloudUpload
|
||||
<component
|
||||
:is="icon"
|
||||
v-else
|
||||
class="stroke-[1.5px]"
|
||||
:width="40"
|
||||
:height="40"
|
||||
/>
|
||||
<div
|
||||
v-if="message"
|
||||
class="font-medium text-lg mb-1"
|
||||
class="text-lg mb-1"
|
||||
:class="{ 'mt-1': !withDescription, 'font-medium': withDescription }"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<div
|
||||
v-if="withDescription"
|
||||
class="text-sm"
|
||||
>
|
||||
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,13 +62,16 @@
|
||||
|
||||
<script>
|
||||
import Upload from './upload'
|
||||
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FileDropzone',
|
||||
components: {
|
||||
IconFilePlus,
|
||||
IconCloudUpload,
|
||||
IconInnerShadowTop
|
||||
IconInnerShadowTop,
|
||||
IconFileSymlink,
|
||||
IconFiles
|
||||
},
|
||||
inject: ['baseFetch', 't'],
|
||||
props: {
|
||||
@@ -68,35 +79,70 @@ export default {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
withHoverClass: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'IconCloudUpload'
|
||||
},
|
||||
cloneTemplateOnUpload: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
emits: ['success', 'error', 'loading'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
isProcessing: false
|
||||
isDragEntering: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputId () {
|
||||
return 'el' + Math.random().toString(32).split('.')[1]
|
||||
},
|
||||
uploadUrl () {
|
||||
if (this.cloneTemplateOnUpload) {
|
||||
return `/templates/${this.templateId}/clone_and_replace`
|
||||
} else {
|
||||
return `/templates/${this.templateId}/documents`
|
||||
}
|
||||
},
|
||||
message () {
|
||||
if (this.isLoading) {
|
||||
return this.t('uploading')
|
||||
} else if (this.isProcessing) {
|
||||
return this.t('processing_')
|
||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||
return this.t('add_pdf_documents_or_images')
|
||||
return this.title || this.t('add_pdf_documents_or_images')
|
||||
} else {
|
||||
return this.t('add_documents_or_images')
|
||||
return this.title || this.t('add_documents_or_images')
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isLoading (value) {
|
||||
this.$emit('loading', value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onDropFiles (e) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
v-for="(icon, type) in fieldIconsSorted"
|
||||
:key="type"
|
||||
>
|
||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
|
||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
:key="type"
|
||||
>
|
||||
<button
|
||||
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
|
||||
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
|
||||
:id="`${type}_type_field_button`"
|
||||
draggable="true"
|
||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
||||
@@ -168,7 +168,7 @@
|
||||
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.com/contact"
|
||||
href="https://www.docuseal.com/qualified-electronic-signature"
|
||||
target="_blank"
|
||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
||||
:style="{ backgroundColor }"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isDragging || isLoading"
|
||||
class="modal modal-open"
|
||||
>
|
||||
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
||||
<Dropzone
|
||||
class="flex-1 h-full"
|
||||
icon="IconFilePlus"
|
||||
:template-id="templateId"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-hover-class="false"
|
||||
:with-description="false"
|
||||
:title="t('add_a_new_document')"
|
||||
type="add_files"
|
||||
@loading="isLoading = $event"
|
||||
@success="$emit('add', $event)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<div class="flex-1 flex gap-2 w-full">
|
||||
<Dropzone
|
||||
class="flex-1 h-full"
|
||||
icon="IconFileSymlink"
|
||||
:template-id="templateId"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-hover-class="false"
|
||||
:with-description="false"
|
||||
:title="t('replace_existing_document')"
|
||||
@loading="isLoading = $event"
|
||||
@success="$emit('replace', $event)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<Dropzone
|
||||
v-if="withReplaceAndClone"
|
||||
class="flex-1 h-full"
|
||||
icon="IconFiles"
|
||||
:template-id="templateId"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-hover-class="false"
|
||||
:with-description="false"
|
||||
:clone-template-on-upload="true"
|
||||
:title="t('clone_and_replace_documents')"
|
||||
@loading="isLoading = $event"
|
||||
@success="$emit('replace-and-clone', $event)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dropzone from './dropzone'
|
||||
|
||||
export default {
|
||||
name: 'HoverDropzone',
|
||||
components: {
|
||||
Dropzone
|
||||
},
|
||||
inject: ['t'],
|
||||
props: {
|
||||
isDragging: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
templateId: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
withReplaceAndClone: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
}
|
||||
},
|
||||
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -63,6 +63,9 @@ const en = {
|
||||
processing_: 'Processing...',
|
||||
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||
add_documents_or_images: 'Add documents or images',
|
||||
add_a_new_document: 'Add a new document',
|
||||
replace_existing_document: 'Replace existing document',
|
||||
clone_and_replace_documents: 'Clone and replace documents',
|
||||
required: 'Required',
|
||||
default_value: 'Default value',
|
||||
format: 'Format',
|
||||
@@ -225,6 +228,9 @@ const es = {
|
||||
processing_: 'Procesando...',
|
||||
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
||||
add_documents_or_images: 'Agregar documentos o imágenes',
|
||||
add_a_new_document: 'Agregar un nuevo documento',
|
||||
replace_existing_document: 'Reemplazar documento existente',
|
||||
clone_and_replace_documents: 'Clonar y reemplazar documentos',
|
||||
required: 'Requerido',
|
||||
default_value: 'Valor predeterminado',
|
||||
format: 'Formato',
|
||||
@@ -393,6 +399,9 @@ const it = {
|
||||
processing_: 'Elaborazione...',
|
||||
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
||||
add_documents_or_images: 'Aggiungi documenti o immagini',
|
||||
add_a_new_document: 'Aggiungi un nuovo documento',
|
||||
replace_existing_document: 'Sostituisci documento esistente',
|
||||
clone_and_replace_documents: 'Clona e sostituisci documenti',
|
||||
required: 'Obbligatorio',
|
||||
default_value: 'Valore predefinito',
|
||||
format: 'Formato',
|
||||
@@ -555,6 +564,9 @@ const pt = {
|
||||
processing_: 'Processando...',
|
||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||
add_a_new_document: 'Ajouter un nouveau document',
|
||||
replace_existing_document: 'Substituir documento existente',
|
||||
clone_and_replace_documents: 'Clonar e substituir documentos',
|
||||
required: 'Obrigatório',
|
||||
default_value: 'Valor padrão',
|
||||
format: 'Formato',
|
||||
@@ -719,6 +731,9 @@ const fr = {
|
||||
processing_: 'Traitement en cours...',
|
||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
||||
add_documents_or_images: 'Ajoutez des documents ou des images',
|
||||
add_a_new_document: 'Adicionar um novo documento',
|
||||
replace_existing_document: 'Remplacer le document existant',
|
||||
clone_and_replace_documents: 'Cloner et remplacer les documents',
|
||||
required: 'Requis',
|
||||
default_value: 'Valeur par défaut',
|
||||
format: 'Format',
|
||||
@@ -884,6 +899,9 @@ const de = {
|
||||
processing_: 'Verarbeitung...',
|
||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
||||
add_a_new_document: 'Neues Dokument hinzufügen',
|
||||
replace_existing_document: 'Vorhandenes Dokument ersetzen',
|
||||
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
|
||||
required: 'Erforderlich',
|
||||
default_value: 'Standardwert',
|
||||
format: 'Format',
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
v-for="(icon, type) in fieldIconsSorted"
|
||||
:key="type"
|
||||
>
|
||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
|
||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
|
||||
@@ -290,7 +290,12 @@ export default {
|
||||
area.cell_w = this.newArea.cell_w
|
||||
}
|
||||
|
||||
this.$emit('draw', area)
|
||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
|
||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
|
||||
|
||||
const isTooSmall = dx < 8 && dy < 8
|
||||
|
||||
this.$emit('draw', { area, isTooSmall })
|
||||
}
|
||||
|
||||
this.showMask = false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label
|
||||
:for="inputId"
|
||||
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
|
||||
:class="{ 'opacity-100': isLoading || isProcessing }"
|
||||
:class="{ 'opacity-100': isLoading }"
|
||||
>
|
||||
{{ message }}
|
||||
<form
|
||||
@@ -41,19 +41,19 @@ export default {
|
||||
emits: ['success'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
isProcessing: false
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputId () {
|
||||
return 'el' + Math.random().toString(32).split('.')[1]
|
||||
},
|
||||
uploadUrl () {
|
||||
return `/templates/${this.templateId}/documents`
|
||||
},
|
||||
message () {
|
||||
if (this.isLoading) {
|
||||
return this.t('uploading_')
|
||||
} else if (this.isProcessing) {
|
||||
return this.t('processing_')
|
||||
} else {
|
||||
return this.t('replace')
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
id="add_document_button"
|
||||
:for="inputId"
|
||||
class="btn btn-outline w-full add-document-button"
|
||||
:class="{ 'btn-disabled': isLoading || isProcessing }"
|
||||
:class="{ 'btn-disabled': isLoading }"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
v-if="isLoading || isProcessing"
|
||||
v-if="isLoading"
|
||||
width="20"
|
||||
class="animate-spin"
|
||||
/>
|
||||
@@ -18,9 +18,6 @@
|
||||
<span v-if="isLoading">
|
||||
{{ t('uploading_') }}
|
||||
</span>
|
||||
<span v-else-if="isProcessing">
|
||||
{{ t('processing_') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('add_document') }}
|
||||
</span>
|
||||
@@ -63,30 +60,34 @@ export default {
|
||||
default: 'image/*, application/pdf'
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
emits: ['success', 'error'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
isProcessing: false
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputId () {
|
||||
return 'el' + Math.random().toString(32).split('.')[1]
|
||||
},
|
||||
uploadUrl () {
|
||||
return `/templates/${this.templateId}/documents`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async upload () {
|
||||
this.isLoading = true
|
||||
|
||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||
this.baseFetch(this.uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
body: new FormData(this.$refs.form)
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.json().then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
this.isLoading = false
|
||||
})
|
||||
} else if (resp.status === 422) {
|
||||
resp.json().then((data) => {
|
||||
@@ -95,21 +96,33 @@ export default {
|
||||
|
||||
formData.append('password', prompt(this.t('enter_pdf_password')))
|
||||
|
||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||
this.baseFetch(this.uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
this.$emit('success', await resp.json())
|
||||
this.$refs.input.value = ''
|
||||
this.isLoading = false
|
||||
} else {
|
||||
alert(this.t('wrong_password'))
|
||||
|
||||
this.$emit('error', await resp.json().error)
|
||||
this.isLoading = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.$emit('error', data.error)
|
||||
this.isLoading = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resp.json().then((data) => {
|
||||
this.$emit('error', data.error)
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
}).catch(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ApplicationMailer < ActionMailer::Base
|
||||
end
|
||||
|
||||
def set_message_metadata
|
||||
message.instance_variable_set(:@message_metadata, @message_metadata)
|
||||
message.instance_variable_set(:@message_metadata, @message_metadata || {})
|
||||
end
|
||||
|
||||
def set_message_uuid
|
||||
@@ -28,10 +28,14 @@ class ApplicationMailer < ActionMailer::Base
|
||||
end
|
||||
|
||||
def assign_message_metadata(tag, record)
|
||||
@message_metadata = {
|
||||
@message_metadata = (@message_metadata || {}).merge(
|
||||
'tag' => tag,
|
||||
'record_id' => record.id,
|
||||
'record_type' => record.class.name
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def put_metadata(attrs)
|
||||
@message_metadata = (@message_metadata || {}).merge(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -233,9 +233,17 @@ class SubmitterMailer < ApplicationMailer
|
||||
def from_address_for_submitter(submitter)
|
||||
if submitter.submission.source.in?(%w[api embed]) &&
|
||||
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
|
||||
user = submitter.account.users.find_by(email: from_email)
|
||||
|
||||
put_metadata('from_user_id' => user.id)
|
||||
|
||||
from_email
|
||||
else
|
||||
(submitter.submission.created_by_user || submitter.submission.template.author).friendly_name
|
||||
user = submitter.submission.created_by_user || submitter.submission.template.author
|
||||
|
||||
put_metadata('from_user_id' => user.id)
|
||||
|
||||
user.friendly_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
|
||||
has_many :account_linked_accounts, dependent: :destroy
|
||||
has_many :email_events, dependent: :destroy
|
||||
has_many :webhook_urls, dependent: :destroy
|
||||
has_many :account_accesses, dependent: :destroy
|
||||
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||
class_name: 'AccountLinkedAccount',
|
||||
inverse_of: :account
|
||||
|
||||
@@ -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
|
||||
@@ -18,10 +18,10 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_email_events_on_account_id (account_id)
|
||||
# index_email_events_on_email (email)
|
||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||
# index_email_events_on_message_id (message_id)
|
||||
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
||||
# index_email_events_on_email (email)
|
||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||
# index_email_events_on_message_id (message_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
|
||||
@@ -65,13 +65,19 @@ class Submission < ApplicationRecord
|
||||
|
||||
scope :active, -> { where(archived_at: nil) }
|
||||
scope :archived, -> { where.not(archived_at: nil) }
|
||||
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
|
||||
scope :pending, lambda {
|
||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
scope :completed, lambda {
|
||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) }
|
||||
scope :expired, -> { where(expire_at: ..Time.current) }
|
||||
scope :declined, lambda {
|
||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
scope :expired, -> { pending.where(expire_at: ..Time.current) }
|
||||
|
||||
enum :source, {
|
||||
invite: 'invite',
|
||||
|
||||
@@ -38,6 +38,8 @@ class SubmissionEvent < ApplicationRecord
|
||||
|
||||
enum :event_type, {
|
||||
send_email: 'send_email',
|
||||
bounce_email: 'bounce_email',
|
||||
complaint_email: 'complaint_email',
|
||||
send_reminder_email: 'send_reminder_email',
|
||||
send_sms: 'send_sms',
|
||||
send_2fa_sms: 'send_2fa_sms',
|
||||
|
||||
@@ -54,7 +54,7 @@ class Submitter < ApplicationRecord
|
||||
has_many_attached :documents
|
||||
has_many_attached :attachments
|
||||
has_many_attached :preview_documents
|
||||
has_many :template_accesses, through: :template
|
||||
has_many :template_accesses, through: :submission
|
||||
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
|
||||
|
||||
has_many :document_generation_events, dependent: :destroy
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# name :string not null
|
||||
# preferences :text not null
|
||||
# schema :text not null
|
||||
# shared_link :boolean default(FALSE), not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters :text not null
|
||||
|
||||
@@ -73,6 +73,8 @@ class User < ApplicationRecord
|
||||
scope :archived, -> { where.not(archived_at: nil) }
|
||||
scope :admins, -> { where(role: ADMIN_ROLE) }
|
||||
|
||||
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
|
||||
|
||||
def access_token
|
||||
super || build_access_token.tap(&:save!)
|
||||
end
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
<div class="text-center">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
|
||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||
Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
Europe
|
||||
</a>
|
||||
<% eu_server = request.host == 'docuseal.eu' %>
|
||||
<server-selector>
|
||||
<div id="global_server_selector" class="text-center hidden">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-40 <%= 'bg-base-300' unless eu_server %>">
|
||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||
Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="relative btn bg-base-200 join-item w-40 <%= 'bg-base-300' if eu_server %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
Europe
|
||||
<% unless eu_server %>
|
||||
<span id="eu_server_alert" class="absolute flex space-x-0.5 hidden" style="top: -1.5rem;">
|
||||
<span class="text-xs font-normal leading-none text-base-content normal-case">
|
||||
<%= t('eu_data_residency') %>
|
||||
</span>
|
||||
<%= svg_icon 'corner_right_down', class: 'w-4 h-5 stroke-1 shrink-0 pt-1' %>
|
||||
</span>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="us_server_selector" class="flex justify-center hidden">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
||||
<% if eu_server %>
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
<span>EU Cloud</span>
|
||||
<% else %>
|
||||
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||
<span>US Cloud</span>
|
||||
<% end %>
|
||||
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
|
||||
<li>
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
|
||||
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||
US Cloud
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
EU Cloud
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</server-selector>
|
||||
<%= render 'scripts/server_selector' %>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<%= t('preferences') %>
|
||||
</h2>
|
||||
</div>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %>
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<div class="flex items-center justify-between py-2.5">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 9l4 -4l4 4m-4 -4v14" />
|
||||
<path d="M21 15l-4 4l-4 -4m4 4v-14" />
|
||||
|
||||
|
Before Width: | Height: | Size: 357 B After Width: | Height: | Size: 362 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M20.986 12.502a9 9 0 1 0 -5.973 7.98" /><path d="M12 7v5l3 3" /><path d="M19 16v3" /><path d="M19 22v.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1,3 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M6 6h6a3 3 0 0 1 3 3v10l-4 -4m8 0l-4 4"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -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">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 8v-2a2 2 0 0 1 2 -2h2"></path>
|
||||
<path d="M4 16v2a2 2 0 0 0 2 2h2"></path>
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v2"></path>
|
||||
<path d="M16 20h2a2 2 0 0 0 2 -2v-2"></path>
|
||||
<path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M10 11v-2a2 2 0 1 1 4 0v2"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 11v-2a2 2 0 1 1 4 0v2" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 619 B After Width: | Height: | Size: 559 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 640 480">
|
||||
<path fill="#bd3d44" d="M0 0h640v480H0" />
|
||||
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640" />
|
||||
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
|
||||
<marker id="um-a" markerHeight="30" markerWidth="30">
|
||||
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
|
||||
</marker>
|
||||
<path fill="none" marker-mid="url(#um-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 671 B |
@@ -7,19 +7,19 @@
|
||||
<div class="w-full">
|
||||
<%= f.fields_for :value, record do |ff| %>
|
||||
<%= ff.label :first_duration, t('first_reminder_in'), class: 'label truncate' %>
|
||||
<%= ff.select :first_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
|
||||
<%= ff.select :first_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<%= f.fields_for :value, record do |ff| %>
|
||||
<%= ff.label :second_duration, t('second_reminder_in'), class: 'label truncate' %>
|
||||
<%= ff.select :second_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
|
||||
<%= ff.select :second_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<%= f.fields_for :value, record do |ff| %>
|
||||
<%= ff.label :third_duration, t('third_reminder_in'), class: 'label truncate' %>
|
||||
<%= ff.select :third_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
|
||||
<%= ff.select :third_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
if (!window.customElements.get('server-selector')) {
|
||||
customElements.define('server-selector', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const usServerSelector = this.querySelector('#us_server_selector');
|
||||
const globalServerSelector = this.querySelector('#global_server_selector');
|
||||
const euServerAlert = this.querySelector('#eu_server_alert');
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
||||
|
||||
if (usTimezones.test(timezone)) {
|
||||
usServerSelector.classList.remove('hidden');
|
||||
} else if (timezone.includes('Europe')) {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
euServerAlert?.classList?.remove('hidden');
|
||||
} else {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1 +1 @@
|
||||
<% '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' %>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<% if @pagy.pages > 1 %>
|
||||
<div class="flex my-6 justify-center md:justify-between">
|
||||
<div class="hidden md:block text-sm">
|
||||
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %>
|
||||
<% if @pagy.count.nil? %>
|
||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to)) %>
|
||||
<% else %>
|
||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to), count: local_assigns.fetch(:count, @pagy.count)) %>
|
||||
<% end %>
|
||||
<%= local_assigns[:left_additional_html] %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %>
|
||||
<div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2">
|
||||
<%= auto_link(MarkdownToHtml.call(configs.value)) %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -23,12 +23,12 @@
|
||||
</div>
|
||||
<% if Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account) %>
|
||||
<toggle-submit class="block">
|
||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, class: 'base-button w-full' %>
|
||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
<% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
||||
<% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
|
||||
<toggle-submit class="block">
|
||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
|
||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, class: 'white-button w-full' %>
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
||||
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="text-center w-full space-y-6">
|
||||
<%= render 'banner' %>
|
||||
<p class="text-xl font-semibold text-center">
|
||||
<%= t('share_link_is_currently_disabled') %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
||||
<% if @template.archived_at? %>
|
||||
<p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||
@@ -42,3 +42,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||
<%= render 'start_form/policy', account: @template.account %>
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
|
||||
<% elsif event.event_type.include?('send_') %>
|
||||
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
|
||||
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
|
||||
<%= t("submission_event_names.#{event.event_type}_html", submitter_name:) %>
|
||||
<% else %>
|
||||
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
|
||||
<% end %>
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
<% font_type = field.dig('preferences', 'font_type') %>
|
||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||
<% if field['type'] == 'signature' %>
|
||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
|
||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
||||
</div>
|
||||
<% if local_assigns[:with_signature_id] && attachment = attachments_index[value] %>
|
||||
<div class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]">
|
||||
<% if (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) && attachment = attachments_index[value] %>
|
||||
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
|
||||
<div class="truncate uppercase">
|
||||
ID: <%= attachment.uuid %>
|
||||
</div>
|
||||
|
||||
@@ -172,12 +172,26 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||
<% if @submission.expire_at? && submitter && !submitter.completed_at? %>
|
||||
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
|
||||
<% else %>
|
||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
<span>
|
||||
<% if submitter&.declined_at? %>
|
||||
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
||||
<% elsif submitter %>
|
||||
<%= submitter.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : t('not_completed_yet') %>
|
||||
<% if submitter.completed_at? %>
|
||||
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
|
||||
<% elsif @submission.expire_at? %>
|
||||
<% if @submission.expired? %>
|
||||
<%= t(:expired) %>
|
||||
<% else %>
|
||||
<%= t('expire_on_time', time: l(@submission.expire_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t('not_completed_yet') %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= t('not_invited_yet') %>
|
||||
<% end %>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<%= render 'submissions_filters/filter_button', filter_params: %>
|
||||
</div>
|
||||
</div>
|
||||
<% if @pagy.count > 0 %>
|
||||
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
||||
<div class="space-y-4">
|
||||
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
||||
<% is_show_tabs = @pagy.count >= 5 || params[:status].present? || filter_params.present? %>
|
||||
<% is_show_tabs = (@pagy.count.nil? || @pagy.count >= 5) || params[:status].present? || filter_params.present? %>
|
||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||
<div class="flex justify-between items-center w-full mb-4">
|
||||
<div class="flex items-center flex-grow min-w-0">
|
||||
@@ -61,12 +61,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @pagy.count > 0 %>
|
||||
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
||||
<div class="space-y-4">
|
||||
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count < 5 %>
|
||||
<% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count.present? && @pagy.count < 5 %>
|
||||
<%= render 'templates/dropzone' %>
|
||||
<% end %>
|
||||
<% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %>
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<%= f.hidden_field :submission_slug, value: @submission.slug %>
|
||||
<%= f.label :email, t('email'), class: 'label' %>
|
||||
<%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: t('send_copy_to_email') %>
|
||||
<% if params[:status] == 'error' %>
|
||||
<span class="label-text-alt text-red-400 mt-1">
|
||||
<%= t('please_enter_your_email_address_associated_with_the_completed_submission') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<toggle-submit dir="auto" class="form-control">
|
||||
<%= f.button button_title(title: t('send_copy_to_email'), disabled_with: t('starting')), class: 'base-button' %>
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template) %>
|
||||
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
|
||||
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
||||
<div class="divider uppercase"><%= t('or') %></div>
|
||||
<toggle-submit class="block">
|
||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
|
||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% is_long = folder.name.size > 32 %>
|
||||
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200">
|
||||
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards">
|
||||
<% if !is_long %>
|
||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||
<% end %>
|
||||
|
||||
@@ -4,47 +4,56 @@
|
||||
<span><%= t('home') %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between items-center w-full mb-4">
|
||||
<h1 class="text-4xl font-bold flex flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
|
||||
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
|
||||
<span class="peer truncate">
|
||||
<%= @template_folder.name %>
|
||||
</span>
|
||||
<% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %>
|
||||
<span class="pl-1 opacity-0 hover:opacity-100 peer-hover:opacity-100">
|
||||
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
|
||||
<%= svg_icon('pencil', class: 'w-7 h-7') %>
|
||||
</a>
|
||||
<dashboard-dropzone>
|
||||
<div class="relative flex justify-between items-center w-full mb-4">
|
||||
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
|
||||
<input name="form_id" value="<%= form_id %>">
|
||||
<input name="folder_name" value="<%= @template_folder.name %>">
|
||||
<button type="submit"></button>
|
||||
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
|
||||
<% end %>
|
||||
<%= render 'templates/dashboard_dropzone', style: 'height: 137px' %>
|
||||
<h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
|
||||
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
|
||||
<span class="peer truncate">
|
||||
<%= @template_folder.name %>
|
||||
</span>
|
||||
<% end %>
|
||||
</h1>
|
||||
<div class="flex space-x-2">
|
||||
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||
<%= render 'shared/search_input' %>
|
||||
<% end %>
|
||||
<% if can?(:create, ::Template) %>
|
||||
<%= render 'templates/upload_button', folder_name: @template_folder.name %>
|
||||
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %>
|
||||
<span class="opacity-0 hover:opacity-100 peer-hover:opacity-100">
|
||||
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
|
||||
<%= svg_icon('pencil', class: 'w-7 h-7') %>
|
||||
</a>
|
||||
</span>
|
||||
<% end %>
|
||||
</h1>
|
||||
<div class="flex space-x-2">
|
||||
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||
<%= render 'shared/search_input' %>
|
||||
<% end %>
|
||||
<% if can?(:create, ::Template) %>
|
||||
<%= render 'templates/upload_button', folder_name: @template_folder.name %>
|
||||
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% if @pagy.count > 0 %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
</div>
|
||||
<% templates_order_select_html = capture do %>
|
||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
|
||||
<% elsif params[:q].present? %>
|
||||
<div class="text-center">
|
||||
<div class="mt-16 text-3xl font-semibold">
|
||||
<%= t('templates_not_found') %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
</div>
|
||||
<% templates_order_select_html = capture do %>
|
||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
|
||||
<% elsif params[:q].present? %>
|
||||
<div class="text-center">
|
||||
<div class="mt-16 text-3xl font-semibold">
|
||||
<%= t('templates_not_found') %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</dashboard-dropzone>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="absolute bottom-0 w-full cursor-pointer rounded-xl bg-base-100 border-2 border-base-300 border-dashed hidden z-50" data-target="dashboard-dropzone.fileDropzone" style="<%= local_assigns[:style] %>">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex justify-center p-2 items-center pointer-events-none">
|
||||
<div class="flex flex-col items-center text-center" data-target="dashboard-dropzone.toggleLoading">
|
||||
<span class="flex flex-col items-center">
|
||||
<span>
|
||||
<%= svg_icon('cloud_upload', class: 'w-9 h-9') %>
|
||||
</span>
|
||||
<div class="font-medium mb-1">
|
||||
<%= t('upload_new_document') %>
|
||||
</div>
|
||||
</span>
|
||||
<span class="flex flex-col items-center hidden" data-target="dashboard-dropzone.fileDropzoneLoading">
|
||||
<%= svg_icon('loader', class: 'w-9 h-9 animate-spin') %>
|
||||
<div class="font-medium mb-1">
|
||||
<%= t('uploading') %>...
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,8 +2,8 @@
|
||||
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||
<button type="submit" class="hidden"></button>
|
||||
<file-dropzone data-submit-on-upload="true" class="w-full">
|
||||
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2">
|
||||
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border-2 border-base-300 border-dashed" data-target="file-dropzone.area">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2 pointer-events-none">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
|
||||
<span>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<% submitter = submitters.first %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex flex-col md:flex-row md:items-center gap-3">
|
||||
<% if submission.expired? %>
|
||||
<% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
|
||||
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||
<%= t('expired') %>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="h-36 relative group">
|
||||
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 peer">
|
||||
<div class="pb-4 text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl" data-targets="dashboard-dropzone.templateCards">
|
||||
<div class="text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||
<% if template.template_accesses.present? %>
|
||||
<%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %>
|
||||
<% end %>
|
||||
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
|
||||
</div>
|
||||
<div class="pb-6 pt-1 space-y-1">
|
||||
<div class="pb-6 pt-1 space-y-1" data-targets="dashboard-dropzone.hiddenOnDrag">
|
||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||
<%= svg_icon('user', class: 'w-4 h-4') %>
|
||||
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
|
||||
@@ -28,7 +28,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px">
|
||||
<div class="absolute top-0 bottom-0 w-0 items-center hidden md:group-hover:flex" style="right: 37px" data-targets="dashboard-dropzone.hiddenOnDrag">
|
||||
<div class="space-y-1">
|
||||
<% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %>
|
||||
<span class="tooltip tooltip-left" data-tip="<%= t('move') %>">
|
||||
|
||||
@@ -49,7 +49,12 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), id: 'share_link_clipboard', class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: t('link'), copied_title: t('copied'), copy_title_md: t('link'), copied_title_md: t('copied') %>
|
||||
<%= link_to template_share_link_path(template), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', data: { turbo_frame: :modal } do %>
|
||||
<span class="flex items-center justify-center space-x-2">
|
||||
<%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6 text-white') %>
|
||||
<span><%= t('link') %></span>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
</span>
|
||||
</button>
|
||||
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.multitenant? %>" multiple>
|
||||
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.advanced_formats? %>" multiple>
|
||||
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
|
||||
<% end %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%= render 'title', template: @template %>
|
||||
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
||||
<% is_show_tabs = @pagy.pages > 1 || params[:q].present? || params[:status].present? || filter_params.present? %>
|
||||
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? || filter_params.present? %>
|
||||
<% if @pagy.count.nil? || !@pagy.count.zero? || params[:q].present? || params[:status].present? || filter_params.present? %>
|
||||
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
|
||||
<div class="flex justify-between items-center md:items-end">
|
||||
<div>
|
||||
@@ -35,33 +35,41 @@
|
||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('all') %></span>
|
||||
</div>
|
||||
<div class="badge badge-neutral badge-outline font-medium">
|
||||
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %>
|
||||
</div>
|
||||
<% unless can?(:manage, :countless) %>
|
||||
<div class="badge badge-neutral badge-outline font-medium">
|
||||
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %>
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('pending') %></span>
|
||||
</div>
|
||||
<div class="badge badge-neutral badge-outline font-medium">
|
||||
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %>
|
||||
</div>
|
||||
<% unless can?(:manage, :countless) %>
|
||||
<div class="badge badge-neutral badge-outline font-medium">
|
||||
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %>
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('completed') %></span>
|
||||
</div>
|
||||
<div class="badge badge-neutral badge-outline font-medium">
|
||||
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %>
|
||||
</div>
|
||||
<% unless can?(:manage, :countless) %>
|
||||
<div class="badge badge-neutral badge-outline font-medium">
|
||||
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %>
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||
<%= render 'submissions_filters/applied_filters', filter_params: %>
|
||||
<%= render 'submissions_filters/filter_button', filter_params: %>
|
||||
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
|
||||
<% unless @template.archived_at? %>
|
||||
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -87,7 +95,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @template.submitters.size == 1 %>
|
||||
<%= link_to start_form_url(slug: @template.slug), id: 'sign_yourself_button', class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
|
||||
<%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
|
||||
<%= svg_icon('writing', class: 'w-6 h-6') %>
|
||||
<span class="mr-1"><%= t('sign_it_yourself') %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @pagy.count > 0 %>
|
||||
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @pagy.count > 0 %>
|
||||
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
||||
<div class="space-y-4">
|
||||
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
|
||||
</div>
|
||||
|
||||
@@ -1,82 +1,92 @@
|
||||
<% has_archived = current_account.templates.where.not(archived_at: nil).exists? %>
|
||||
<% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
|
||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||
<div class="flex justify-between items-center w-full mb-4">
|
||||
<div class="flex items-center flex-grow min-w-0">
|
||||
<% if has_archived || @pagy.count > 0 || @template_folders.present? %>
|
||||
<div class="mr-2">
|
||||
<%= render 'dashboard/toggle_view', selected: 'templates' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<h1 class="text-2xl truncate md:text-3xl sm:text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>">
|
||||
<%= t('document_templates_html') %>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
|
||||
<%= render 'shared/search_input' %>
|
||||
<% end %>
|
||||
<% if can?(:create, ::Template) %>
|
||||
<span class="hidden sm:block">
|
||||
<%= render 'templates/upload_button' %>
|
||||
</span>
|
||||
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% view_archived_html = capture do %>
|
||||
<% if has_archived %>
|
||||
<div>
|
||||
<a href="<%= templates_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
|
||||
</div>
|
||||
<dashboard-dropzone>
|
||||
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
|
||||
<input name="form_id" value="<%= form_id %>">
|
||||
<button type="submit"></button>
|
||||
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% templates_order_select_html = capture do %>
|
||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @template_folders.present? %>
|
||||
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
|
||||
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @templates.present? %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
<% if show_dropzone && current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
|
||||
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
|
||||
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
||||
<div class="hidden md:block">
|
||||
<app-tour id="app_tour" data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="dashboard" data-next-page-path="<%= @templates.first && can?(:edit, @templates.first) ? edit_template_path(@templates.first, params.permit(:tour)) : settings_account_path %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
|
||||
<% if user_config.new_record? && !params.key?(:tour) %>
|
||||
<div class="h-36 rounded-2xl pt-3 px-7 w-full border border-dashed border-base-300">
|
||||
<div class="text-xl text-center font-semibold text-base-content">
|
||||
<%= t('welcome_to_docuseal') %>
|
||||
</div>
|
||||
<div class="my-2 text-center text-xs text-base-content/70">
|
||||
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3 w-full">
|
||||
<%= button_to button_title(title: t('skip'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: false } }, class: 'btn btn-sm btn-outline w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.parentNode.remove()' } %>
|
||||
<%= button_to t('start_tour'), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'btn btn-sm btn-warning w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.start()' } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex justify-between items-center w-full mb-4 relative">
|
||||
<% unless show_dropzone %>
|
||||
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
|
||||
<% end %>
|
||||
<div class="flex items-center flex-grow min-w-0">
|
||||
<% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
|
||||
<div class="mr-2">
|
||||
<%= render 'dashboard/toggle_view', selected: 'templates' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<h1 class="text-2xl truncate md:text-3xl sm:text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>">
|
||||
<%= t('document_templates_html') %>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
|
||||
<%= render 'shared/search_input' %>
|
||||
<% end %>
|
||||
<% if can?(:create, ::Template) %>
|
||||
<span class="hidden sm:block">
|
||||
<%= render 'templates/upload_button' %>
|
||||
</span>
|
||||
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% view_archived_html = capture do %>
|
||||
<% if has_archived %>
|
||||
<div>
|
||||
<a href="<%= templates_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% templates_order_select_html = capture do %>
|
||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @template_folders.present? %>
|
||||
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
|
||||
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @templates.present? %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
<% if show_dropzone && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') && current_user == true_user %>
|
||||
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
|
||||
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
||||
<div class="hidden md:block">
|
||||
<app-tour id="app_tour" data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="dashboard" data-next-page-path="<%= @templates.first && can?(:edit, @templates.first) ? edit_template_path(@templates.first, params.permit(:tour)) : settings_account_path %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
|
||||
<% if user_config.new_record? && !params.key?(:tour) %>
|
||||
<div class="h-36 rounded-2xl pt-3 px-7 w-full border border-dashed border-base-300">
|
||||
<div class="text-xl text-center font-semibold text-base-content">
|
||||
<%= t('welcome_to_docuseal') %>
|
||||
</div>
|
||||
<div class="my-2 text-center text-xs text-base-content/70">
|
||||
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3 w-full">
|
||||
<%= button_to button_title(title: t('skip'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: false } }, class: 'btn btn-sm btn-outline w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.parentNode.remove()' } %>
|
||||
<%= button_to t('start_tour'), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'btn btn-sm btn-warning w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.start()' } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</dashboard-dropzone>
|
||||
<% if show_dropzone %>
|
||||
<%= render 'templates/dropzone' %>
|
||||
<% end %>
|
||||
<% if @templates.present? || params[:q].blank? %>
|
||||
<% if @templates.present? || @template_folders.present? || params[:q].blank? %>
|
||||
<% if @pagy.pages > 1 %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %>
|
||||
<% else %>
|
||||
<div class="mt-2">
|
||||
<%= view_archived_html %>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= tag.input type: 'email', multiple: true, name: 'template[preferences][bcc_completed]', autocomplete: 'off', class: 'base-input', value: ff.object.bcc_completed %>
|
||||
<%= tag.input type: 'email', multiple: true, name: 'template[preferences][bcc_completed]', autocomplete: 'off', class: 'base-input', value: ff.object.bcc_completed, id: ff.field_id(:bcc_completed) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-3">
|
||||
@@ -41,7 +41,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'templates_code_modal/preferences' %>
|
||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mb-5' }, data: { close_on_submit: false } do |f| %>
|
||||
<%= f.fields_for :preferences, Struct.new(:default_expire_at_duration, :default_expire_at).new(@template.preferences['default_expire_at_duration'], @template.preferences['default_expire_at'] ? Time.zone.parse(@template.preferences['default_expire_at']).in_time_zone(current_account.timezone) : nil) do |ff| %>
|
||||
<div class="form-control">
|
||||
<% duration_options = Templates::EXPIRATION_DURATIONS.keys.map { |duration| [t(duration), duration] } + [[t('specified_date'), 'specified_date']] %>
|
||||
<%= ff.label :default_expire_at_duration, t('default_expiration'), class: 'label pt-0' %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= ff.select :default_expire_at_duration, duration_options, { include_blank: t('none') }, required: false, class: 'base-select flex-1', dir: 'auto', autocomplete: 'off', onchange: "this.value == 'specified_date' ? window.template_preferences_default_expire_at.classList.remove('hidden') : [window.template_preferences_default_expire_at.classList.add('hidden'), window.template_preferences_default_expire_at.value = '', this.form.requestSubmit()]" %>
|
||||
<%= ff.datetime_field :default_expire_at, required: false, class: ['base-input flex-1', ff.object.default_expire_at.blank? && 'hidden'].compact_blank.join(' '), dir: 'auto', autocomplete: 'off', onchange: 'this.value && this.form.requestSubmit()' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="collapse collapse-arrow join-item border border-base-300 mb-2">
|
||||
<input type="checkbox" name="accordion">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
@@ -273,6 +284,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'templates_code_modal/preferences', class: 'pt-2' %>
|
||||
</div>
|
||||
<% if show_recipients %>
|
||||
<div id="recipients" class="hidden mt-2 mb-4 px-5">
|
||||
@@ -368,8 +380,18 @@
|
||||
<%= t('embedding_url') %>
|
||||
</label>
|
||||
<div class="flex gap-2 mb-4 mt-2">
|
||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'w-full mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||
<div class="flex gap-2">
|
||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
||||
<check-on-click data-element-id="template_shared_link">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</check-on-click>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-1 pt-3">
|
||||
<span><%= t('enable_shared_link') %></span>
|
||||
<%= f.check_box :shared_link, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'templates_code_modal/placeholder' %>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
|
||||
<div class="mt-2 mb-4 px-5">
|
||||
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
|
||||
<div class="flex items-center justify-between gap-1 px-1">
|
||||
<span><%= t('enable_shared_link') %></span>
|
||||
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
||||
<check-on-click data-element-id="template_shared_link">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</check-on-click>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||