mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac6d5a7aed | ||
|
|
3166a31b67 | ||
|
|
fc3283eb3a | ||
|
|
d6a96d4aa3 | ||
|
|
cda349d71c | ||
|
|
87345a3b3d | ||
|
|
b791d0699c | ||
|
|
2cb15f3282 | ||
|
|
8b823ca0d0 | ||
|
|
f3f08797f7 | ||
|
|
cab9672d25 | ||
|
|
c8d3b1bace | ||
|
|
5ee51b2b57 | ||
|
|
525dc503a6 | ||
|
|
12cf28792b | ||
|
|
33466c1d07 | ||
|
|
226ae2172e | ||
|
|
8e2700b8b8 | ||
|
|
753cbd4d27 | ||
|
|
f16405ca70 | ||
|
|
23f052c400 | ||
|
|
90a0a03e15 | ||
|
|
2dba8ae366 | ||
|
|
5bb7ad571c | ||
|
|
0cdb7c2c4f | ||
|
|
11bdc522cd | ||
|
|
014835f90d | ||
|
|
05e66b60ee | ||
|
|
505b185cf2 | ||
|
|
50c32ac6ff | ||
|
|
878ab25fd4 | ||
|
|
1b31772ff6 | ||
|
|
d4940234b7 | ||
|
|
9bdc5d4863 | ||
|
|
a73fd7929c | ||
|
|
8269a61751 | ||
|
|
de072d1437 | ||
|
|
5b24793f2b | ||
|
|
660f7ab8f3 | ||
|
|
6e3fea5fa0 | ||
|
|
29ecbecbc2 | ||
|
|
ad3bbe9892 | ||
|
|
b570ebee69 | ||
|
|
207780fa3f | ||
|
|
8aa219433b | ||
|
|
cb1e9f2be4 | ||
|
|
202943f58d | ||
|
|
6dd15b2091 | ||
|
|
1c55e4f18b | ||
|
|
6cc36b1728 | ||
|
|
37751cd5c1 | ||
|
|
80d882cd20 | ||
|
|
8bef75e570 | ||
|
|
077eab7005 | ||
|
|
715c70d422 | ||
|
|
a3c23f9d53 | ||
|
|
c1e67a8af0 | ||
|
|
2fdc294293 | ||
|
|
1f49ba069e | ||
|
|
43ef0fc0d0 | ||
|
|
a1a920e536 | ||
|
|
dec33b4331 | ||
|
|
432f290633 | ||
|
|
cf98616a27 | ||
|
|
1a720cf1ad | ||
|
|
557c6c34f2 | ||
|
|
943c76505d | ||
|
|
66eb83e6a7 | ||
|
|
0844427357 | ||
|
|
72e9d9ea2d |
No files matched your search
@@ -8,7 +8,8 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"vue/no-deprecated-html-element-is": 0,
|
"vue/no-deprecated-html-element-is": 0,
|
||||||
"vue/no-mutating-props": 0
|
"vue/no-mutating-props": 0,
|
||||||
|
"vue/one-component-per-file": 0
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"ecmaVersion": 2022,
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ Layout/LineLength:
|
|||||||
AllowedPatterns: ['\A\s*#']
|
AllowedPatterns: ['\A\s*#']
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 40
|
Max: 45
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 500
|
Max: 500
|
||||||
|
|||||||
+1
-2
@@ -36,7 +36,6 @@ FROM ruby:3.2.2-alpine3.18 as app
|
|||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
ENV PIDFILE=/dev/null
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -67,4 +66,4 @@ WORKDIR /data/docuseal
|
|||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/rails", "server"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- Signing available in 12 Languages
|
- Signing available in 13 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
@@ -51,11 +51,13 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- User roles
|
- User roles
|
||||||
- Automated reminders
|
- Automated reminders
|
||||||
- Invitation and identify verification via SMS
|
- Invitation and identify verification via SMS
|
||||||
|
- Conditional fields and formulas
|
||||||
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
- [Learn more](https://www.docuseal.co/pricing)
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
@@ -65,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||||
|**DigitalOcean**|**Render**|
|
|**DigitalOcean**|**Render**|
|
||||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|**Koyeb**|**Elestio**|
|
|
||||||
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
|
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@@ -100,3 +99,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
|||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||||
|
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||||
|
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||||
@@ -27,8 +27,6 @@ module Api
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -161,14 +161,15 @@ module Api
|
|||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to,
|
||||||
{
|
{
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id,
|
:completed, :phone, :application_key, :external_id, :reply_to,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [%i[name default_value title description
|
fields: [:name, :default_value, :title, :description,
|
||||||
readonly validation_pattern invalid_message]] }]]
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [] }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class SubmittersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :submitter, parent: false
|
|
||||||
|
|
||||||
SELECT_COLUMNS = %w[email phone name].freeze
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
submitters = search_submitters(@submitters)
|
|
||||||
|
|
||||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
|
||||||
|
|
||||||
render json: attrs
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def search_submitters(submitters)
|
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
|
||||||
|
|
||||||
term = "#{params[:q].downcase}%"
|
|
||||||
|
|
||||||
submitters.where(column.matches(term, false, true))
|
|
||||||
else
|
|
||||||
Submitters.search(submitters, params[:q])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -76,7 +76,8 @@ module Api
|
|||||||
:send_email, :send_sms, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id,
|
:completed, :phone, :application_key, :external_id,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
|
fields: [[:name, :default_value, :readonly, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplateFoldersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
|
||||||
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -49,6 +49,14 @@ module Api
|
|||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
|
||||||
|
if (item = @template.submitters[index])
|
||||||
|
item['name'] = role
|
||||||
|
else
|
||||||
|
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
if archived.in?([true, false])
|
if archived.in?([true, false])
|
||||||
@@ -83,24 +91,10 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
permit_params = [
|
|
||||||
:name,
|
|
||||||
{ schema: [%i[attachment_uuid name]],
|
|
||||||
submitters: [%i[name uuid]],
|
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
|
||||||
:required, :readonly, :default_value,
|
|
||||||
:title, :description,
|
|
||||||
{ preferences: {},
|
|
||||||
conditions: [%i[field_uuid value action]],
|
|
||||||
options: [%i[value uuid]],
|
|
||||||
validation: %i[message pattern],
|
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
|
||||||
]
|
|
||||||
|
|
||||||
if params.key?(:template)
|
if params.key?(:template)
|
||||||
params.require(:template).permit(*permit_params)
|
params.require(:template).permit(:name, submitters: [%i[name uuid]])
|
||||||
else
|
else
|
||||||
params.permit(*permit_params)
|
params.permit(:name, submitters: [%i[name uuid]])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplatesDocumentsController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template
|
|
||||||
|
|
||||||
def create
|
|
||||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {
|
|
||||||
schema:,
|
|
||||||
documents: documents.as_json(
|
|
||||||
methods: %i[metadata signed_uuid],
|
|
||||||
include: {
|
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
|
||||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,66 +7,18 @@ class DashboardController < ApplicationController
|
|||||||
before_action :maybe_render_landing
|
before_action :maybe_render_landing
|
||||||
before_action :maybe_redirect_mfa_setup
|
before_action :maybe_redirect_mfa_setup
|
||||||
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
skip_authorization_check
|
||||||
load_and_authorize_resource :template, parent: false
|
|
||||||
|
|
||||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
|
||||||
TEMPLATES_PER_PAGE = 12
|
|
||||||
FOLDERS_PER_PAGE = 18
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = filter_template_folders(@template_folders)
|
if cookies.permanent[:dashboard_view] == 'submissions'
|
||||||
|
SubmissionsDashboardController.dispatch(:index, request, response)
|
||||||
@pagy, @template_folders = pagy(
|
|
||||||
@template_folders,
|
|
||||||
items: FOLDERS_PER_PAGE,
|
|
||||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
|
||||||
@templates = @templates.none
|
|
||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
TemplatesDashboardController.dispatch(:index, request, response)
|
||||||
@templates = filter_templates(@templates)
|
|
||||||
|
|
||||||
items =
|
|
||||||
if @template_folders.size < 4
|
|
||||||
TEMPLATES_PER_PAGE
|
|
||||||
else
|
|
||||||
(@template_folders.size < 7 ? 9 : 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items:)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_template_folders(template_folders)
|
|
||||||
rel = template_folders.joins(:active_templates)
|
|
||||||
.order(id: :desc)
|
|
||||||
.distinct
|
|
||||||
|
|
||||||
TemplateFolders.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_templates(templates)
|
|
||||||
rel = templates.active.preload(:author).order(id: :desc)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_product_url
|
def maybe_redirect_product_url
|
||||||
return if !Docuseal.multitenant? || signed_in?
|
return if !Docuseal.multitenant? || signed_in?
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
super do |resource|
|
||||||
|
resource.errors.clear unless Docuseal.multitenant?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
super do |resource|
|
super do |resource|
|
||||||
Current.user = resource
|
Current.user = resource
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
class SsoSettingsController < ApplicationController
|
class SsoSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
authorize_resource :encrypted_config, only: :index
|
authorize_resource :encrypted_config, only: :index
|
||||||
authorize_resource :encrypted_config, parent: false, except: :index
|
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsArchivedController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@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.search(@submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsDashboardController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submissions = @submissions.joins(:template)
|
||||||
|
|
||||||
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
|
.where(templates: { archived_at: nil })
|
||||||
|
.preload(:created_by_user, template: :author)
|
||||||
|
|
||||||
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
|
@submissions = @submissions.pending if params[:status] == 'pending'
|
||||||
|
@submissions = @submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,8 +31,6 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
cookies[:submitter_sid] = @submitter.signed_id
|
|
||||||
|
|
||||||
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
|
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, parent: false
|
||||||
|
|
||||||
|
SELECT_COLUMNS = %w[email phone name].freeze
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = search_submitters(@submitters)
|
||||||
|
|
||||||
|
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||||
|
|
||||||
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
|
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||||
|
|
||||||
|
render json: attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def search_submitters(submitters)
|
||||||
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|
||||||
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
|
submitters.where(column.matches(term, false, true))
|
||||||
|
else
|
||||||
|
Submitters.search(submitters, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||||
|
|
||||||
|
old_fields_hash = @template.fields.hash
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
|
schema = documents.map do |doc|
|
||||||
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
schema:,
|
||||||
|
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||||
|
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||||
|
documents: documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: {
|
||||||
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||||
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q])
|
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
|
|
||||||
@@ -67,6 +67,12 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].present?
|
||||||
@@ -86,6 +92,22 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(
|
||||||
|
:name,
|
||||||
|
{ schema: [%i[attachment_uuid name]],
|
||||||
|
submitters: [%i[name uuid]],
|
||||||
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
def authorized_clone_account_id?(account_id)
|
||||||
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
end
|
end
|
||||||
@@ -98,10 +120,6 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
|
||||||
params.require(:template).permit(:name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDashboardController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||||
|
TEMPLATES_PER_PAGE = 12
|
||||||
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
|
def index
|
||||||
|
@template_folders = filter_template_folders(@template_folders)
|
||||||
|
|
||||||
|
@pagy, @template_folders = pagy(
|
||||||
|
@template_folders,
|
||||||
|
items: FOLDERS_PER_PAGE,
|
||||||
|
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||||
|
@templates = @templates.none
|
||||||
|
else
|
||||||
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
|
@templates = filter_templates(@templates)
|
||||||
|
|
||||||
|
items =
|
||||||
|
if @template_folders.size < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @templates = pagy(@templates, items:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filter_template_folders(template_folders)
|
||||||
|
rel = template_folders.joins(:active_templates)
|
||||||
|
.order(id: :desc)
|
||||||
|
.distinct
|
||||||
|
|
||||||
|
TemplateFolders.search(rel, params[:q])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_templates(templates)
|
||||||
|
rel = templates.active.preload(:author).order(id: :desc)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates.search(rel, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,11 +9,14 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
HexaPDF::Document.new(io: file.open)
|
HexaPDF::Document.new(io: file.open)
|
||||||
end
|
end
|
||||||
|
|
||||||
cert_data = if Docuseal.multitenant?
|
cert_data =
|
||||||
Docuseal::CERTS
|
if Docuseal.multitenant?
|
||||||
else
|
value = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
|
||||||
end
|
Docuseal::CERTS.merge(value)
|
||||||
|
else
|
||||||
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
end
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
|
|||||||
|
|
||||||
import { createApp, reactive } from 'vue'
|
import { createApp, reactive } from 'vue'
|
||||||
import TemplateBuilder from './template_builder/builder'
|
import TemplateBuilder from './template_builder/builder'
|
||||||
|
import ImportList from './template_builder/import_list'
|
||||||
|
|
||||||
import ToggleVisible from './elements/toggle_visible'
|
import ToggleVisible from './elements/toggle_visible'
|
||||||
|
import ToggleCookies from './elements/toggle_cookies'
|
||||||
import DisableHidden from './elements/disable_hidden'
|
import DisableHidden from './elements/disable_hidden'
|
||||||
import TurboModal from './elements/turbo_modal'
|
import TurboModal from './elements/turbo_modal'
|
||||||
import FileDropzone from './elements/file_dropzone'
|
import FileDropzone from './elements/file_dropzone'
|
||||||
@@ -55,6 +57,7 @@ window.customElements.define('signature-form', SignatureForm)
|
|||||||
window.customElements.define('submit-form', SubmitForm)
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
window.customElements.define('prompt-password', PromptPassword)
|
window.customElements.define('prompt-password', PromptPassword)
|
||||||
window.customElements.define('emails-textarea', EmailsTextarea)
|
window.customElements.define('emails-textarea', EmailsTextarea)
|
||||||
|
window.customElements.define('toggle-cookies', ToggleCookies)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
@@ -91,12 +94,33 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
withPayment: this.dataset.withPayment === 'true',
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
|
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||||
withFormula: this.dataset.withFormula === 'true',
|
withFormula: this.dataset.withFormula === 'true',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
})
|
||||||
|
|
||||||
|
this.app.mount(this.appElem)
|
||||||
|
|
||||||
|
this.appendChild(this.appElem)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.app?.unmount()
|
||||||
|
this.appElem?.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.customElements.define('import-list', class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
|
this.app = createApp(ImportList, {
|
||||||
|
template: JSON.parse(this.dataset.template),
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.app.mount(this.appElem)
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ button[disabled] .enabled {
|
|||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-pre:before {
|
||||||
|
white-space: pre;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:after {
|
.tooltip-bottom-end:after {
|
||||||
transform: translateX(-25%);
|
transform: translateX(-25%);
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ export default targetable(class extends HTMLElement {
|
|||||||
fileRequests.reduce(
|
fileRequests.reduce(
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
)
|
).finally(() => {
|
||||||
|
this.toggleState()
|
||||||
this.toggleState()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.isDirectUpload === 'true') {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventListener('drop', this.onDrop)
|
this.addEventListener('drop', this.onDrop)
|
||||||
|
|
||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
@@ -48,61 +44,11 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.classList.toggle('opacity-50')
|
this.classList.toggle('opacity-50')
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFiles (files) {
|
uploadFiles () {
|
||||||
this.toggleLoading()
|
this.toggleLoading()
|
||||||
|
|
||||||
if (this.dataset.isDirectUpload === 'true') {
|
if (this.dataset.submitOnUpload) {
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
this.closest('form').querySelector('button[type="submit"]').click()
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(files).map(async (file) => {
|
|
||||||
const upload = new DirectUpload(
|
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
upload.create((error, blob) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return reject(error)
|
|
||||||
} else {
|
|
||||||
return resolve(blob)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
).then((blobs) => {
|
|
||||||
if (this.dataset.submitOnUpload) {
|
|
||||||
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
|
|
||||||
}
|
|
||||||
|
|
||||||
blobs.forEach((blob) => {
|
|
||||||
const input = document.createElement('input')
|
|
||||||
|
|
||||||
input.type = 'hidden'
|
|
||||||
input.name = 'blob_signed_ids[]'
|
|
||||||
input.value = blob.signed_id
|
|
||||||
|
|
||||||
this.append(input)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.dataset.submitOnUpload === 'true') {
|
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
if (this.dataset.submitOnUpload !== 'true') {
|
|
||||||
this.toggleLoading()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (this.dataset.submitOnUpload) {
|
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
|
|||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
resolve(items)
|
resolve(items)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default class extends HTMLElement {
|
|||||||
if (q) {
|
if (q) {
|
||||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||||
|
|
||||||
this.currentFetch ||= fetch('/api/submitters_autocomplete?' + queryParams)
|
this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
|
||||||
|
|
||||||
this.currentFetch.then(async (resp) => {
|
this.currentFetch.then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.button.addEventListener('click', () => {
|
||||||
|
const expirationDate = new Date()
|
||||||
|
|
||||||
|
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
|
||||||
|
|
||||||
|
const expires = expirationDate.toUTCString()
|
||||||
|
|
||||||
|
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('button')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,14 +11,15 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
completedButton: JSON.parse(this.dataset.completedButton),
|
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
|
||||||
|
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
@upload="onUpload"
|
@upload="onUpload"
|
||||||
/>
|
/>
|
||||||
@@ -86,11 +85,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -3,16 +3,24 @@
|
|||||||
class="mx-auto max-w-md flex flex-col"
|
class="mx-auto max-w-md flex flex-col"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||||
<IconCircleCheck
|
<IconCircleCheck
|
||||||
class="inline text-green-600"
|
class="inline text-green-600"
|
||||||
:width="30"
|
:width="30"
|
||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ t('form_has_been_completed') }}
|
{{ completedMessage.title || t('form_has_been_completed') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="completedMessage.body"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
:string="completedMessage.body"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
@@ -92,10 +100,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
components: {
|
components: {
|
||||||
|
MarkdownContent,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
@@ -143,6 +153,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
completedMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -212,9 +227,9 @@ export default {
|
|||||||
fileRequests.reduce(
|
fileRequests.reduce(
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
)
|
).finally(() => {
|
||||||
|
this.isDownloading = false
|
||||||
this.isDownloading = false
|
})
|
||||||
},
|
},
|
||||||
sanitizeHref (href) {
|
sanitizeHref (href) {
|
||||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
|||||||
@@ -70,11 +70,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: '*/*'
|
default: '*/*'
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -92,11 +87,6 @@ export default {
|
|||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
this.uploadFiles(e.dataTransfer.files)
|
this.uploadFiles(e.dataTransfer.files)
|
||||||
@@ -113,72 +103,25 @@ export default {
|
|||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
return await Promise.all(
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
Array.from(files).map((file) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
const blobs = await Promise.all(
|
formData.append('file', file)
|
||||||
Array.from(files).map(async (file) => {
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
const upload = new DirectUpload(
|
formData.append('name', 'attachments')
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.$refs.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
upload.create((error, blob) => {
|
method: 'POST',
|
||||||
if (error) {
|
body: formData
|
||||||
console.error(error)
|
}).then(resp => resp.json()).then((data) => {
|
||||||
|
return data
|
||||||
return reject(error)
|
|
||||||
} else {
|
|
||||||
return resolve(blob)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
)
|
})).then((result) => {
|
||||||
|
this.$emit('upload', result)
|
||||||
return await Promise.all(
|
}).finally(() => {
|
||||||
blobs.map((blob) => {
|
this.isLoading = false
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
})
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: 'attachments',
|
|
||||||
blob_signed_id: blob.signed_id,
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then(resp => resp.json()).then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
})).then((result) => {
|
|
||||||
this.$emit('upload', result)
|
|
||||||
}).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return await Promise.all(
|
|
||||||
Array.from(files).map((file) => {
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
formData.append('file', file)
|
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
|
||||||
formData.append('name', 'attachments')
|
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
}).then(resp => resp.json()).then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
})).then((result) => {
|
|
||||||
this.$emit('upload', result)
|
|
||||||
}).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
:value="option.value || `${t('option')} ${index + 1}`"
|
:value="option.value || `${t('option')} ${index + 1}`"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
|
@click="scrollIntoField(currentField)"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
@@ -305,7 +306,6 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@@ -318,7 +318,6 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousSignatureValueFor(currentField)"
|
:previous-value="previousSignatureValueFor(currentField)"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -334,7 +333,6 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousInitialsValue"
|
:previous-value="previousInitialsValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -347,7 +345,6 @@
|
|||||||
v-else-if="currentField.type === 'file'"
|
v-else-if="currentField.type === 'file'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -417,6 +414,7 @@
|
|||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||||
|
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||||
:with-download-button="withDownloadButton && !completedRedirectUrl"
|
:with-download-button="withDownloadButton && !completedRedirectUrl"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
@@ -576,11 +574,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
allowToSkip: {
|
allowToSkip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -611,6 +604,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -635,6 +633,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
completedMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -658,9 +661,6 @@ export default {
|
|||||||
queryParams () {
|
queryParams () {
|
||||||
return new URLSearchParams(window.location.search)
|
return new URLSearchParams(window.location.search)
|
||||||
},
|
},
|
||||||
authenticityToken () {
|
|
||||||
return document.querySelector('meta[name="csrf-token"]')?.content
|
|
||||||
},
|
|
||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -782,6 +782,72 @@ const ar = {
|
|||||||
files: 'الملفات'
|
files: 'الملفات'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar }
|
const ko = {
|
||||||
|
text: '텍스트',
|
||||||
|
signature: '서명',
|
||||||
|
initials: '이니셜',
|
||||||
|
date: '날짜',
|
||||||
|
number: '숫자',
|
||||||
|
image: '이미지',
|
||||||
|
take_photo: '사진 찍기',
|
||||||
|
file: '파일',
|
||||||
|
select: '선택',
|
||||||
|
checkbox: '체크박스',
|
||||||
|
multiple: '다중',
|
||||||
|
radio: '라디오',
|
||||||
|
cells: '셀',
|
||||||
|
stamp: '스탬프',
|
||||||
|
minimize: '최소화',
|
||||||
|
payment: '결제',
|
||||||
|
phone: '전화',
|
||||||
|
submit_form: '양식 제출',
|
||||||
|
sign_now: '지금 서명',
|
||||||
|
type_here_: '여기에 입력...',
|
||||||
|
optional: '선택 사항',
|
||||||
|
option: '옵션',
|
||||||
|
appears_on: '다음에 나타납니다',
|
||||||
|
page: '페이지',
|
||||||
|
select_your_option: '옵션 선택',
|
||||||
|
complete_hightlighted_checkboxes_and_click: '강조된 체크박스를 완료하고 클릭',
|
||||||
|
submit: '제출',
|
||||||
|
next: '다음',
|
||||||
|
click_to_upload: '업로드하려면 클릭',
|
||||||
|
or_drag_and_drop_files: '또는 파일을 드래그하여 놓으십시오',
|
||||||
|
send_copy_via_email: '이메일로 사본 보내기',
|
||||||
|
download: '다운로드',
|
||||||
|
clear: '지우기',
|
||||||
|
redraw: '다시 그리기',
|
||||||
|
draw_initials: '이니셜 그리기',
|
||||||
|
type_signature_here: '여기에 서명 입력',
|
||||||
|
type_initial_here: '여기에 이니셜 입력',
|
||||||
|
form_has_been_completed: '양식이 완료되었습니다!',
|
||||||
|
create_a_free_account: '무료 계정 생성',
|
||||||
|
signed_with: '다음으로 서명:',
|
||||||
|
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
|
||||||
|
open_source_documents_software: '오픈 소스 문서 소프트웨어',
|
||||||
|
verified_phone_number: '전화번호 확인됨',
|
||||||
|
use_international_format: '국제 포맷 사용: +1xxx',
|
||||||
|
six_digits_code: '6자리 코드',
|
||||||
|
change_phone_number: '전화번호 변경',
|
||||||
|
sending: '전송 중...',
|
||||||
|
resend_code: '코드 다시 보내기',
|
||||||
|
verification_code_has_been_resent: '인증 코드가 SMS로 다시 전송되었습니다',
|
||||||
|
please_fill_all_required_fields: '필수 필드를 모두 입력하십시오',
|
||||||
|
set_today: '오늘 날짜로 설정',
|
||||||
|
toggle_multiline_text: '다중 텍스트 전환',
|
||||||
|
draw_signature: '서명 그리기',
|
||||||
|
type_initial: '이니셜 입력',
|
||||||
|
draw: '그리기',
|
||||||
|
type: '입력',
|
||||||
|
type_text: '텍스트 입력',
|
||||||
|
email_has_been_sent: '이메일이 전송되었습니다',
|
||||||
|
processing: '처리 중',
|
||||||
|
pay_with_strip: '스트라이프로 결제',
|
||||||
|
reupload: '다시 업로드',
|
||||||
|
upload: '업로드',
|
||||||
|
files: '파일'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
@@ -38,7 +38,6 @@
|
|||||||
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:accept="'image/*'"
|
:accept="'image/*'"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
@upload="onImageUpload"
|
@upload="onImageUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,11 +61,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -152,11 +152,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -202,10 +197,6 @@ export default {
|
|||||||
this.$refs.textInput?.focus()
|
this.$refs.textInput?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -293,45 +284,21 @@ export default {
|
|||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
||||||
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
const formData = new FormData()
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
|
||||||
|
|
||||||
new DirectUpload(
|
formData.append('file', file)
|
||||||
file,
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
'/direct_uploads'
|
formData.append('name', 'attachments')
|
||||||
).create((_error, data) => {
|
|
||||||
fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
blob_signed_id: data.signed_id,
|
|
||||||
name: 'attachments'
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
})
|
method: 'POST',
|
||||||
})
|
body: formData
|
||||||
} else {
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
const formData = new FormData()
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
formData.append('file', file)
|
return resolve(attachment)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
})
|
||||||
formData.append('name', 'attachments')
|
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<label
|
<label
|
||||||
:for="option.uuid"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3"
|
||||||
|
@click="scrollIntoField(field)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="option.uuid"
|
:id="option.uuid"
|
||||||
|
|||||||
@@ -176,11 +176,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
withTypedSignature: {
|
withTypedSignature: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -230,10 +225,6 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -390,45 +381,21 @@ export default {
|
|||||||
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
const formData = new FormData()
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
|
||||||
|
|
||||||
new DirectUpload(
|
formData.append('file', file)
|
||||||
file,
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
'/direct_uploads'
|
formData.append('name', 'attachments')
|
||||||
).create((_error, data) => {
|
|
||||||
fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
blob_signed_id: data.signed_id,
|
|
||||||
name: 'attachments'
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
})
|
method: 'POST',
|
||||||
})
|
body: formData
|
||||||
} else {
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
const formData = new FormData()
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
formData.append('file', file)
|
return resolve(attachment)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
})
|
||||||
formData.append('name', 'attachments')
|
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
return reject(error)
|
return reject(error)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
|
||||||
:allow-add-new="!defaultSubmitters.length"
|
:allow-add-new="!defaultSubmitters.length"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@update:model-value="updateName"
|
@update:model-value="updateName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center flex-shrink-0">
|
||||||
<slot
|
<slot
|
||||||
v-if="$slots.buttons"
|
v-if="$slots.buttons"
|
||||||
name="buttons"
|
name="buttons"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
@click="maybeShowEmptyTemplateAlert"
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconWritingSign
|
<IconWritingSign
|
||||||
width="20"
|
width="22"
|
||||||
class="inline"
|
class="inline"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
@@ -115,7 +115,6 @@
|
|||||||
:with-replace-button="withUploadButton"
|
:with-replace-button="withUploadButton"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@remove="onDocumentRemove"
|
@remove="onDocumentRemove"
|
||||||
@replace="onDocumentReplace"
|
@replace="onDocumentReplace"
|
||||||
@@ -131,7 +130,6 @@
|
|||||||
v-if="sortedDocuments.length && editable && withUploadButton"
|
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +146,6 @@
|
|||||||
v-if="!sortedDocuments.length && withUploadButton"
|
v-if="!sortedDocuments.length && withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -166,9 +163,10 @@
|
|||||||
:allow-draw="!onlyDefinedFields"
|
:allow-draw="!onlyDefinedFields"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
|
:draw-field-type="drawFieldType"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:base-url="baseUrl"
|
:base-url="baseUrl"
|
||||||
@draw="onDraw"
|
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
/>
|
/>
|
||||||
@@ -177,9 +175,9 @@
|
|||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||||
:with-replace-button="withUploadButton"
|
:with-replace-button="withUploadButton"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
:document="document"
|
:document="document"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
class="pb-2 mb-2 border-b border-base-300 border-dashed"
|
class="pb-2 mb-2 border-b border-base-300 border-dashed"
|
||||||
@remove="onDocumentRemove"
|
@remove="onDocumentRemove"
|
||||||
@replace="onDocumentReplace"
|
@replace="onDocumentReplace"
|
||||||
@@ -195,7 +193,6 @@
|
|||||||
<Upload
|
<Upload
|
||||||
v-if="withUploadButton"
|
v-if="withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
@@ -210,13 +207,13 @@
|
|||||||
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="drawField"
|
v-if="showDrawField || drawField"
|
||||||
class="sticky inset-0 h-full z-20"
|
class="sticky inset-0 h-full z-20"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
|
||||||
<p>
|
<p>
|
||||||
{{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
|
{{ t('draw_field_on_the_document').replace('{field}', drawField?.name || '') }}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
@@ -226,10 +223,10 @@
|
|||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
|
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials'].includes(drawField?.type || drawFieldType)"
|
||||||
href="#"
|
href="#"
|
||||||
class="link block mt-3 text-sm"
|
class="link block mt-3 text-sm"
|
||||||
@click.prevent="[drawField = null, drawOption = null]"
|
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||||
>
|
>
|
||||||
{{ t('or_add_field_without_drawing') }}
|
{{ t('or_add_field_without_drawing') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -244,12 +241,15 @@
|
|||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:with-help="withHelp"
|
:with-help="withHelp"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
|
:draw-field-type="drawFieldType"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
@add-field="addField"
|
||||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||||
@set-drag="dragField = $event"
|
@set-drag="dragField = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@drag-end="dragField = null"
|
@drag-end="dragField = null"
|
||||||
@@ -328,6 +328,7 @@ export default {
|
|||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
withPayment: this.withPayment,
|
withPayment: this.withPayment,
|
||||||
|
isPaymentConnected: this.isPaymentConnected,
|
||||||
withFormula: this.withFormula,
|
withFormula: this.withFormula,
|
||||||
withConditions: this.withConditions,
|
withConditions: this.withConditions,
|
||||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||||
@@ -340,11 +341,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
i18n: {
|
i18n: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -375,6 +371,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
withSelectedFieldType: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
defaultDrawFieldType: {
|
defaultDrawFieldType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -444,6 +445,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
withDocumentsList: {
|
withDocumentsList: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -459,6 +465,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
isPaymentConnected: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withFormula: {
|
withFormula: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -486,7 +497,9 @@ export default {
|
|||||||
isBreakpointLg: false,
|
isBreakpointLg: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
|
showDrawField: false,
|
||||||
drawField: null,
|
drawField: null,
|
||||||
|
drawFieldType: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null
|
dragField: null
|
||||||
}
|
}
|
||||||
@@ -519,6 +532,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
if (!this.template.fields?.length && this.template.submitters?.length === 1) {
|
||||||
|
if (this.template.submitters[0]?.name === 'First Party') {
|
||||||
|
this.template.submitters[0].name = this.t('first_party')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
|
const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
|
||||||
return this.template.submitters.find(e => e.name === name)?.uuid
|
return this.template.submitters.find(e => e.name === name)?.uuid
|
||||||
})
|
})
|
||||||
@@ -569,6 +588,34 @@ export default {
|
|||||||
t (key) {
|
t (key) {
|
||||||
return this.i18n[key] || i18nEn[key] || key
|
return this.i18n[key] || i18nEn[key] || key
|
||||||
},
|
},
|
||||||
|
addField (type, area = null) {
|
||||||
|
const field = {
|
||||||
|
name: '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: type !== 'checkbox',
|
||||||
|
areas: area ? [area] : [],
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields.push(field)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
startFieldDraw ({ name, type }) {
|
startFieldDraw ({ name, type }) {
|
||||||
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
|
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
|
||||||
|
|
||||||
@@ -649,15 +696,13 @@ export default {
|
|||||||
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
},
|
},
|
||||||
clearDrawField () {
|
clearDrawField () {
|
||||||
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
|
|
||||||
const fieldIndex = this.template.fields.indexOf(this.drawField)
|
|
||||||
|
|
||||||
if (fieldIndex !== -1) {
|
|
||||||
this.template.fields.splice(fieldIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
this.drawOption = null
|
this.drawOption = null
|
||||||
|
this.showDrawField = false
|
||||||
|
|
||||||
|
if (!this.withSelectedFieldType) {
|
||||||
|
this.drawFieldType = ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
@@ -707,6 +752,27 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setDefaultAreaSize (area, type) {
|
||||||
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
|
if (type === 'checkbox') {
|
||||||
|
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (type === 'image') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (type === 'signature' || type === 'stamp') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
||||||
|
} else if (type === 'initials') {
|
||||||
|
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
} else {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
}
|
||||||
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
if (this.drawOption) {
|
if (this.drawOption) {
|
||||||
@@ -729,25 +795,7 @@ export default {
|
|||||||
area.w = previousArea.w
|
area.w = previousArea.w
|
||||||
area.h = previousArea.h
|
area.h = previousArea.h
|
||||||
} else {
|
} else {
|
||||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
this.setDefaultAreaSize(area, this.drawOption ? 'checkbox' : this.drawField?.type)
|
||||||
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
|
||||||
|
|
||||||
if (this.drawField.type === 'checkbox' || this.drawOption) {
|
|
||||||
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
|
||||||
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
|
||||||
} else if (this.drawField.type === 'image') {
|
|
||||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
|
||||||
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
|
||||||
} else if (this.drawField.type === 'signature' || this.drawField.type === 'stamp') {
|
|
||||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
|
||||||
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
|
||||||
} else if (this.drawField.type === 'initials') {
|
|
||||||
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
|
||||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
|
||||||
} else {
|
|
||||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
|
||||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
area.x -= area.w / 2
|
area.x -= area.w / 2
|
||||||
@@ -782,13 +830,15 @@ export default {
|
|||||||
|
|
||||||
let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
||||||
|
|
||||||
if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
|
if (this.drawFieldType) {
|
||||||
|
type = this.drawFieldType
|
||||||
|
} else if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
|
||||||
type = this.defaultDrawFieldType
|
type = this.defaultDrawFieldType
|
||||||
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
|
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
|
||||||
type = this.fieldTypes[0]
|
type = this.fieldTypes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'checkbox') {
|
if (type === 'checkbox' && !this.drawFieldType) {
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
||||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||||
|
|
||||||
@@ -806,21 +856,22 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (area.w) {
|
if (this.drawFieldType && (area.w === 0 || area.h === 0)) {
|
||||||
const field = {
|
if (this.selectedField?.type === this.drawFieldType) {
|
||||||
name: '',
|
area.w = this.selectedAreaRef.value.w
|
||||||
uuid: v4(),
|
area.h = this.selectedAreaRef.value.h
|
||||||
required: type !== 'checkbox',
|
} else {
|
||||||
type,
|
this.setDefaultAreaSize(area, this.drawFieldType)
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
|
||||||
areas: [area]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.template.fields.push(field)
|
area.x -= area.w / 2
|
||||||
|
area.y -= area.h / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (area.w) {
|
||||||
|
this.addField(type, area)
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
|
|
||||||
this.save()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -921,14 +972,26 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
updateFromUpload ({ schema, documents }) {
|
updateFromUpload (data) {
|
||||||
this.template.schema.push(...schema)
|
this.template.schema.push(...data.schema)
|
||||||
this.template.documents.push(...documents)
|
this.template.documents.push(...data.documents)
|
||||||
|
|
||||||
|
if (data.fields) {
|
||||||
|
this.template.fields = data.fields
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.submitters) {
|
||||||
|
this.template.submitters = data.submitters
|
||||||
|
|
||||||
|
if (!this.template.submitters.find((s) => s.uuid === this.selectedSubmitter?.uuid)) {
|
||||||
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
|
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
|
||||||
|
|
||||||
this.scrollIntoDocument(schema[0])
|
this.scrollIntoDocument(data.schema[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.template.name === 'New Document') {
|
if (this.template.name === 'New Document') {
|
||||||
@@ -951,19 +1014,56 @@ export default {
|
|||||||
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removedFieldUuids = []
|
||||||
|
|
||||||
this.template.fields.forEach((field) => {
|
this.template.fields.forEach((field) => {
|
||||||
[...(field.areas || [])].forEach((area) => {
|
[...(field.areas || [])].forEach((area) => {
|
||||||
if (area.attachment_uuid === item.attachment_uuid) {
|
if (area.attachment_uuid === item.attachment_uuid) {
|
||||||
field.areas.splice(field.areas.indexOf(area), 1)
|
field.areas.splice(field.areas.indexOf(area), 1)
|
||||||
|
|
||||||
|
removedFieldUuids.push(field.uuid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.template.fields =
|
||||||
|
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
onDocumentReplace ({ replaceSchemaItem, schema, documents }) {
|
onDocumentReplace (data) {
|
||||||
|
const { replaceSchemaItem, schema, documents } = data
|
||||||
|
|
||||||
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
|
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
|
||||||
this.template.documents.push(...documents)
|
this.template.documents.push(...documents)
|
||||||
|
|
||||||
|
if (data.fields) {
|
||||||
|
this.template.fields = data.fields
|
||||||
|
|
||||||
|
const removedFieldUuids = []
|
||||||
|
|
||||||
|
this.template.fields.forEach((field) => {
|
||||||
|
[...(field.areas || [])].forEach((area) => {
|
||||||
|
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
||||||
|
field.areas.splice(field.areas.indexOf(area), 1)
|
||||||
|
|
||||||
|
removedFieldUuids.push(field.uuid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.template.fields =
|
||||||
|
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.submitters) {
|
||||||
|
this.template.submitters = data.submitters
|
||||||
|
|
||||||
|
if (!this.template.submitters.find((s) => s.uuid === this.selectedSubmitter?.uuid)) {
|
||||||
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.template.fields.forEach((field) => {
|
this.template.fields.forEach((field) => {
|
||||||
(field.areas || []).forEach((area) => {
|
(field.areas || []).forEach((area) => {
|
||||||
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
||||||
@@ -1023,7 +1123,11 @@ export default {
|
|||||||
baseFetch (path, options = {}) {
|
baseFetch (path, options = {}) {
|
||||||
return fetch(this.baseUrl + path, {
|
return fetch(this.baseUrl + path, {
|
||||||
...options,
|
...options,
|
||||||
headers: { ...this.fetchOptions.headers, ...options.headers }
|
headers: {
|
||||||
|
'X-CSRF-Token': this.authenticityToken,
|
||||||
|
...this.fetchOptions.headers,
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save ({ force } = { force: false }) {
|
save ({ force } = { force: false }) {
|
||||||
@@ -1039,7 +1143,7 @@ export default {
|
|||||||
|
|
||||||
this.pushUndo()
|
this.pushUndo()
|
||||||
|
|
||||||
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
template: {
|
template: {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
>
|
>
|
||||||
{{ value }}
|
<template
|
||||||
|
v-for="(item, index) in (value || '').split(/(_)/)"
|
||||||
|
:key="index"
|
||||||
|
>{{ item }}<wbr></template>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="withRequired"
|
v-if="withRequired"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
/>
|
/>
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
v-if="withReplaceButton"
|
v-if="withReplaceButton"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
@click.stop
|
@click.stop
|
||||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
/>
|
/>
|
||||||
@@ -63,10 +63,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
acceptFileTypes: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: 'image/*, application/pdf'
|
||||||
},
|
},
|
||||||
withReplaceButton: {
|
withReplaceButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -80,11 +80,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['change', 'remove', 'up', 'down', 'replace'],
|
emits: ['change', 'remove', 'up', 'down', 'replace'],
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onUpdateName (value) {
|
onUpdateName (value) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
|
:draw-field-type="drawFieldType"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:image="image"
|
:image="image"
|
||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||||
@@ -43,6 +44,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
@drop.prevent="onDropFiles"
|
@drop.prevent="onDropFiles"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="w-full relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed"
|
class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
:class="{ 'opacity-50': isLoading || isProcessing }"
|
:class="{ 'opacity-50': isLoading || isProcessing }"
|
||||||
>
|
>
|
||||||
@@ -71,11 +71,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
},
|
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
@@ -101,11 +96,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ export default {
|
|||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
},
|
},
|
||||||
dateFormats () {
|
dateFormats () {
|
||||||
return [
|
const formats = [
|
||||||
'MM/DD/YYYY',
|
'MM/DD/YYYY',
|
||||||
'DD/MM/YYYY',
|
'DD/MM/YYYY',
|
||||||
'YYYY-MM-DD',
|
'YYYY-MM-DD',
|
||||||
@@ -593,6 +593,16 @@ export default {
|
|||||||
'D MMM YYYY',
|
'D MMM YYYY',
|
||||||
'D MMMM YYYY'
|
'D MMMM YYYY'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
||||||
|
formats.push('YYYY년 MM월 DD일')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.format && !formats.includes(this.field.preferences.format)) {
|
||||||
|
formats.unshift(this.field.preferences.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="compact"
|
v-if="compact"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:title="selectedSubmitter.name"
|
:title="selectedSubmitter?.name"
|
||||||
class="cursor-pointer text-base-100 flex h-full items-center justify-center"
|
class="cursor-pointer text-base-100 flex h-full items-center justify-center"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -36,32 +36,63 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="submitterDefaultFields.length">
|
<div v-if="submitterDefaultFields.length">
|
||||||
<hr class="mb-2">
|
<hr class="mb-2">
|
||||||
<template
|
<template v-if="isShowFieldSearch">
|
||||||
v-for="field in submitterDefaultFields"
|
<input
|
||||||
:key="field.name"
|
v-model="defaultFieldsSearch"
|
||||||
|
:placeholder="t('search_field')"
|
||||||
|
class="input input-ghost input-xs px-0 text-base mb-2 !outline-0 !rounded bg-transparent w-full"
|
||||||
|
>
|
||||||
|
<hr class="mb-2">
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
class="overflow-auto relative"
|
||||||
|
:style="{
|
||||||
|
maxHeight: isShowFieldSearch ? '210px' : '',
|
||||||
|
minHeight: isShowFieldSearch ? '210px' : ''
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
v-if="!filteredSubmitterDefaultFields.length && defaultFieldsSearch"
|
||||||
draggable="true"
|
class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
|
||||||
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
|
|
||||||
@dragstart="onDragstart({ type: 'text', ...field })"
|
|
||||||
@dragend="$emit('drag-end')"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative cursor-grab">
|
<div>
|
||||||
<div class="flex items-center p-1 space-x-1">
|
{{ t('field_not_found') }}
|
||||||
<IconDrag />
|
</div>
|
||||||
<FieldType
|
<a
|
||||||
:model-value="field.type || 'text'"
|
href="#"
|
||||||
:editable="false"
|
class="link"
|
||||||
:button-width="20"
|
@click.prevent="defaultFieldsSearch = ''"
|
||||||
/>
|
>
|
||||||
<span class="block pl-0.5">
|
{{ t('clear') }}
|
||||||
{{ field.name }}
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
<template
|
||||||
|
v-for="field in filteredSubmitterDefaultFields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
draggable="true"
|
||||||
|
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
|
||||||
|
@dragstart="onDragstart({ type: 'text', ...field })"
|
||||||
|
@dragend="$emit('drag-end')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between relative cursor-grab">
|
||||||
|
<div class="flex items-center p-1 space-x-1">
|
||||||
|
<IconDrag />
|
||||||
|
<FieldType
|
||||||
|
:model-value="field.type || 'text'"
|
||||||
|
:editable="false"
|
||||||
|
:button-width="20"
|
||||||
|
/>
|
||||||
|
<span class="block pl-0.5">
|
||||||
|
{{ field.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable && !onlyDefinedFields"
|
v-if="editable && !onlyDefinedFields"
|
||||||
@@ -74,14 +105,18 @@
|
|||||||
<button
|
<button
|
||||||
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
||||||
@dragstart="onDragstart({ type: type })"
|
@dragstart="onDragstart({ type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="addField(type)"
|
@click="['file', 'payment'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
||||||
>
|
>
|
||||||
<div class="flex items-console group-hover:bg-base-200/50 transition-all cursor-grab h-full absolute left-0">
|
<div
|
||||||
<IconDrag class=" my-auto" />
|
class="flex items-console transition-all cursor-grab h-full absolute left-0"
|
||||||
|
:class="drawFieldType === type ? 'bg-base-200/50' : 'group-hover:bg-base-200/50'"
|
||||||
|
>
|
||||||
|
<IconDrag class="my-auto" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col px-2 py-2">
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
<component :is="icon" />
|
<component :is="icon" />
|
||||||
@@ -139,10 +174,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { v4 } from 'uuid'
|
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock } from '@tabler/icons-vue'
|
import { IconLock, IconCirclePlus } from '@tabler/icons-vue'
|
||||||
import IconDrag from './icon_drag'
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -150,6 +184,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
IconCirclePlus,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
@@ -180,6 +215,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -204,10 +244,18 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
defaultFieldsSearch: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
isShowFieldSearch () {
|
||||||
|
return this.submitterDefaultFields.length > 15
|
||||||
|
},
|
||||||
fieldIconsSorted () {
|
fieldIconsSorted () {
|
||||||
if (this.fieldTypes.length) {
|
if (this.fieldTypes.length) {
|
||||||
return this.fieldTypes.reduce((acc, type) => {
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
@@ -226,6 +274,13 @@ export default {
|
|||||||
return this.defaultFields.filter((f) => {
|
return this.defaultFields.filter((f) => {
|
||||||
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
filteredSubmitterDefaultFields () {
|
||||||
|
if (this.defaultFieldsSearch) {
|
||||||
|
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
|
||||||
|
} else {
|
||||||
|
return this.submitterDefaultFields
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -286,38 +341,6 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
addField (type, area = null) {
|
|
||||||
const field = {
|
|
||||||
name: '',
|
|
||||||
uuid: v4(),
|
|
||||||
required: type !== 'checkbox',
|
|
||||||
areas: [],
|
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
|
||||||
type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'stamp') {
|
|
||||||
field.readonly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'date') {
|
|
||||||
field.preferences = {
|
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fields.push(field)
|
|
||||||
|
|
||||||
if (!['payment', 'file'].includes(type)) {
|
|
||||||
this.$emit('set-draw', { field })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
search_field: 'Search field',
|
||||||
|
field_not_found: 'Field not found',
|
||||||
|
clear: 'Clear',
|
||||||
align: 'Align',
|
align: 'Align',
|
||||||
left: 'Left',
|
left: 'Left',
|
||||||
right: 'Right',
|
right: 'Right',
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="selectedSheetIndex === null && spreadsheet">
|
||||||
|
<form @submit.prevent="[selectedSheetIndex = $refs.selectWorksheet.value, buildDefaultMappings()]">
|
||||||
|
<label class="label">
|
||||||
|
Select Worksheet
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
ref="selectWorksheet"
|
||||||
|
class="base-select"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="(sheet, index) in spreadsheet"
|
||||||
|
:key="index"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
{{ sheet[0] || index }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button class="base-button mt-4 w-full">
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="selectedSheetIndex !== null">
|
||||||
|
<div
|
||||||
|
v-for="submitter in submitters"
|
||||||
|
:key="submitter.uuid"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="submitters.length > 1"
|
||||||
|
class="px-3 border-y py-2 border-base-300 text-center w-full"
|
||||||
|
>
|
||||||
|
{{ submitter.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="relative w-full py-2 px-2 text-sm">
|
||||||
|
Recipient field
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full py-2 pl-4 text-sm">
|
||||||
|
Spreadsheet column
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="mapping in mappings.filter((m) => m.submitter_uuid === submitter.uuid)"
|
||||||
|
:key="mapping.uuid"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<select
|
||||||
|
class="base-select !select-sm !h-10"
|
||||||
|
required
|
||||||
|
@change="mapping.field_name = $event.target.value"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
disabled
|
||||||
|
value=""
|
||||||
|
:selected="!mapping.field_name"
|
||||||
|
>
|
||||||
|
Select Field
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(field, index) in selectFieldsForSubmitter(submitter)"
|
||||||
|
:key="index"
|
||||||
|
:value="field.name"
|
||||||
|
:selected="mapping.field_name === field.name"
|
||||||
|
>
|
||||||
|
{{ field.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="flex items-center px-1">
|
||||||
|
<IconArrowsHorizontal style="width: 19px; height: 19px" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full relative">
|
||||||
|
<select
|
||||||
|
class="base-select !select-sm !h-10"
|
||||||
|
required
|
||||||
|
@change="mapping.column_index = parseInt($event.target.value)"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
disabled
|
||||||
|
value=""
|
||||||
|
:selected="mapping.column_index == null"
|
||||||
|
>
|
||||||
|
Select Column
|
||||||
|
</option>
|
||||||
|
<template
|
||||||
|
v-for="(column, index) in columns"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-if="column"
|
||||||
|
:value="index"
|
||||||
|
:selected="index === mapping.column_index"
|
||||||
|
>
|
||||||
|
{{ column }}
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<div
|
||||||
|
v-if="mapping.column_index != null"
|
||||||
|
class="absolute top-0 bottom-0 right-1 flex items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-bottom-end pr-1 tooltip-pre"
|
||||||
|
style="padding-top: 2px"
|
||||||
|
:data-tip="[0, 1, 2].map((i) => rows[i]?.[mapping.column_index] ?? '---').join('\n')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-xs btn-circle bg-white border-0 border-gray-300"
|
||||||
|
@click.prevent
|
||||||
|
>
|
||||||
|
<IconInfoCircle class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center pl-1">
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-top"
|
||||||
|
data-tip="Remove"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
:disabled="mappings.filter((m) => m.submitter_uuid === submitter.uuid).length < 2"
|
||||||
|
class="btn btn-xs btn-circle"
|
||||||
|
@click.prevent="mappings.splice(mappings.indexOf(mapping), 1)"
|
||||||
|
>
|
||||||
|
<IconX class="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary w-full !normal-case font-medium"
|
||||||
|
@click.prevent="addMapping(submitter)"
|
||||||
|
>
|
||||||
|
<IconPlus class="w-4 h-4" />
|
||||||
|
New Field Mapping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
name="submissions_json"
|
||||||
|
hidden
|
||||||
|
:value="JSON.stringify(submissionsData.slice(0, 1100))"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="px-3 border-y py-2 border-base-300 text-center w-full text-sm font-semibold"
|
||||||
|
>
|
||||||
|
Total entries: {{ submissionsData.length }}
|
||||||
|
<template v-if="submissionsData.length >= 1000">
|
||||||
|
/ 1000
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-52 w-full"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="onDropFiles"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="w-full relative bg-base-200/20 hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
|
||||||
|
for="import_list_file"
|
||||||
|
:class="{ 'opacity-50': isLoading }"
|
||||||
|
>
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isLoading"
|
||||||
|
class="animate-spin"
|
||||||
|
:width="40"
|
||||||
|
:height="40"
|
||||||
|
/>
|
||||||
|
<IconCloudUpload
|
||||||
|
v-else
|
||||||
|
:width="40"
|
||||||
|
:height="40"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="font-medium text-lg mb-1"
|
||||||
|
>
|
||||||
|
Upload CSV or XLSX Spreadsheet
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<span class="font-medium">Click to Upload</span> or drag and drop files.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
ref="form"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="import_list_file"
|
||||||
|
ref="input"
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
accept=".xlsx, .xls, .csv"
|
||||||
|
@change="onSelectFile"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
Or <a
|
||||||
|
:download="`${template.name}.csv`"
|
||||||
|
:href="`data:text/csv;base64,${csvBase64}`"
|
||||||
|
class="link font-medium"
|
||||||
|
>download</a> a spreadsheet to fill and import
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCloudUpload, IconX, IconPlus, IconArrowsHorizontal, IconInfoCircle, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FileDropzone',
|
||||||
|
components: {
|
||||||
|
IconCloudUpload,
|
||||||
|
IconX,
|
||||||
|
IconArrowsHorizontal,
|
||||||
|
IconPlus,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconInnerShadowTop
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
spreadsheet: null,
|
||||||
|
selectedSheetIndex: null,
|
||||||
|
mappings: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
table () {
|
||||||
|
return this.spreadsheet[this.selectedSheetIndex][1]
|
||||||
|
},
|
||||||
|
submissionsData () {
|
||||||
|
const submissions = []
|
||||||
|
|
||||||
|
this.rows.forEach((row) => {
|
||||||
|
const submittersIndex = {}
|
||||||
|
|
||||||
|
this.mappings.forEach((mapping) => {
|
||||||
|
if (mapping.field_name && mapping.column_index != null) {
|
||||||
|
submittersIndex[mapping.submitter_uuid] ||= { uuid: mapping.submitter_uuid, fields: [] }
|
||||||
|
|
||||||
|
if (['name', 'email', 'phone'].includes(mapping.field_name.toLowerCase())) {
|
||||||
|
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
|
||||||
|
} else {
|
||||||
|
submittersIndex[mapping.submitter_uuid].fields.push({
|
||||||
|
name: mapping.field_name, default_value: row[mapping.column_index], readonly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Object.keys(submittersIndex).length !== 0) {
|
||||||
|
submissions.push({ submitters: Object.values(submittersIndex) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return submissions
|
||||||
|
},
|
||||||
|
csvBase64 () {
|
||||||
|
const rows = []
|
||||||
|
|
||||||
|
this.submitters.forEach((submitter) => {
|
||||||
|
this.selectFieldsForSubmitter(submitter).forEach((field) => {
|
||||||
|
rows.push(this.submitters.length > 1 ? `${submitter.name} - ${field.name}` : field.name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const csv = rows.map(item => {
|
||||||
|
if (/[",\n]/.test(item)) {
|
||||||
|
return `"${item.replace(/"/g, '""')}"`
|
||||||
|
} else {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}).join(',')
|
||||||
|
|
||||||
|
return window.btoa(window.unescape(window.encodeURIComponent(csv + '\n' + rows.map(() => '').join(',') + '\n')))
|
||||||
|
},
|
||||||
|
submitters () {
|
||||||
|
return this.template.submitters
|
||||||
|
},
|
||||||
|
columns () {
|
||||||
|
return this.table[0]
|
||||||
|
},
|
||||||
|
form () {
|
||||||
|
return this.$el.closest('form')
|
||||||
|
},
|
||||||
|
fieldTypes () {
|
||||||
|
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox']
|
||||||
|
},
|
||||||
|
defaultFields () {
|
||||||
|
return [
|
||||||
|
{ name: 'Name' },
|
||||||
|
{ name: 'Email' },
|
||||||
|
{ name: 'Phone' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rows () {
|
||||||
|
return this.table.slice(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedSheetIndex (value) {
|
||||||
|
if (value !== null) {
|
||||||
|
document.getElementById('list_form_buttons')?.classList?.remove('hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDropFiles (e) {
|
||||||
|
this.uploadFile(e.dataTransfer.files[0])
|
||||||
|
},
|
||||||
|
onSelectFile (e) {
|
||||||
|
this.uploadFile(e.target.files[0])
|
||||||
|
},
|
||||||
|
addMapping (submitter) {
|
||||||
|
this.mappings.push({ uuid: v4(), field_name: '', column_index: null, submitter_uuid: submitter.uuid })
|
||||||
|
},
|
||||||
|
selectFieldsForSubmitter (submitter) {
|
||||||
|
const templateFields = this.template.fields.filter((field) => {
|
||||||
|
return field.submitter_uuid === submitter.uuid &&
|
||||||
|
field.name &&
|
||||||
|
this.fieldTypes.includes(field.type) &&
|
||||||
|
this.defaultFields.every((f) => field.name?.toLowerCase() !== f.name?.toLowerCase())
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...this.defaultFields, ...templateFields]
|
||||||
|
},
|
||||||
|
buildDefaultMappings () {
|
||||||
|
this.submitters.forEach((submitter) => {
|
||||||
|
const fields = this.selectFieldsForSubmitter(submitter)
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const columnIndex = this.columns.findIndex((column, index) => {
|
||||||
|
return column &&
|
||||||
|
column.toString().toLowerCase().includes(field.name?.toLowerCase()) &&
|
||||||
|
this.mappings.every((m) => m.column_index !== index)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (columnIndex !== -1) {
|
||||||
|
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.mappings.some((m) => m.field_name.toLowerCase() === 'name' && m.submitter_uuid === submitter.uuid)) {
|
||||||
|
this.mappings.unshift({ uuid: v4(), field_name: 'Name', submitter_uuid: submitter.uuid })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.mappings.some((m) => m.field_name.toLowerCase() === 'email' && m.submitter_uuid === submitter.uuid)) {
|
||||||
|
this.mappings.unshift({ uuid: v4(), field_name: 'Email', submitter_uuid: submitter.uuid })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
uploadFile (file) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
return fetch('/upload_spreadsheet', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': this.authenticityToken
|
||||||
|
}
|
||||||
|
}).then(resp => resp.json()).then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
return alert(data.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spreadsheet = data
|
||||||
|
|
||||||
|
if (data.length === 1) {
|
||||||
|
this.selectedSheetIndex = 0
|
||||||
|
|
||||||
|
this.buildDefaultMappings()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -77,6 +77,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
allowDraw: {
|
allowDraw: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -123,7 +128,9 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
defaultFieldType () {
|
defaultFieldType () {
|
||||||
if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
|
if (this.drawFieldType) {
|
||||||
|
return this.drawFieldType
|
||||||
|
} else if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
|
||||||
return this.defaultDrawFieldType
|
return this.defaultDrawFieldType
|
||||||
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
|
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
|
||||||
return this.fieldTypes[0]
|
return this.fieldTypes[0]
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandStripe
|
IconBrandStripe
|
||||||
},
|
},
|
||||||
inject: ['backgroundColor', 'save', 'currencies', 't'],
|
inject: ['backgroundColor', 'save', 'currencies', 't', 'isPaymentConnected'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -221,6 +221,8 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.field.preferences.currency ||= this.defaultCurrency
|
this.field.preferences.currency ||= this.defaultCurrency
|
||||||
|
|
||||||
|
isConnected.value ||= this.isPaymentConnected
|
||||||
|
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
this.checkStatus()
|
this.checkStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
v-if="withReplaceButton"
|
v-if="withReplaceButton"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
class="opacity-0 group-hover:opacity-100"
|
class="opacity-0 group-hover:opacity-100"
|
||||||
@@ -115,11 +114,6 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -132,11 +126,6 @@ export default {
|
|||||||
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
|
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onUpdateName (value) {
|
onUpdateName (value) {
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
},
|
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
@@ -64,11 +59,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload
|
upload: Upload.methods.upload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
},
|
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
@@ -79,112 +74,43 @@ export default {
|
|||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
async upload () {
|
async upload () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
method: 'POST',
|
||||||
|
body: new FormData(this.$refs.form)
|
||||||
|
}).then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
resp.json().then((data) => {
|
||||||
|
this.$emit('success', data)
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
})
|
||||||
|
} else if (resp.status === 422) {
|
||||||
|
resp.json().then((data) => {
|
||||||
|
if (data.error === 'PDF encrypted') {
|
||||||
|
const formData = new FormData(this.$refs.form)
|
||||||
|
|
||||||
const blobs = await Promise.all(
|
formData.append('password', prompt('Enter PDF password'))
|
||||||
Array.from(this.$refs.input.files).map(async (file) => {
|
|
||||||
const upload = new DirectUpload(
|
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.$refs.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||||
upload.create((error, blob) => {
|
method: 'POST',
|
||||||
if (error) {
|
body: formData
|
||||||
console.error(error)
|
}).then(async (resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
return reject(error)
|
this.$emit('success', await resp.json())
|
||||||
|
this.$refs.input.value = ''
|
||||||
} else {
|
} else {
|
||||||
return resolve(blob)
|
alert('Wrong password')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).catch((error) => {
|
}
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
).finally(() => {
|
}
|
||||||
this.isLoading = false
|
}).finally(() => {
|
||||||
})
|
this.isLoading = false
|
||||||
|
})
|
||||||
this.isProcessing = true
|
|
||||||
|
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ blobs }),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then((resp) => {
|
|
||||||
if (resp.ok) {
|
|
||||||
resp.json().then((data) => {
|
|
||||||
this.$emit('success', data)
|
|
||||||
this.$refs.input.value = ''
|
|
||||||
})
|
|
||||||
} else if (resp.status === 422) {
|
|
||||||
resp.json().then((data) => {
|
|
||||||
if (data.error === 'PDF encrypted') {
|
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then(async (resp) => {
|
|
||||||
if (resp.ok) {
|
|
||||||
this.$emit('success', await resp.json())
|
|
||||||
this.$refs.input.value = ''
|
|
||||||
} else {
|
|
||||||
alert('Wrong password')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
this.isProcessing = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: new FormData(this.$refs.form)
|
|
||||||
}).then((resp) => {
|
|
||||||
if (resp.ok) {
|
|
||||||
resp.json().then((data) => {
|
|
||||||
this.$emit('success', data)
|
|
||||||
this.$refs.input.value = ''
|
|
||||||
})
|
|
||||||
} else if (resp.status === 422) {
|
|
||||||
resp.json().then((data) => {
|
|
||||||
if (data.error === 'PDF encrypted') {
|
|
||||||
const formData = new FormData(this.$refs.form)
|
|
||||||
|
|
||||||
formData.append('password', prompt('Enter PDF password'))
|
|
||||||
|
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
}).then(async (resp) => {
|
|
||||||
if (resp.ok) {
|
|
||||||
this.$emit('success', await resp.json())
|
|
||||||
this.$refs.input.value = ''
|
|
||||||
} else {
|
|
||||||
alert('Wrong password')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,15 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
user = submission.created_by_user || submitter.template.author
|
user = submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
|
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
|
||||||
if submitter.submission.submitters.map(&:email).exclude?(user.email) &&
|
if submission.submitters.map(&:email).exclude?(user.email) &&
|
||||||
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
|
||||||
|
user.role != 'integration'
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
bcc = submission.preferences['bcc_completed'].presence ||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
submission.account.account_configs
|
submission.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
||||||
|
|
||||||
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
||||||
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
|
if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account)
|
||||||
|
Rollbar.error("Skip email: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
mail = SubmitterMailer.invitation_email(submitter)
|
mail = SubmitterMailer.invitation_email(submitter)
|
||||||
|
|
||||||
Submitters::ValidateSending.call(submitter, mail)
|
Submitters::ValidateSending.call(submitter, mail)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
|
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
|
||||||
|
|
||||||
def invitation_email(submitter)
|
def invitation_email(submitter)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
|
|
||||||
if submitter.preferences['email_message_uuid']
|
if submitter.preferences['email_message_uuid']
|
||||||
@@ -30,12 +30,13 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
to: @submitter.friendly_name,
|
to: @submitter.friendly_name,
|
||||||
from: from_address_for_submitter(submitter),
|
from: from_address_for_submitter(submitter),
|
||||||
subject:,
|
subject:,
|
||||||
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
reply_to: submitter.preferences['reply_to'].presence ||
|
||||||
|
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user, to: nil)
|
def completed_email(submitter, user, to: nil)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@submission = submitter.submission
|
@submission = submitter.submission
|
||||||
@user = user
|
@user = user
|
||||||
@@ -63,7 +64,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def documents_copy_email(submitter, to: nil, sig: false)
|
def documents_copy_email(submitter, to: nil, sig: false)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
|
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
|
||||||
|
|
||||||
@@ -84,6 +85,9 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: to || @submitter.friendly_name,
|
to: to || @submitter.friendly_name,
|
||||||
|
reply_to: @submitter.preferences['reply_to'].presence ||
|
||||||
|
(@submitter.submission.created_by_user ||
|
||||||
|
@submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@'),
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,17 @@
|
|||||||
# locale :string not null
|
# locale :string not null
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# timezone :string not null
|
# timezone :string not null
|
||||||
|
# uuid :string not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_accounts_on_uuid (uuid) UNIQUE
|
||||||
|
#
|
||||||
class Account < ApplicationRecord
|
class Account < ApplicationRecord
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :account_configs, dependent: :destroy
|
has_many :account_configs, dependent: :destroy
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
SUBMITTER_REMAILERS = 'submitter_reminders'
|
SUBMITTER_REMAILERS = 'submitter_reminders'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
|
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
||||||
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
||||||
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<form action="<%= root_path %>" method="get" class="bg-base-200 px-1.5 rounded-xl py-1">
|
||||||
|
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="Templates">
|
||||||
|
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
|
||||||
|
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
</button>
|
||||||
|
</toggle-cookies>
|
||||||
|
<toggle-cookies data-value="submissions" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="Submissions">
|
||||||
|
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
|
||||||
|
<%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
</button>
|
||||||
|
</toggle-cookies>
|
||||||
|
</form>
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
|
||||||
<div class="flex justify-between mb-4 items-center">
|
|
||||||
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
|
|
||||||
<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) %>
|
|
||||||
<%= render 'templates/upload_button' %>
|
|
||||||
<%= 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">Create</span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% view_archived_html = capture do %>
|
|
||||||
<% if current_account.templates.where.not(archived_at: nil).exists? %>
|
|
||||||
<div>
|
|
||||||
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
|
|
||||||
</div>
|
|
||||||
<% 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 %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if 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)) %>
|
|
||||||
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
|
|
||||||
<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">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
|
|
||||||
<span>
|
|
||||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
|
||||||
</span>
|
|
||||||
<div class="font-medium mb-1">
|
|
||||||
Upload New Document
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
|
|
||||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
|
||||||
<div class="font-medium mb-1">
|
|
||||||
Uploading...
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</file-dropzone>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if @templates.present? || params[:q].blank? %>
|
|
||||||
<% if @pagy.pages > 1 %>
|
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
|
|
||||||
<% else %>
|
|
||||||
<div class="mt-2">
|
|
||||||
<%= view_archived_html %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% elsif params[:q].present? %>
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="mt-16 text-3xl font-semibold">
|
|
||||||
Templates not Found
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
Analyzing...
|
Analyzing...
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
<file-dropzone data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||||
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
||||||
<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">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
|
|||||||
@@ -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="M5 12l14 0" />
|
||||||
|
<path d="M15 16l4 -4" />
|
||||||
|
<path d="M15 8l4 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||||
|
<path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||||
|
<path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||||
|
<path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<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="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
|
||||||
|
<path d="M4 14m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
Whitespace-only changes.
@@ -16,7 +16,6 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'email_stats' %>
|
|
||||||
<%= render 'bcc_form', config: @bcc_config %>
|
<%= render 'bcc_form', config: @bcc_config %>
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
<h2 class="text-3xl font-bold">Sign Request Email Reminders</h2>
|
<h2 class="text-3xl font-bold">Sign Request Email Reminders</h2>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
<div>
|
||||||
|
Completed Form Message
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<%= f.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :title, 'Title', class: 'label' %>
|
||||||
|
<%= ff.text_field :title, class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :body, 'Body', class: 'label' %>
|
||||||
|
<autoresize-textarea>
|
||||||
|
<%= ff.text_area :body, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||||
|
</autoresize-textarea>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||||
<%= render 'logo_form' %>
|
<%= render 'logo_form' %>
|
||||||
<p class="text-4xl font-bold mb-4 mt-8">Submission Form</p>
|
<p class="text-4xl font-bold mb-4 mt-8">Submission Form</p>
|
||||||
<%= render 'form_completed_button_form' %>
|
<div class="space-y-4">
|
||||||
|
<%= render 'form_completed_message_form' %>
|
||||||
|
<%= render 'form_completed_button_form' %>
|
||||||
|
</div>
|
||||||
<%= render 'form_customization_settings' %>
|
<%= render 'form_customization_settings' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
|
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
|
||||||
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
|
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get field() {
|
get field() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>">
|
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
|
||||||
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
||||||
<span class="enabled">
|
<span class="enabled">
|
||||||
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
|||||||
@@ -39,16 +39,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:read, User) %>
|
<% if can?(:read, User) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Users', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% unless Docuseal.multitenant? %>
|
|
||||||
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
|
|
||||||
<li>
|
|
||||||
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||||
<% if can?(:read, AccessToken) %>
|
<% if can?(:read, AccessToken) %>
|
||||||
<li>
|
<li>
|
||||||
@@ -88,6 +81,11 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||||
<li>
|
<li>
|
||||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %>
|
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div dir="auto" class="text-center text-2xl font-semibold">
|
<div dir="auto" class="text-center text-2xl font-semibold">
|
||||||
<%= t('form_has_been_submitted_already') %>
|
<%= t('form_has_been_submitted_already') %>
|
||||||
</div>
|
</div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.account) %>
|
<% if Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account) %>
|
||||||
<div>
|
<div>
|
||||||
<%= 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 }, form: { onsubmit: 'event.submitter.disabled = true' }, 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: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="alert">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Bulk send from Excel XLSX or CSV</p>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Unlock with DocuSeal Pro
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>" data-turbo="false">Learn More</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% if template.submitters.size == 1 %>
|
<% if template.submitters.size == 1 %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 50 : 1) : nil %>">
|
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
|
||||||
<submitters-autocomplete data-field="email" class="block relative">
|
<submitters-autocomplete data-field="email" class="block relative">
|
||||||
<autoresize-textarea>
|
<autoresize-textarea>
|
||||||
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'submissions/bulk_send_placeholder' %>
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !is_smtp_configured, checked: is_smtp_configured %>
|
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, checked: can_send_emails %>
|
||||||
<span class="label">Send emails</span>
|
<span class="label">Send emails</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<% if can_send_emails %>
|
||||||
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
|
<label>
|
||||||
<span class="link peer-checked:hidden">Edit message</span>
|
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
|
||||||
</label>
|
<span class="link peer-checked:hidden">Edit message</span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
<%= render 'email_stats' %>
|
<%= render 'email_stats' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% unless is_smtp_configured %>
|
<% if !Docuseal.multitenant? && !can_send_emails %>
|
||||||
<div class="alert my-4">
|
<div class="alert my-4">
|
||||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
||||||
<% option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] } %>
|
<% option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] } %>
|
||||||
<% if option && Array.wrap(value).include?(option['value']) %>
|
<% option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}" %>
|
||||||
|
<% if option && Array.wrap(value).include?(option_name) %>
|
||||||
<div class="w-full flex items-center justify-center">
|
<div class="w-full flex items-center justify-center">
|
||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,9 +33,10 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'date' && value != '{{date}}' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex items-center px-0.5">
|
||||||
|
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
|
||||||
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
|
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %>
|
<%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %>
|
||||||
<% options = [['via Email', 'email'], ['via Phone', 'phone'], %w[Detailed detailed], (Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 ? ['via Link', 'link'] : nil)].compact %>
|
<% options = [['via Email', 'email'], ['via Phone', 'phone'], %w[Detailed detailed], ['Upload List', 'list']].compact %>
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<% options.each_with_index do |(label, value), index| %>
|
<% options.each_with_index do |(label, value), index| %>
|
||||||
@@ -22,11 +22,9 @@
|
|||||||
<div id="detailed" class="hidden">
|
<div id="detailed" class="hidden">
|
||||||
<%= render 'detailed_form', template: @template %>
|
<%= render 'detailed_form', template: @template %>
|
||||||
</div>
|
</div>
|
||||||
<% if Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 %>
|
<div id="list" class="hidden">
|
||||||
<div id="link" class="hidden">
|
<%= render 'list_form', template: @template %>
|
||||||
<%= render 'link_form', template: @template %>
|
</div>
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<%= content_for(:modal_extra) %>
|
<%= content_for(:modal_extra) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
<div style="max-width: 1600px" class="mx-auto pl-4">
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
||||||
<a href="<%= signed_in? && @submission.template.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
<a href="<%= signed_in? && @submission.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
||||||
<span><%= render 'submissions/logo' %></span>
|
<span><%= render 'submissions/logo' %></span>
|
||||||
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><%= @submission.template.name %></span>
|
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% @submission.template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
|
||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<% if @submission.audit_trail.present? %>
|
<% if @submission.audit_trail.present? %>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']] %>
|
<% value = values[field['uuid']] %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.template.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,10 +112,10 @@
|
|||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||||
<span>
|
<span>
|
||||||
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.template.account.timezone), format: :long, locale: @submission.template.account.locale) : 'Not completed yet' %>
|
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : 'Not completed yet' %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) %>
|
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) && Accounts.can_send_emails?(current_account) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<%= svg_icon('check', class: 'w-6 h-6') %>
|
<%= svg_icon('check', class: 'w-6 h-6') %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.template.account.locale) %>
|
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= Array.wrap(value).join(', ') %>
|
<%= Array.wrap(value).join(', ') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<div>
|
||||||
|
<%= link_to root_path do %>
|
||||||
|
←
|
||||||
|
<span>Back to Active</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-4 items-center">
|
||||||
|
<h1 class="text-4xl font-bold">Submissions <span class="badge badge-outline badge-lg align-middle">Archived</span></h1>
|
||||||
|
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||||
|
<%= render 'shared/search_input', placeholder: 'Search...' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% if @pagy.count > 0 %>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
|
||||||
|
</div>
|
||||||
|
<% elsif params[:q].present? %>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mt-16 text-3xl font-semibold">
|
||||||
|
Submissions not Found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions' %>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
|
||||||
|
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||||
|
<div class="flex justify-between mb-4 items-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-2">
|
||||||
|
<%= render 'dashboard/toggle_view', selected: 'submissions' %>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold">Submissions</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||||
|
<%= 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">Create</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% view_archived_html = capture do %>
|
||||||
|
<% if current_account.submissions.where.not(archived_at: nil).exists? %>
|
||||||
|
<div>
|
||||||
|
<a href="<%= submissions_archived_index_path %>" class="link text-sm">View Archived</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if is_show_tabs %>
|
||||||
|
<div class="flex items-center md:items-end flex-col space-y-2 md:space-y-0 md:flex-row md:space-x-2 mb-4">
|
||||||
|
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? '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-600">
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||||
|
<span class="font-normal">All</span>
|
||||||
|
</div>
|
||||||
|
</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-600">
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||||
|
<span class="font-normal">Pending</span>
|
||||||
|
</div>
|
||||||
|
</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-600">
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||||
|
<span class="font-normal">Completed</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if @pagy.count > 0 %>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if params[:q].blank? && @pagy.count < 5 %>
|
||||||
|
<%= render 'templates/dropzone' %>
|
||||||
|
<% end %>
|
||||||
|
<% if @submissions.present? || params[:q].blank? %>
|
||||||
|
<% if @pagy.pages > 1 %>
|
||||||
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %>
|
||||||
|
<% else %>
|
||||||
|
<div class="mt-2">
|
||||||
|
<%= view_archived_html %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% elsif params[:q].present? %>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mt-16 text-3xl font-semibold">
|
||||||
|
Submissions not Found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if Accounts.can_send_emails?(@submission.account) %>
|
<% if Docuseal.multitenant? || Accounts.can_send_emails?(@submission.account) %>
|
||||||
<%= form_for '', url: send_submission_email_index_path, method: :post, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
|
<%= form_for '', url: send_submission_email_index_path, method: :post, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
|
||||||
<div dir="auto" class="form-control !mt-0">
|
<div dir="auto" class="form-control !mt-0">
|
||||||
<%= f.hidden_field :submission_slug, value: @submission.slug %>
|
<%= f.hidden_field :submission_slug, value: @submission.slug %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
||||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||||
<% configs = Submitters::FormConfigs.call(submitter) %>
|
<% configs = Submitters::FormConfigs.call(submitter) %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.account) && @submitter.email.present? %>
|
<% if (Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account)) && @submitter.email.present? %>
|
||||||
<%= 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 }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-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: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
<% if @submitter.submission.template.submitters.size != 1 %>
|
<% if @submitter.submission.template.submitters.size != 1 %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
|
|||||||
@@ -21,11 +21,12 @@
|
|||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
|
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
|
||||||
<% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])))) %>
|
<%= auto_link(simple_format(MarkdownToHtml.call(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig]))))) %>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
|
||||||
|
<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">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
|
||||||
|
<span>
|
||||||
|
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||||
|
</span>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Upload New Document
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="font-medium">Click to upload</span> or drag and drop
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
|
||||||
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Uploading...
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</file-dropzone>
|
||||||
|
<% end %>
|
||||||
@@ -1,131 +1,55 @@
|
|||||||
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
|
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
|
||||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-5 md:px-6 py-5 md:items-center">
|
<div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row items-strech">
|
||||||
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
<% if local_assigns[:with_template] %>
|
||||||
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
|
<% template = submission.template %>
|
||||||
<% if submitters.size == 1 %>
|
<a href="<%= template_path(template) %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0">
|
||||||
<div>
|
<div>
|
||||||
<% submitter = submitters.first %>
|
<div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1">
|
||||||
<div class="flex items-center space-x-4">
|
<%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %>
|
||||||
<span class="flex flex-col md:flex-row md:items-center gap-3">
|
<span style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<%= template.name %>
|
||||||
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
|
<%= svg_icon('arrow_right', class: 'w-4 h-4 sm:inline group-hover:visible invisible hidden') %>
|
||||||
<%= submitter.status %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-lg break-all flex items-center">
|
|
||||||
<%= submitter.name || submitter.email || submitter.phone %>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-2 items-center">
|
|
||||||
<% if submitter.completed_at? %>
|
|
||||||
<form onsubmit="event.preventDefault()" class="flex-1 md:flex-none">
|
|
||||||
<button onclick="event.stopPropagation()" class="w-full md:w-fit">
|
|
||||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
|
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
|
||||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
|
||||||
<span class="inline">Download</span>
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
|
||||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
|
||||||
<span class="inline">Downloa...</span>
|
|
||||||
</span>
|
|
||||||
</download-button>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% else %>
|
|
||||||
<% if current_user.email == submitter.email %>
|
|
||||||
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
|
|
||||||
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
|
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
|
||||||
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
|
||||||
<span class="inline shrink-0">Sign now</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% else %>
|
|
||||||
<div class="flex-1 md:flex-none">
|
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<div class="flex-1 md:flex-none">
|
|
||||||
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
|
|
||||||
</div>
|
|
||||||
<% if !submission.archived_at? && can?(:destroy, submission) %>
|
|
||||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<div class="space-y-1 w-full md:mr-2">
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center gap-3">
|
|
||||||
<% if is_submission_completed %>
|
|
||||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
|
||||||
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
|
||||||
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
|
||||||
<%= latest_submitter.status %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0">
|
|
||||||
<% submitters.each_with_index do |submitter, index| %>
|
|
||||||
<div class="relative flex justify-between items-start md:items-center space-x-3">
|
|
||||||
<span class="flex flex-col md:flex-row md:items-center gap-2">
|
|
||||||
<% unless is_submission_completed %>
|
|
||||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
|
||||||
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
|
||||||
<%= submitter.status %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<span class="text-lg break-all">
|
|
||||||
<%= submitter.name || submitter.email || submitter.phone %>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<% if submitter.completed_at? && !is_submission_completed %>
|
|
||||||
<form onsubmit="event.preventDefault()">
|
|
||||||
<button onclick="event.stopPropagation()">
|
|
||||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36">
|
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
|
||||||
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
|
|
||||||
<span class="inline">Download</span>
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
|
||||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
|
|
||||||
<span class="inline">Downloa...</span>
|
|
||||||
</span>
|
|
||||||
</download-button>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% elsif !is_submission_completed %>
|
|
||||||
<div class="relative flex items-center space-x-3">
|
|
||||||
<% if current_user.email == submitter.email %>
|
|
||||||
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
|
|
||||||
<button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36">
|
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
|
||||||
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
|
||||||
<span class="inline shrink-0">Sign now</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% else %>
|
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy Link', copied_title_md: 'Copied' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex-shrink-0 sm:mt-2">
|
||||||
<div class="flex space-x-1 md:space-x-2 items-center">
|
<div class="flex text-xs flex-col text-base-content/60 space-y-0.5">
|
||||||
<% if is_submission_completed %>
|
<span class="flex items-center space-x-1">
|
||||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
<%= svg_icon('user', class: 'w-4 h-4 flex-shrink-0') %>
|
||||||
<div class="flex-1 md:flex-none">
|
<span><%= (submission.created_by_user || template.author).full_name.presence || (submission.created_by_user || template.author).email.to_s.sub(/\+\w+@/, '@') %></span>
|
||||||
<form onsubmit="event.preventDefault()" class="w-full md:w-fit">
|
</span>
|
||||||
<button onclick="event.stopPropagation()">
|
<span class="flex items-center space-x-1">
|
||||||
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
|
<%= svg_icon('calendar', class: 'w-4 h-4 flex-shrink-0') %>
|
||||||
|
<span><%= l(submission.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<a href="<%= submission_path(submission) %>" class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5">
|
||||||
|
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||||
|
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
|
||||||
|
<% if submitters.size == 1 %>
|
||||||
|
<div>
|
||||||
|
<% submitter = submitters.first %>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex flex-col md:flex-row md:items-center gap-3">
|
||||||
|
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
|
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
|
||||||
|
<%= submitter.status %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg break-all flex items-center">
|
||||||
|
<%= submitter.name || submitter.email || submitter.phone %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2 items-center">
|
||||||
|
<% if submitter.completed_at? %>
|
||||||
|
<form onsubmit="event.preventDefault()" class="flex-1 md:flex-none">
|
||||||
|
<button onclick="event.stopPropagation()" class="w-full md:w-fit">
|
||||||
|
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||||
<span class="inline">Download</span>
|
<span class="inline">Download</span>
|
||||||
@@ -137,14 +61,122 @@
|
|||||||
</download-button>
|
</download-button>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<% if current_user.email == submitter.email %>
|
||||||
|
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
|
||||||
|
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
|
||||||
|
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="inline shrink-0">Sign now</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<div class="flex-1 md:flex-none">
|
||||||
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex-1 md:flex-none">
|
||||||
|
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% if !submission.archived_at? && can?(:destroy, submission) %>
|
||||||
<div class="flex-1 md:flex-none">
|
<span data-tip="Archive" class="sm:tooltip tooltip-top">
|
||||||
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
|
<%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% unless submission.archived_at? %>
|
<% else %>
|
||||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
<div class="space-y-1 w-full md:mr-2">
|
||||||
<% end %>
|
<div class="flex flex-col md:flex-row md:items-center gap-3">
|
||||||
</div>
|
<% if is_submission_completed %>
|
||||||
<% end %>
|
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
</a>
|
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
|
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||||
|
<%= latest_submitter.status %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0">
|
||||||
|
<% submitters.each_with_index do |submitter, index| %>
|
||||||
|
<div class="relative flex justify-between items-start md:items-center space-x-3">
|
||||||
|
<span class="flex flex-col md:flex-row md:items-center gap-2">
|
||||||
|
<% unless is_submission_completed %>
|
||||||
|
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
|
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
||||||
|
<%= submitter.status %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<span class="text-lg break-all">
|
||||||
|
<%= submitter.name || submitter.email || submitter.phone %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% if submitter.completed_at? && !is_submission_completed %>
|
||||||
|
<form onsubmit="event.preventDefault()">
|
||||||
|
<button onclick="event.stopPropagation()">
|
||||||
|
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36">
|
||||||
|
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
||||||
|
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="inline">Download</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
|
||||||
|
<span class="inline">Downloa...</span>
|
||||||
|
</span>
|
||||||
|
</download-button>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% elsif !is_submission_completed %>
|
||||||
|
<div class="relative flex items-center space-x-3">
|
||||||
|
<% if current_user.email == submitter.email %>
|
||||||
|
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
|
||||||
|
<button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36">
|
||||||
|
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||||
|
<span class="inline shrink-0">Sign now</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy Link', copied_title_md: 'Copied' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-1 md:space-x-2 items-center">
|
||||||
|
<% if is_submission_completed %>
|
||||||
|
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
|
<div class="flex-1 md:flex-none">
|
||||||
|
<form onsubmit="event.preventDefault()" class="w-full md:w-fit">
|
||||||
|
<button onclick="event.stopPropagation()">
|
||||||
|
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
|
||||||
|
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
||||||
|
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||||
|
<span class="inline">Download</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
|
<span class="inline">Downloa...</span>
|
||||||
|
</span>
|
||||||
|
</download-button>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex-1 md:flex-none">
|
||||||
|
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
|
||||||
|
</div>
|
||||||
|
<% unless submission.archived_at? %>
|
||||||
|
<span data-tip="Archive" class="sm:tooltip tooltip-top">
|
||||||
|
<%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="relative flex items-start justify-between w-full space-x-0">
|
<div class="relative flex items-start justify-between w-full space-x-0">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
<h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||||
<%= template.name %>
|
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
|
||||||
<% if template.archived_at? %>
|
<% if template.archived_at? %>
|
||||||
<span class="badge badge-outline badge-lg align-middle">Archived</span>
|
<span class="badge badge-outline badge-lg align-middle">Archived</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
|
<template-builder class="grid" data-template="<%= @template_data %>"></template-builder>
|
||||||
@@ -3,6 +3,11 @@
|
|||||||
<% if @base_template %>
|
<% if @base_template %>
|
||||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists? %>
|
||||||
|
<div class="form-control -mb-2 mt-2">
|
||||||
|
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
|
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,11 +22,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</folder-autocomplete>
|
</folder-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists?(account_type: :linked) %>
|
|
||||||
<div class="form-control mb-4">
|
|
||||||
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.where.not(id: true_user.account.testing_accounts)].map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<%= render 'title', template: @template %>
|
<%= render 'title', template: @template %>
|
||||||
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
|
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
|
||||||
<% if !@pagy.count.zero? || params[:q].present? %>
|
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? %>
|
||||||
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
|
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
|
||||||
<div class="flex justify-between items-center md:items-end">
|
<div class="flex justify-between items-center md:items-end">
|
||||||
<p class="text-3xl font-bold">Submissions</p>
|
<p class="text-3xl font-bold">Submissions</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="flex justify-between mb-4 items-center">
|
<div class="flex justify-between mb-4 items-center">
|
||||||
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates <span class="badge badge-outline badge-lg align-middle">Archived</span></h1>
|
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates <span class="badge badge-outline badge-lg align-middle">Archived</span></h1>
|
||||||
<% if params[:q].present? || @pagy.pages > 1 %>
|
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||||
<%= render 'shared/search_input' %>
|
<%= render 'shared/search_input', placeholder: 'Search...' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if @pagy.count > 0 %>
|
<% if @pagy.count > 0 %>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<% has_archived = current_account.templates.where.not(archived_at: nil).exists? %>
|
||||||
|
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||||
|
<div class="flex justify-between mb-4 items-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<% if has_archived || @pagy.count > 0 %>
|
||||||
|
<div class="mr-2">
|
||||||
|
<%= render 'dashboard/toggle_view', selected: 'templates' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</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">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">View Archived</a>
|
||||||
|
</div>
|
||||||
|
<% 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 %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if 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)) %>
|
||||||
|
<%= render 'templates/dropzone' %>
|
||||||
|
<% end %>
|
||||||
|
<% if @templates.present? || params[:q].blank? %>
|
||||||
|
<% if @pagy.pages > 1 %>
|
||||||
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
|
||||||
|
<% else %>
|
||||||
|
<div class="mt-2">
|
||||||
|
<%= view_archived_html %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% elsif params[:q].present? %>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mt-16 text-3xl font-semibold">
|
||||||
|
Templates not Found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -1 +1 @@
|
|||||||
<template-builder class="grid" data-editable="false" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
|
<template-builder class="grid" data-editable="false" data-template="<%= @template_data %>"></template-builder>
|
||||||
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user