mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5439c63ce7 | ||
|
|
b78287242b | ||
|
|
b869ae38b3 | ||
|
|
4a320e9ba6 | ||
|
|
0025928a35 | ||
|
|
c227a1988a | ||
|
|
3ade45f541 | ||
|
|
17da8bde80 | ||
|
|
59d9a59769 | ||
|
|
7210badde9 | ||
|
|
21659a73f3 | ||
|
|
c0e49ebeea | ||
|
|
2d24eac4a3 | ||
|
|
14c3c8181b | ||
|
|
036f6049a2 | ||
|
|
236e4cb35f | ||
|
|
ee77cb0982 | ||
|
|
d474ad05e5 | ||
|
|
dbf1fa37c5 | ||
|
|
7444bb8b68 | ||
|
|
f7132445a1 | ||
|
|
0127dd738e | ||
|
|
df561a6211 | ||
|
|
50d3404f3b | ||
|
|
607fc8d338 | ||
|
|
8c366dfc38 | ||
|
|
5ed08fa9ab | ||
|
|
ddb557ebef | ||
|
|
b48615df3f | ||
|
|
86c7b92675 | ||
|
|
041f362cc4 | ||
|
|
65f491325e | ||
|
|
57aef5f5e5 | ||
|
|
e704e6fa04 | ||
|
|
4c99bbb58a | ||
|
|
c0647eef2c | ||
|
|
c619b9be93 | ||
|
|
abab3f9688 | ||
|
|
7b15111e8f | ||
|
|
2c7f38c503 | ||
|
|
ed80df1372 | ||
|
|
3614d1796c | ||
|
|
ce39494f84 | ||
|
|
c639a3f733 | ||
|
|
74bef133b3 | ||
|
|
33142966dc | ||
|
|
42ec8d5fc9 | ||
|
|
01d752e811 | ||
|
|
99b33d4165 | ||
|
|
11a90d797a | ||
|
|
9da7463657 | ||
|
|
b6d350ebbd | ||
|
|
9e87a0373f | ||
|
|
aee55165c5 | ||
|
|
9ddbcae153 | ||
|
|
10cd3aa72a | ||
|
|
6684a27e2f | ||
|
|
b6f7a7dd15 | ||
|
|
309e43e18e | ||
|
|
d5b8640a0e | ||
|
|
49b9064d9f | ||
|
|
698031cfc0 | ||
|
|
6dc21e66e1 | ||
|
|
60b32fec94 | ||
|
|
c621166b45 | ||
|
|
8e2da83f9e | ||
|
|
a39ba5b275 | ||
|
|
edf466a6de | ||
|
|
3b8bb8c5bb | ||
|
|
79f1061353 | ||
|
|
745f2160c5 | ||
|
|
2f0c79444b | ||
|
|
69a3782903 | ||
|
|
3dd7c47558 | ||
|
|
761bb07ee6 | ||
|
|
92d7bc65f5 |
No files matched your search
@@ -67,8 +67,14 @@ RSpec/MultipleMemoizedHelpers:
|
|||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/FindEach:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/SkipsModelValidations:
|
Rails/SkipsModelValidations:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Capybara/ClickLinkOrButtonStyle:
|
||||||
|
Enabled: false
|
||||||
+2
-1
@@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test"
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation
|
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ COPY LICENSE README.md Rakefile config.ru ./
|
|||||||
|
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
|
RUN ln -s /fonts /app/public/fonts
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
|
|||||||
+13
-15
@@ -98,7 +98,6 @@ GEM
|
|||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.1.1)
|
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.19)
|
||||||
better_html (2.0.2)
|
better_html (2.0.2)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
@@ -294,7 +293,7 @@ GEM
|
|||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.4)
|
||||||
minitest (5.19.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
@@ -345,7 +344,7 @@ GEM
|
|||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.0.4)
|
pagy (6.0.4)
|
||||||
parallel (1.23.0)
|
parallel (1.23.0)
|
||||||
parser (3.2.2.3)
|
parser (3.2.2.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.11.0)
|
pdf-reader (2.11.0)
|
||||||
@@ -418,7 +417,7 @@ GEM
|
|||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
redis-client (0.16.0)
|
redis-client (0.16.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.2)
|
||||||
reline (0.3.8)
|
reline (0.3.8)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
@@ -455,33 +454,32 @@ GEM
|
|||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.12)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.12)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rubocop (1.56.1)
|
rubocop (1.57.2)
|
||||||
base64 (~> 0.1.1)
|
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.3)
|
parser (>= 3.2.2.4)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.28.1, < 2.0)
|
rubocop-ast (>= 1.28.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.30.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-capybara (2.18.0)
|
rubocop-capybara (2.19.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.23.1)
|
rubocop-factory_bot (2.24.0)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-performance (1.19.0)
|
rubocop-performance (1.19.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 0.4.0)
|
||||||
rubocop-rails (2.20.2)
|
rubocop-rails (2.22.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-rspec (2.23.2)
|
rubocop-rspec (2.25.0)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
@@ -537,7 +535,7 @@ GEM
|
|||||||
tzinfo-data (1.2023.3)
|
tzinfo-data (1.2023.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
version_gem (1.1.3)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module Api
|
|||||||
|
|
||||||
def paginate(relation)
|
def paginate(relation)
|
||||||
result = relation.order(id: :desc)
|
result = relation.order(id: :desc)
|
||||||
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
result = result.where('id < ?', params[:after]) if params[:after].present?
|
||||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
result = result.where('id > ?', params[:before]) if params[:before].present?
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ module Api
|
|||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
||||||
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.as_json(serialize_params),
|
data: submissions.as_json(serialize_params),
|
||||||
@@ -93,6 +94,7 @@ module Api
|
|||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
only: %i[id source submitters_order created_at updated_at],
|
only: %i[id source submitters_order created_at updated_at],
|
||||||
|
methods: %i[audit_log_url],
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
completed_at opened_at sent_at
|
completed_at opened_at sent_at
|
||||||
@@ -106,7 +108,9 @@ module Api
|
|||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{
|
params.permit(submission: [{
|
||||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]]
|
submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key,
|
||||||
|
{ values: {}, readonly_fields: [],
|
||||||
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||||
}])
|
}])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -115,11 +119,15 @@ module Api
|
|||||||
|
|
||||||
Array.wrap(submissions_params).each do |submission|
|
Array.wrap(submissions_params).each do |submission|
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
next if submitter[:values].blank?
|
default_values = submitter[:values] || {}
|
||||||
|
|
||||||
|
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
|
||||||
|
|
||||||
|
next if default_values.blank?
|
||||||
|
|
||||||
values, new_attachments =
|
values, new_attachments =
|
||||||
Submitters::NormalizeValues.call(template,
|
Submitters::NormalizeValues.call(template,
|
||||||
submitter[:values],
|
default_values,
|
||||||
submitter[:role] || template.submitters[index]['name'])
|
submitter[:role] || template.submitters[index]['name'])
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
|||||||
@@ -4,6 +4,27 @@ module Api
|
|||||||
class SubmittersController < ApiBaseController
|
class SubmittersController < ApiBaseController
|
||||||
load_and_authorize_resource :submitter
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = Submitters.search(@submitters, params[:q])
|
||||||
|
|
||||||
|
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||||
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
submitters = paginate(
|
||||||
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
|
||||||
|
pagination: {
|
||||||
|
count: submitters.size,
|
||||||
|
next: submitters.last&.id,
|
||||||
|
prev: submitters.first&.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module Api
|
|||||||
templates = Templates.search(@templates, params[:q])
|
templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
templates = params[:archived] ? templates.archived : templates.active
|
templates = params[:archived] ? templates.archived : templates.active
|
||||||
|
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||||
|
|
||||||
@@ -51,11 +52,13 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name,
|
params.require(:template).permit(
|
||||||
schema: [%i[attachment_uuid name]],
|
:name,
|
||||||
submitters: [%i[name uuid]],
|
schema: [%i[attachment_uuid name]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
submitters: [%i[name uuid]],
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
|
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,6 +16,7 @@ module Api
|
|||||||
render json: {
|
render json: {
|
||||||
schema:,
|
schema:,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
|
methods: [:metadata],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.order('random()').take) unless signed_in?
|
sign_in(User.active.order('random()').take) unless signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EnquiriesController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
if params[:talk_to_sales] == 'on'
|
||||||
|
Faraday.post(Docuseal::ENQUIRIES_URL,
|
||||||
|
enquiry_params.merge(type: :talk_to_sales).to_json,
|
||||||
|
'Content-Type' => 'application/json')
|
||||||
|
end
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def enquiry_params
|
||||||
|
params.require(:user).permit(:email)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
@@ -22,7 +22,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||||
else
|
else
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
|
||||||
@error_message = 'Code is invalid'
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PasswordsController < Devise::PasswordsController
|
||||||
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
|
attribute :user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
super do |resource|
|
||||||
|
Current.user = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PreviewDocumentPageController < ActionController::API
|
||||||
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
|
def show
|
||||||
|
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
||||||
|
|
||||||
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
||||||
|
|
||||||
|
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||||
|
|
||||||
|
file_path =
|
||||||
|
if attachment.service.name == :disk
|
||||||
|
ActiveStorage::Blob.service.path_for(attachment.key)
|
||||||
|
else
|
||||||
|
find_or_create_document_tempfile_path(attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
|
render plain: io.tap(&:rewind).read
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
||||||
|
|
||||||
|
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||||
|
f.flock(File::LOCK_EX)
|
||||||
|
|
||||||
|
# rubocop:disable Style/ZeroLengthPredicate
|
||||||
|
if f.size.zero?
|
||||||
|
f.binmode
|
||||||
|
|
||||||
|
f.write(attachment.download)
|
||||||
|
end
|
||||||
|
# rubocop:enable Style/ZeroLengthPredicate
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,10 +31,10 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def set_flash_message(key, kind, options = {})
|
||||||
super
|
return if key == :alert && kind == 'already_authenticated'
|
||||||
|
|
||||||
flash.clear
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_resource(_hash = {})
|
def build_resource(_hash = {})
|
||||||
@@ -43,7 +43,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
self.resource = account.users.new(user_params)
|
||||||
|
|
||||||
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
account.name ||= resource.full_name if params[:action] == 'create'
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ class SessionsController < Devise::SessionsController
|
|||||||
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def set_flash_message(key, kind, options = {})
|
||||||
super
|
return if key == :alert && kind == 'already_authenticated'
|
||||||
|
|
||||||
flash.clear
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SsoSettingsController < ApplicationController
|
||||||
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
authorize_resource :encrypted_config, parent: false, except: :index
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,7 +14,8 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
||||||
.find_or_initialize_by(email: submitter_params[:email])
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||||
@@ -46,7 +47,7 @@ class StartFormController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,12 +6,24 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
def show
|
def show
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }]
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||||
|
|
||||||
|
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: @submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,28 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter =
|
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
Submitter.preload(submission: [
|
|
||||||
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
|
|
||||||
])
|
|
||||||
.find_by!(slug: params[:slug])
|
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submitter],
|
||||||
|
associations: [submission: [:template, { template_schema_documents: :blob }]]
|
||||||
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||||
|
|
||||||
|
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: @submitter.submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
cookies[:submitter_sid] = @submitter.signed_id
|
cookies[:submitter_sid] = @submitter.signed_id
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ class TemplatesController < ApplicationController
|
|||||||
submissions = submissions.active if @template.deleted_at.blank?
|
submissions = submissions.active if @template.deleted_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q])
|
||||||
|
|
||||||
|
@base_submissions = submissions
|
||||||
|
|
||||||
|
submissions = submissions.pending if params[:status] == 'pending'
|
||||||
|
submissions = submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
@@ -25,6 +30,14 @@ class TemplatesController < ApplicationController
|
|||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
@template_data =
|
||||||
|
@template.as_json.merge(
|
||||||
|
documents: @template.schema_documents.as_json(
|
||||||
|
methods: [:metadata],
|
||||||
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
).to_json
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,9 +57,18 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template.update!(deleted_at: Time.current)
|
notice =
|
||||||
|
if !Docuseal.multitenant? && params[:permanently].present?
|
||||||
|
@template.destroy!
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
'Template has been removed.'
|
||||||
|
else
|
||||||
|
@template.update!(deleted_at: Time.current)
|
||||||
|
|
||||||
|
'Template has been archived.'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path, notice:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -3,20 +3,55 @@
|
|||||||
class TemplatesUploadsController < ApplicationController
|
class TemplatesUploadsController < ApplicationController
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
layout 'plain'
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
|
|
||||||
@template.account = current_account
|
@template.account = current_account
|
||||||
@template.author = current_user
|
@template.author = current_user
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
@template.name = File.basename(params[:files].first.original_filename, '.*')
|
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
@template.save!
|
@template.save!
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to root_path, alert: 'Unable to upload file'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_file_params_from_url
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(conn.get(params[:url]).body)
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename: File.basename(
|
||||||
|
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
||||||
|
),
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
{ files: [file] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def conn
|
||||||
|
Faraday.new do |faraday|
|
||||||
|
faraday.response :follow_redirects
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -12,7 +12,7 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
cert_data = if Docuseal.multitenant?
|
cert_data = if Docuseal.multitenant?
|
||||||
Docuseal::CERTS
|
Docuseal::CERTS
|
||||||
else
|
else
|
||||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
|
|||||||
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
|
import SubmitForm from './elements/submit_form'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
|||||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
|
|
||||||
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) => {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,10 @@ select:required:invalid {
|
|||||||
@apply border-base-content/20;
|
@apply border-base-content/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.base-textarea {
|
||||||
|
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply no-animation;
|
@apply no-animation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,9 +110,10 @@
|
|||||||
<span v-else-if="field.type === 'date'">
|
<span v-else-if="field.type === 'date'">
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span
|
||||||
{{ modelValue }}
|
v-else
|
||||||
</span>
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
>
|
>
|
||||||
{{ t('signed_with') }}
|
{{ t('signed_with') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co"
|
href="https://www.docuseal.co/start"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl"
|
||||||
|
>{{ field.name || t('date') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
|
@click.prevent="setCurrentDate"
|
||||||
|
>
|
||||||
|
<IconCalendarCheck :width="16" />
|
||||||
|
{{ t('set_today') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="value"
|
||||||
|
class="base-input !text-2xl text-center w-full"
|
||||||
|
:required="field.required"
|
||||||
|
type="date"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DateStep',
|
||||||
|
components: {
|
||||||
|
IconCalendarCheck
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCurrentDate () {
|
||||||
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -58,54 +58,20 @@
|
|||||||
>
|
>
|
||||||
<div class="md:mt-4">
|
<div class="md:mt-4">
|
||||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||||
<label
|
<TextStep
|
||||||
v-if="currentField.name"
|
:key="currentField.uuid"
|
||||||
:for="currentField.uuid"
|
v-model="values[currentField.uuid]"
|
||||||
class="label text-2xl mb-2"
|
:field="currentField"
|
||||||
>{{ currentField.name }}
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl w-full"
|
|
||||||
:required="currentField.required"
|
|
||||||
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
|
||||||
type="text"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="currentField.type === 'date'">
|
|
||||||
<label
|
|
||||||
v-if="currentField.name"
|
|
||||||
:for="currentField.uuid"
|
|
||||||
class="label text-2xl mb-2"
|
|
||||||
>{{ currentField.name }}
|
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
<div class="text-center">
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl text-center w-full"
|
|
||||||
:required="currentField.required"
|
|
||||||
type="date"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DateStep
|
||||||
|
v-else-if="currentField.type === 'date'"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
v-if="currentField.name"
|
||||||
@@ -179,6 +145,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<MultiSelectStep
|
<MultiSelectStep
|
||||||
v-else-if="currentField.type === 'multiple'"
|
v-else-if="currentField.type === 'multiple'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
/>
|
/>
|
||||||
@@ -240,6 +207,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
v-else-if="currentField.type === 'image'"
|
v-else-if="currentField.type === 'image'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -250,8 +218,10 @@
|
|||||||
<SignatureStep
|
<SignatureStep
|
||||||
v-else-if="currentField.type === 'signature'"
|
v-else-if="currentField.type === 'signature'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:previous-value="previousSignatureValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -262,8 +232,10 @@
|
|||||||
<InitialsStep
|
<InitialsStep
|
||||||
v-else-if="currentField.type === 'initials'"
|
v-else-if="currentField.type === 'initials'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:previous-value="previousInitialsValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -274,6 +246,7 @@
|
|||||||
/>
|
/>
|
||||||
<AttachmentStep
|
<AttachmentStep
|
||||||
v-else-if="currentField.type === 'file'"
|
v-else-if="currentField.type === 'file'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
@@ -284,6 +257,7 @@
|
|||||||
<PhoneStep
|
<PhoneStep
|
||||||
v-else-if="currentField.type === 'phone'"
|
v-else-if="currentField.type === 'phone'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
@@ -294,6 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div class="mt-6 md:mt-8">
|
||||||
<button
|
<button
|
||||||
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="base-button w-full flex justify-center"
|
class="base-button w-full flex justify-center"
|
||||||
:disabled="isButtonDisabled"
|
:disabled="isButtonDisabled"
|
||||||
@@ -314,6 +289,12 @@
|
|||||||
><span>...</span></span>
|
><span>...</span></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="showFillAllRequiredFields"
|
||||||
|
class="text-center mt-1"
|
||||||
|
>
|
||||||
|
{{ t('please_fill_all_required_fields') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<FormCompleted
|
<FormCompleted
|
||||||
@@ -332,7 +313,7 @@
|
|||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
href="#"
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
||||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
|
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,6 +330,8 @@ import InitialsStep from './initials_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
|
import TextStep from './text_step'
|
||||||
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { t } from './i18n'
|
import { t } from './i18n'
|
||||||
@@ -363,7 +346,9 @@ export default {
|
|||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
|
DateStep,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
@@ -389,6 +374,13 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
onComplete: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -422,7 +414,7 @@ export default {
|
|||||||
allowToSkip: {
|
allowToSkip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: false
|
||||||
},
|
},
|
||||||
goToLast: {
|
goToLast: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -454,6 +446,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: true,
|
isFormVisible: true,
|
||||||
|
showFillAllRequiredFields: false,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
@@ -467,6 +460,16 @@ export default {
|
|||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
|
previousSignatureValue () {
|
||||||
|
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[signatureField?.uuid]
|
||||||
|
},
|
||||||
|
previousInitialsValue () {
|
||||||
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[initialsField?.uuid]
|
||||||
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||||
},
|
},
|
||||||
@@ -484,7 +487,7 @@ export default {
|
|||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
},
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
return this.fields.reduce((acc, f) => {
|
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
const prevStep = acc[acc.length - 1]
|
||||||
|
|
||||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
||||||
@@ -510,11 +513,27 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
|
this.fields.forEach((field) => {
|
||||||
|
if (field.default_value && !field.readonly) {
|
||||||
|
this.values[field.uuid] = field.default_value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (this.goToLast) {
|
if (this.goToLast) {
|
||||||
this.currentStep = Math.min(
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||||
this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1,
|
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||||
this.stepFields.length - 1
|
|
||||||
)
|
const indexesList = [this.stepFields.length - 1]
|
||||||
|
|
||||||
|
if (requiredEmptyStepIndex !== -1) {
|
||||||
|
indexesList.push(requiredEmptyStepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFilledStepIndex !== -1) {
|
||||||
|
indexesList.push(lastFilledStepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStep = Math.min(...indexesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
@@ -605,6 +624,7 @@ export default {
|
|||||||
},
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
|
this.showFillAllRequiredFields = false
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
@@ -643,8 +663,9 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
|
const isLastStep = this.currentStep === this.stepFields.length - 1
|
||||||
|
|
||||||
if (this.currentStep === this.stepFields.length - 1 && !emptyRequiredField) {
|
if (isLastStep && !emptyRequiredField) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,12 +680,26 @@ export default {
|
|||||||
|
|
||||||
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
||||||
|
|
||||||
const nextStep = emptyRequiredField || this.stepFields[this.currentStep + 1]
|
if (isLastStep) {
|
||||||
|
this.isSecondWalkthrough = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
this.goToStep(nextStep, true)
|
this.goToStep(nextStep, true)
|
||||||
|
|
||||||
|
if (emptyRequiredField === nextStep) {
|
||||||
|
this.showFillAllRequiredFields = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.isCompleted = true
|
this.isCompleted = true
|
||||||
|
|
||||||
|
const respData = await response.text()
|
||||||
|
|
||||||
|
if (respData) {
|
||||||
|
this.onComplete(JSON.parse(respData))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.error('Error submitting form:', error)
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ const en = {
|
|||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
resend_code: 'Re-send code',
|
resend_code: 'Re-send code',
|
||||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
||||||
|
please_fill_all_required_fields: 'Please fill all required fields',
|
||||||
|
set_today: 'Set Today',
|
||||||
|
toggle_multiline_text: 'Toggle Multiline Text',
|
||||||
|
date: 'Date',
|
||||||
email_has_been_sent: 'Email has been sent'
|
email_has_been_sent: 'Email has been sent'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +67,10 @@ const es = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||||
|
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
||||||
|
set_today: 'Establecer Hoy',
|
||||||
|
date: 'Fecha',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +105,10 @@ const it = {
|
|||||||
sending: 'Invio in corso...',
|
sending: 'Invio in corso...',
|
||||||
resend_code: 'Rinvia codice',
|
resend_code: 'Rinvia codice',
|
||||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||||
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
|
set_today: 'Imposta Oggi',
|
||||||
|
date: 'Data',
|
||||||
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
email_has_been_sent: "L'email è stata inviata"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +143,10 @@ const de = {
|
|||||||
sending: 'Senden...',
|
sending: 'Senden...',
|
||||||
resend_code: 'Code erneut senden',
|
resend_code: 'Code erneut senden',
|
||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||||
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
|
set_today: 'Heute einstellen',
|
||||||
|
date: 'Datum',
|
||||||
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +181,10 @@ const fr = {
|
|||||||
sending: 'Envoi en cours...',
|
sending: 'Envoi en cours...',
|
||||||
resend_code: 'Renvoyer le code',
|
resend_code: 'Renvoyer le code',
|
||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
|
set_today: "Définir Aujourd'hui",
|
||||||
|
date: 'Date',
|
||||||
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
email_has_been_sent: "L'email a été envoyé"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +219,10 @@ const pl = {
|
|||||||
sending: 'Wysyłanie...',
|
sending: 'Wysyłanie...',
|
||||||
resend_code: 'Ponownie wyślij kod',
|
resend_code: 'Ponownie wyślij kod',
|
||||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
|
set_today: 'Ustaw Dziś',
|
||||||
|
date: 'Data',
|
||||||
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
email_has_been_sent: 'E-mail został wysłany'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +257,10 @@ const uk = {
|
|||||||
sending: 'Надсилаю...',
|
sending: 'Надсилаю...',
|
||||||
resend_code: 'Повторно відправити код',
|
resend_code: 'Повторно відправити код',
|
||||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
|
set_today: 'Задати Сьогодні',
|
||||||
|
date: 'Дата',
|
||||||
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +295,10 @@ const cs = {
|
|||||||
sending: 'Odesílání...',
|
sending: 'Odesílání...',
|
||||||
resend_code: 'Znovu odeslat kód',
|
resend_code: 'Znovu odeslat kód',
|
||||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||||
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
|
set_today: 'Nastavit Dnes',
|
||||||
|
date: 'Datum',
|
||||||
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
email_has_been_sent: 'E-mail byl odeslán'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +333,10 @@ const pt = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||||
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
|
set_today: 'Definir Hoje',
|
||||||
|
date: 'Data',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado'
|
email_has_been_sent: 'Email enviado'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,33 +9,37 @@
|
|||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-sm btn-circle"
|
||||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -43,26 +47,26 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
@@ -78,6 +82,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
@@ -106,6 +111,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -115,10 +125,20 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isInitialsStarted: false,
|
isInitialsStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isDrawInitials: false
|
isDrawInitials: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -133,8 +153,6 @@ export default {
|
|||||||
import('@rails/activestorage')
|
import('@rails/activestorage')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -148,6 +166,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isInitialsStarted = false
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -189,7 +210,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||||
<a
|
<a
|
||||||
|
v-if="!defaultValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="link"
|
class="link"
|
||||||
@click.prevent="isCodeSent = false"
|
@click.prevent="isCodeSent = false"
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="phone"
|
ref="phone"
|
||||||
:value="modelValue || defaultValue"
|
:value="modelValue || defaultValue"
|
||||||
|
:readonly="!!defaultValue"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
pattern="^\+[0-9\s\-]+$"
|
pattern="^\+[0-9\s\-]+$"
|
||||||
|
|||||||
@@ -9,14 +9,15 @@
|
|||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Type text"
|
data-tip="Type text"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-sm btn-circle"
|
||||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
</button>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
@@ -34,23 +35,26 @@
|
|||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('redraw') }}
|
{{ t('redraw') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
|
href="#"
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
@@ -59,21 +63,21 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded"
|
||||||
/>
|
/>
|
||||||
@@ -93,6 +97,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
|
let isFontLoaded = false
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignatureStep',
|
name: 'SignatureStep',
|
||||||
@@ -122,6 +129,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -131,10 +143,20 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: false,
|
isSignatureStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isTextSignature: false
|
isTextSignature: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -147,8 +169,6 @@ export default {
|
|||||||
import('@rails/activestorage')
|
import('@rails/activestorage')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -162,6 +182,22 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isSignatureStarted = false
|
||||||
|
},
|
||||||
|
loadFont () {
|
||||||
|
if (!isFontLoaded) {
|
||||||
|
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
|
||||||
|
|
||||||
|
font.load().then((loadedFont) => {
|
||||||
|
document.fonts.add(loadedFont)
|
||||||
|
|
||||||
|
isFontLoaded = true
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Font loading failed:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -178,8 +214,8 @@ export default {
|
|||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const fontFamily = 'Arial'
|
const fontFamily = 'Dancing Script'
|
||||||
const fontSize = '44px'
|
const fontSize = '38px'
|
||||||
const fontStyle = 'italic'
|
const fontStyle = 'italic'
|
||||||
const fontWeight = ''
|
const fontWeight = ''
|
||||||
|
|
||||||
@@ -197,6 +233,8 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
this.$refs.textInput.focus()
|
||||||
|
|
||||||
|
this.loadFont()
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -250,7 +288,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="field.name"
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<div class="items-center flex">
|
||||||
|
<input
|
||||||
|
v-if="!isTextArea"
|
||||||
|
:id="field.uuid"
|
||||||
|
v-model="text"
|
||||||
|
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||||
|
:required="field.required"
|
||||||
|
:pattern="field.validation?.pattern"
|
||||||
|
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
||||||
|
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
||||||
|
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
|
type="text"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-if="isTextArea"
|
||||||
|
:id="field.uuid"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="text"
|
||||||
|
class="base-textarea !text-2xl w-full"
|
||||||
|
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
|
:required="field.required"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@input="resizeTextarea"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!isTextArea"
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('toggle_multiline_text')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-ghost btn-circle btn-sm"
|
||||||
|
@click.prevent="toggleTextArea"
|
||||||
|
>
|
||||||
|
<IconAlignBoxLeftTop />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextStep',
|
||||||
|
components: {
|
||||||
|
IconAlignBoxLeftTop
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isTextArea: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
text: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isTextArea = this.modelValue?.includes('\n')
|
||||||
|
|
||||||
|
if (this.isTextArea) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.resizeTextarea()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resizeTextarea () {
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px'
|
||||||
|
},
|
||||||
|
toggleTextArea () {
|
||||||
|
this.isTextArea = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.textarea.focus()
|
||||||
|
this.$refs.textarea.setSelectionRange(this.$refs.textarea.value.length, this.$refs.textarea.value.length)
|
||||||
|
|
||||||
|
this.resizeTextarea()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="index === 0"
|
v-if="index === 0 && editable"
|
||||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||||
style="left: -4px"
|
style="left: -4px"
|
||||||
@mousedown.stop="startResizeCell"
|
@mousedown.stop="startResizeCell"
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
|
:editable="editable"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:button-width="27"
|
:button-width="27"
|
||||||
|
:editable="editable"
|
||||||
:button-classes="'px-1'"
|
:button-classes="'px-1'"
|
||||||
:menu-classes="'bg-white rounded-t-none'"
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@@ -58,7 +60,7 @@
|
|||||||
<span
|
<span
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
ref="name"
|
ref="name"
|
||||||
contenteditable
|
:contenteditable="editable"
|
||||||
class="pr-1 cursor-text outline-none block"
|
class="pr-1 cursor-text outline-none block"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
>Required</label>
|
>Required</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else-if="editable"
|
||||||
class="pr-1"
|
class="pr-1"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click.prevent="$emit('remove')"
|
@click.prevent="$emit('remove')"
|
||||||
@@ -93,18 +95,27 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="opacity-50 flex items-center justify-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
:class="bgColors[submitterIndex]"
|
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1 h-full"
|
class="flex justify-center items-center space-x-1 h-full"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="field?.default_value"
|
||||||
|
class="text-[1.5vw] lg:text-base"
|
||||||
|
>
|
||||||
|
<div class="flex items-center px-0.5">
|
||||||
|
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="fieldIcons[field.type]"
|
:is="fieldIcons[field.type]"
|
||||||
|
v-else
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10"
|
class="max-h-10 opacity-50"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +124,7 @@
|
|||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type"
|
v-if="field?.type && editable"
|
||||||
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
@touchstart="startTouchResize"
|
@touchstart="startTouchResize"
|
||||||
@@ -145,6 +156,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -185,30 +201,30 @@ export default {
|
|||||||
},
|
},
|
||||||
borderColors () {
|
borderColors () {
|
||||||
return [
|
return [
|
||||||
'border-red-500',
|
'border-red-500/50',
|
||||||
'border-sky-500',
|
'border-sky-500/50',
|
||||||
'border-emerald-500',
|
'border-emerald-500/50',
|
||||||
'border-yellow-300',
|
'border-yellow-300/50',
|
||||||
'border-purple-600',
|
'border-purple-600/50',
|
||||||
'border-pink-500',
|
'border-pink-500/50',
|
||||||
'border-cyan-500',
|
'border-cyan-500/50',
|
||||||
'border-orange-500',
|
'border-orange-500/50',
|
||||||
'border-lime-500',
|
'border-lime-500/50',
|
||||||
'border-indigo-500'
|
'border-indigo-500/50'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
'bg-red-100',
|
'bg-red-100/50',
|
||||||
'bg-sky-100',
|
'bg-sky-100/50',
|
||||||
'bg-emerald-100',
|
'bg-emerald-100/50',
|
||||||
'bg-yellow-100',
|
'bg-yellow-100/50',
|
||||||
'bg-purple-100',
|
'bg-purple-100/50',
|
||||||
'bg-pink-100',
|
'bg-pink-100/50',
|
||||||
'bg-cyan-100',
|
'bg-cyan-100/50',
|
||||||
'bg-orange-100',
|
'bg-orange-100/50',
|
||||||
'bg-lime-100',
|
'bg-lime-100/50',
|
||||||
'bg-indigo-100'
|
'bg-indigo-100/50'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
@@ -262,6 +278,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
@@ -313,6 +331,10 @@ export default {
|
|||||||
startDrag (e) {
|
startDrag (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
|
:editable="editable"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
class="text-3xl font-semibold focus:text-clip"
|
||||||
:icon-stroke-width="2.3"
|
:icon-stroke-width="2.3"
|
||||||
@update:model-value="updateName"
|
@update:model-value="updateName"
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
:href="`/templates/${template.id}/submissions/new`"
|
:href="`/templates/${template.id}/submissions/new`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="btn btn-primary text-base"
|
class="btn btn-primary text-base"
|
||||||
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconUsersPlus
|
<IconUsersPlus
|
||||||
width="20"
|
width="20"
|
||||||
@@ -75,6 +77,7 @@
|
|||||||
:item="item"
|
:item="item"
|
||||||
:document="sortedDocuments[index]"
|
:document="sortedDocuments[index]"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:editable="editable"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@@ -89,7 +92,7 @@
|
|||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length"
|
v-if="sortedDocuments.length && editable"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -121,12 +124,13 @@
|
|||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragFieldType"
|
:is-drag="!!dragFieldType"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
|
:editable="editable"
|
||||||
@draw="onDraw"
|
@draw="onDraw"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
/>
|
/>
|
||||||
<DocumentControls
|
<DocumentControls
|
||||||
v-if="isBreakpointLg"
|
v-if="isBreakpointLg && editable"
|
||||||
: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)"
|
||||||
:document="document"
|
:document="document"
|
||||||
@@ -141,7 +145,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
v-if="sortedDocuments.length && isBreakpointLg"
|
v-if="sortedDocuments.length && isBreakpointLg && editable"
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
@@ -152,34 +156,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<MobileDrawField
|
||||||
v-if="drawField"
|
v-if="drawField && isBreakpointLg"
|
||||||
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
|
:draw-field="drawField"
|
||||||
>
|
:fields="template.fields"
|
||||||
<span
|
:submitters="template.submitters"
|
||||||
class="rounded bg-base-200 px-4 py-2 rounded-full inline-flex space-x-2 mx-auto items-center mb-4 z-20 md:hidden"
|
:selected-submitter="selectedSubmitter"
|
||||||
>
|
class="md:hidden"
|
||||||
<component
|
:editable="editable"
|
||||||
:is="fieldIcons[drawField.type]"
|
@cancel="drawField = null"
|
||||||
:width="20"
|
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||||
:height="20"
|
/>
|
||||||
class="inline"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
Draw {{ fieldNames[drawField.type] }} Field
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="link block text-center"
|
|
||||||
@click.prevent="drawField = null"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<FieldType
|
<FieldType
|
||||||
v-if="sortedDocuments.length && !drawField"
|
v-if="sortedDocuments.length && !drawField && editable"
|
||||||
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
||||||
:model-value="''"
|
:model-value="''"
|
||||||
@update:model-value="startFieldDraw($event)"
|
@update:model-value="startFieldDraw($event)"
|
||||||
@@ -230,6 +219,7 @@
|
|||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
|
:editable="editable"
|
||||||
@set-draw="drawField = $event"
|
@set-draw="drawField = $event"
|
||||||
@set-drag="dragFieldType = $event"
|
@set-drag="dragFieldType = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@@ -246,6 +236,7 @@
|
|||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import Dropzone from './dropzone'
|
import Dropzone from './dropzone'
|
||||||
import Fields from './fields'
|
import Fields from './fields'
|
||||||
|
import MobileDrawField from './mobile_draw_field'
|
||||||
import Document from './document'
|
import Document from './document'
|
||||||
import Logo from './logo'
|
import Logo from './logo'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
@@ -262,6 +253,7 @@ export default {
|
|||||||
Upload,
|
Upload,
|
||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
|
MobileDrawField,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
FieldType,
|
FieldType,
|
||||||
IconX,
|
IconX,
|
||||||
@@ -299,6 +291,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -691,14 +688,25 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
onSaveClick () {
|
maybeShowEmptyTemplateAlert (e) {
|
||||||
this.isSaving = true
|
if (!this.template.fields.length) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
this.save().then(() => {
|
alert('Please draw fields to prepare the document.')
|
||||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
}
|
||||||
}).finally(() => {
|
},
|
||||||
this.isSaving = false
|
onSaveClick () {
|
||||||
})
|
if (this.template.fields.length) {
|
||||||
|
this.isSaving = true
|
||||||
|
|
||||||
|
this.save().then(() => {
|
||||||
|
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isSaving = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
alert('Please draw fields to prepare the document.')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
contenteditable
|
:contenteditable="editable"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
:class="iconInline ? 'inline' : 'block'"
|
:class="iconInline ? 'inline' : 'block'"
|
||||||
class="peer outline-none focus:block"
|
class="peer outline-none focus:block"
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
v-if="editable"
|
||||||
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
@@ -69,6 +70,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
iconStrokeWidth: {
|
iconStrokeWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
:key="image.id"
|
:key="image.id"
|
||||||
:ref="setPageRefs"
|
:ref="setPageRefs"
|
||||||
:number="index"
|
:number="index"
|
||||||
|
:editable="editable"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
@@ -38,6 +39,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
drawField: {
|
drawField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -56,8 +62,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
numberOfPages () {
|
||||||
|
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||||
|
},
|
||||||
sortedPreviewImages () {
|
sortedPreviewImages () {
|
||||||
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))
|
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
|
||||||
|
|
||||||
|
return [...Array(this.numberOfPages).keys()].map((i) => {
|
||||||
|
return this.previewImagesIndex[i] || {
|
||||||
|
metadata: lazyloadMetadata,
|
||||||
|
id: Math.random().toString(),
|
||||||
|
url: `/preview/${this.document.uuid}/${i}.jpg`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
previewImagesIndex () {
|
||||||
|
return this.document.preview_images.reduce((acc, e) => {
|
||||||
|
acc[parseInt(e.filename)] = e
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group"
|
class="border border-base-300 rounded rounded-tr-none relative group"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative">
|
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<div class="flex items-center p-1 space-x-1">
|
<div class="flex items-center p-1 space-x-1">
|
||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
|
:editable="editable"
|
||||||
:button-width="20"
|
:button-width="20"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
ref="name"
|
ref="name"
|
||||||
:model-value="field.name || defaultName"
|
:model-value="field.name || defaultName"
|
||||||
|
:editable="editable"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
:icon-stroke-width="1.6"
|
:icon-stroke-width="1.6"
|
||||||
@@ -46,19 +49,29 @@
|
|||||||
>Required</label>
|
>Required</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="editable"
|
||||||
class="flex items-center space-x-1"
|
class="flex items-center space-x-1"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="field && !field.areas.length"
|
||||||
|
title="Draw"
|
||||||
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
@click="$emit('set-draw', field)"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<span
|
<span
|
||||||
v-if="field.areas?.length"
|
|
||||||
class="dropdown dropdown-end"
|
class="dropdown dropdown-end"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Areas"
|
title="Settings"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconSettings
|
||||||
:width="18"
|
:width="18"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
@@ -66,8 +79,57 @@
|
|||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'text'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.default_value"
|
||||||
|
type="text"
|
||||||
|
placeholder="Default value"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.default_value"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Default value
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<li @click.stop>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.required"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">Required</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type === 'text'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.readonly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">Read-only</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<hr class="pb-0.5 mt-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in field.areas || []"
|
v-for="(area, index) in field.areas || []"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -113,18 +175,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-else
|
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||||
title="Areas"
|
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
|
||||||
@click="$emit('set-draw', field)"
|
|
||||||
>
|
|
||||||
<IconShape
|
|
||||||
:width="18"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="relative text-transparent group-hover:text-base-content"
|
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
@@ -133,24 +184,6 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
|
|
||||||
<button
|
|
||||||
title="Up"
|
|
||||||
class="relative"
|
|
||||||
style="font-size: 10px; margin-bottom: -2px"
|
|
||||||
@click="$emit('move-up')"
|
|
||||||
>
|
|
||||||
▲
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Down"
|
|
||||||
class="relative"
|
|
||||||
style="font-size: 10px; margin-top: -2px"
|
|
||||||
@click="$emit('move-down')"
|
|
||||||
>
|
|
||||||
▼
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -194,26 +227,32 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy } from '@tabler/icons-vue'
|
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateField',
|
name: 'TemplateField',
|
||||||
components: {
|
components: {
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
|
IconSettings,
|
||||||
IconShape,
|
IconShape,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save'],
|
inject: ['template', 'save', 'backgroundColor'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isNameFocus: false
|
isNameFocus: false
|
||||||
@@ -266,6 +305,8 @@ export default {
|
|||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
},
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,91 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dropdown">
|
<div v-if="mobileView">
|
||||||
|
<div class="flex space-x-2 items-end">
|
||||||
|
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span
|
||||||
|
class="w-3 h-3 flex-shrink-0 rounded-full"
|
||||||
|
:class="colors[submitters.indexOf(selectedSubmitter)]"
|
||||||
|
/>
|
||||||
|
<Contenteditable
|
||||||
|
v-model="selectedSubmitter.name"
|
||||||
|
class="cursor-text"
|
||||||
|
:icon-inline="true"
|
||||||
|
:editable="editable"
|
||||||
|
:select-on-edit-click="true"
|
||||||
|
:icon-width="18"
|
||||||
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown dropdown-top dropdown-end">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
|
||||||
|
>
|
||||||
|
<IconChevronUp
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="editable"
|
||||||
|
tabindex="0"
|
||||||
|
class="rounded-md min-w-max mb-2"
|
||||||
|
:class="menuClasses"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(submitter, index) in submitters"
|
||||||
|
:key="submitter.uuid"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-2 group justify-between items-center"
|
||||||
|
:class="{ 'active': submitter === selectedSubmitter }"
|
||||||
|
@click.prevent="selectSubmitter(submitter)"
|
||||||
|
>
|
||||||
|
<span class="py-1 flex items-center">
|
||||||
|
<span
|
||||||
|
class="rounded-full w-3 h-3 ml-1 mr-3"
|
||||||
|
:class="colors[index]"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ submitter.name }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="submitters.length > 1 && editable"
|
||||||
|
class="px-2"
|
||||||
|
@click.stop="remove(submitter)"
|
||||||
|
>
|
||||||
|
<IconTrashX :width="18" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-if="submitters.length < 10 && editable">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-2"
|
||||||
|
@click.prevent="addSubmitter"
|
||||||
|
>
|
||||||
|
<IconUserPlus
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="py-1">
|
||||||
|
Add Submitter
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="dropdown"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
v-if="compact"
|
v-if="compact"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -14,7 +100,7 @@
|
|||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-between"
|
class="cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 w-full flex justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
@@ -25,6 +111,7 @@
|
|||||||
v-model="selectedSubmitter.name"
|
v-model="selectedSubmitter.name"
|
||||||
class="cursor-text"
|
class="cursor-text"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
|
:editable="editable"
|
||||||
:select-on-edit-click="true"
|
:select-on-edit-click="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
@@ -38,6 +125,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="editable || !compact"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
@@ -62,7 +150,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="!compact && submitters.length > 1"
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
class="hidden group-hover:block px-2"
|
class="hidden group-hover:block px-2"
|
||||||
@click.stop="remove(submitter)"
|
@click.stop="remove(submitter)"
|
||||||
>
|
>
|
||||||
@@ -70,7 +158,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 10">
|
<li v-if="submitters.length < 10 && editable">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -90,7 +178,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -100,18 +188,29 @@ export default {
|
|||||||
IconUserPlus,
|
IconUserPlus,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrashX
|
IconTrashX,
|
||||||
|
IconChevronUp
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
mobileView: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</slot>
|
</slot>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="editable"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@@ -65,6 +66,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
buttonWidth: {
|
buttonWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -5,26 +5,37 @@
|
|||||||
class="w-full rounded-lg"
|
class="w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
|
:editable="editable"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1 mt-2">
|
<div
|
||||||
|
class="mb-1 mt-2"
|
||||||
|
@dragover.prevent="onFieldDragover"
|
||||||
|
@drop="save"
|
||||||
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
|
:editable="editable && !dragField"
|
||||||
|
:draggable="editable"
|
||||||
|
@dragstart="dragField = field"
|
||||||
|
@dragend="dragField = null"
|
||||||
@remove="removeField"
|
@remove="removeField"
|
||||||
@move-up="move(field, -1)"
|
|
||||||
@move-down="move(field, 1)"
|
|
||||||
@scroll-to="$emit('scroll-to-area', $event)"
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1 pb-2">
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="grid grid-cols-3 gap-1 pb-2"
|
||||||
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
@@ -100,7 +111,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4"
|
v-if="fields.length < 4 && editable"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
@@ -137,6 +148,11 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -152,6 +168,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
dragField: null
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
@@ -163,6 +184,24 @@ export default {
|
|||||||
onDragstart (fieldType) {
|
onDragstart (fieldType) {
|
||||||
this.$emit('set-drag', fieldType)
|
this.$emit('set-drag', fieldType)
|
||||||
},
|
},
|
||||||
|
onFieldDragover (e) {
|
||||||
|
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid
|
||||||
|
|
||||||
|
if (this.dragField && targetFieldUuid && this.dragField.uuid !== targetFieldUuid) {
|
||||||
|
const field = this.fields.find((f) => f.uuid === targetFieldUuid)
|
||||||
|
|
||||||
|
const currentIndex = this.fields.indexOf(this.dragField)
|
||||||
|
const targetIndex = this.fields.indexOf(field)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
this.fields.splice(targetIndex + 1, 0, this.dragField)
|
||||||
|
this.fields.splice(currentIndex, 1)
|
||||||
|
} else {
|
||||||
|
this.fields.splice(targetIndex, 0, this.dragField)
|
||||||
|
this.fields.splice(currentIndex + 1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
if (field.submitter_uuid === submitter.uuid) {
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
@@ -178,26 +217,6 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
move (field, direction) {
|
|
||||||
const currentIndex = this.submitterFields.indexOf(field)
|
|
||||||
const fieldsIndex = this.fields.indexOf(field)
|
|
||||||
|
|
||||||
this.fields.splice(fieldsIndex, 1)
|
|
||||||
|
|
||||||
if (currentIndex + direction > this.submitterFields.length) {
|
|
||||||
const firstIndex = this.fields.indexOf(this.submitterFields[0])
|
|
||||||
|
|
||||||
this.fields.splice(firstIndex, 0, field)
|
|
||||||
} else if (currentIndex + direction < 0) {
|
|
||||||
const lastIndex = this.fields.indexOf(this.submitterFields[this.submitterFields.length - 1])
|
|
||||||
|
|
||||||
this.fields.splice(lastIndex + 1, 0, field)
|
|
||||||
} else {
|
|
||||||
this.fields.splice(fieldsIndex + direction, 0, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
removeField (field) {
|
removeField (field) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
|
||||||
|
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[drawField.type]"
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
class="inline"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span> Draw {{ fieldNames[drawField.type] }} Field </span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link block text-center"
|
||||||
|
@click.prevent="$emit('cancel')"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<FieldSubmitter
|
||||||
|
:model-value="selectedSubmitter.uuid"
|
||||||
|
:submitters="submitters"
|
||||||
|
:editable="editable"
|
||||||
|
:mobile-view="true"
|
||||||
|
@new-submitter="save"
|
||||||
|
@remove="removeSubmitter"
|
||||||
|
@name-change="save"
|
||||||
|
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import FieldSubmitter from './field_submitter'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileDrawField',
|
||||||
|
components: {
|
||||||
|
Field,
|
||||||
|
FieldSubmitter
|
||||||
|
},
|
||||||
|
inject: ['save'],
|
||||||
|
props: {
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
submitters: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['change-submitter', 'cancel'],
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeSubmitter (submitter) {
|
||||||
|
[...this.fields].forEach((field) => {
|
||||||
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
|
this.removeField(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.submitters.splice(this.submitters.indexOf(submitter), 1)
|
||||||
|
|
||||||
|
if (this.selectedSubmitter === submitter) {
|
||||||
|
this.$emit('change-submitter', this.submitters[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeField (field) {
|
||||||
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,11 +5,12 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
|
loading="lazy"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
:width="width"
|
:width="width"
|
||||||
class="border rounded mb-4"
|
|
||||||
:height="height"
|
:height="height"
|
||||||
loading="lazy"
|
class="border rounded mb-4"
|
||||||
|
@load="onImageLoad"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:area="item.area"
|
:area="item.area"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
|
:editable="editable"
|
||||||
@start-resize="resizeDirection = $event"
|
@start-resize="resizeDirection = $event"
|
||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@start-drag="isMove = true"
|
@start-drag="isMove = true"
|
||||||
@@ -76,6 +78,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -117,6 +124,10 @@ export default {
|
|||||||
this.areaRefs = []
|
this.areaRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onImageLoad (e) {
|
||||||
|
e.target.setAttribute('width', e.target.naturalWidth)
|
||||||
|
e.target.setAttribute('height', e.target.naturalHeight)
|
||||||
|
},
|
||||||
setAreaRefs (el) {
|
setAreaRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.areaRefs.push(el)
|
this.areaRefs.push(el)
|
||||||
@@ -136,6 +147,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.showMask = true
|
this.showMask = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
||||||
@click="$emit('scroll-to', item)"
|
@click="$emit('scroll-to', item)"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between w-full">
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="flex justify-between w-full"
|
||||||
|
>
|
||||||
<div style="width: 26px" />
|
<div style="width: 26px" />
|
||||||
<div class="">
|
<div class="">
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
@@ -95,6 +98,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -114,7 +122,7 @@ export default {
|
|||||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
||||||
computed: {
|
computed: {
|
||||||
previewImage () {
|
previewImage () {
|
||||||
return this.document.preview_images[0]
|
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|||||||
@@ -10,19 +10,18 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||||
|
|
||||||
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless is_all_completed
|
return if Accounts.load_webhook_configs(submitter.account).blank?
|
||||||
return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at)
|
|
||||||
|
|
||||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|
||||||
enqueue_emails(submitter)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_emails(submitter)
|
def enqueue_completed_emails(submitter)
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = submitter.submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.template.account.users.exists?(id: user.id)
|
if submitter.template.account.users.exists?(id: user.id)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmitterMailer < ApplicationMailer
|
class SubmitterMailer < ApplicationMailer
|
||||||
|
MAX_ATTACHMENTS_SIZE = 10.megabytes
|
||||||
|
|
||||||
def invitation_email(submitter, body: nil, subject: nil)
|
def invitation_email(submitter, body: nil, subject: nil)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@@ -12,7 +14,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
if @email_config || subject.present?
|
if @email_config || subject.present?
|
||||||
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'You have been invited to submit a form'
|
'You are invited to submit a form'
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.friendly_name,
|
mail(to: @submitter.friendly_name,
|
||||||
@@ -62,7 +64,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
if @email_config
|
if @email_config
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'Your copy of documents'
|
'Your document copy'
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
@@ -75,13 +77,24 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
def add_completed_email_attachments!(submitter)
|
def add_completed_email_attachments!(submitter)
|
||||||
documents = Submitters.select_attachments_for_download(submitter)
|
documents = Submitters.select_attachments_for_download(submitter)
|
||||||
|
|
||||||
|
total_size = 0
|
||||||
|
audit_trail_data = nil
|
||||||
|
|
||||||
|
if submitter.submission.audit_trail.present?
|
||||||
|
audit_trail_data = submitter.submission.audit_trail.download
|
||||||
|
|
||||||
|
total_size = audit_trail_data.size
|
||||||
|
end
|
||||||
|
|
||||||
documents.each do |attachment|
|
documents.each do |attachment|
|
||||||
|
total_size += attachment.byte_size
|
||||||
|
|
||||||
|
break if total_size >= MAX_ATTACHMENTS_SIZE
|
||||||
|
|
||||||
attachments[attachment.filename.to_s] = attachment.download
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.submission.audit_trail.present?
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = submitter.submission.audit_trail.download
|
|
||||||
end
|
|
||||||
|
|
||||||
documents
|
documents
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
def invitation_email(user)
|
def invitation_email(user, invited_by: nil)
|
||||||
@current_account = user.account
|
@current_account = invited_by&.account || user.account
|
||||||
@user = user
|
@user = user
|
||||||
@token = @user.send(:set_reset_password_token)
|
@token = @user.send(:set_reset_password_token)
|
||||||
|
|
||||||
mail(to: @user.friendly_name,
|
mail(to: @user.friendly_name,
|
||||||
subject: 'You have been invited to Docuseal')
|
subject: 'You are invited to DocuSeal')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
# fk_rails_... (user_id => users.id)
|
# fk_rails_... (user_id => users.id)
|
||||||
#
|
#
|
||||||
class AccessToken < ApplicationRecord
|
class AccessToken < ApplicationRecord
|
||||||
TOKEN_LENGTH = 22
|
TOKEN_LENGTH = 43
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ class AccountConfig < ApplicationRecord
|
|||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
'subject' => 'You have been invited to submit a form',
|
'subject' => 'You are invited to submit a form',
|
||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
|
"You have been invited to submit the \"{{template.name}}\" form.\n\n" \
|
||||||
"{{submitter.link}}\n\n" \
|
"{{submitter.link}}\n\n" \
|
||||||
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
@@ -45,10 +45,10 @@ class AccountConfig < ApplicationRecord
|
|||||||
'{{submission.link}}'
|
'{{submission.link}}'
|
||||||
},
|
},
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
||||||
'subject' => 'Your copy of documents',
|
'subject' => 'Your document copy',
|
||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||||
"Alternatively, you can download the copy using:\n\n" \
|
"Alternatively, you can download your copy using:\n\n" \
|
||||||
"{{documents.links}}\n\n" \
|
"{{documents.links}}\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
'{{account.name}}'
|
'{{account.name}}'
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: encrypted_user_configs
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# key :string not null
|
||||||
|
# value :text not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_encrypted_user_configs_on_user_id (user_id)
|
||||||
|
# index_encrypted_user_configs_on_user_id_and_key (user_id,key) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
class EncryptedUserConfig < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
encrypts :value
|
||||||
|
|
||||||
|
serialize :value, JSON
|
||||||
|
end
|
||||||
@@ -48,6 +48,8 @@ class Submission < ApplicationRecord
|
|||||||
through: :template, source: :documents_attachments
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
|
||||||
|
scope :completed, -> { where.not(id: pending.select(:submission_id)) }
|
||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
@@ -60,4 +62,9 @@ class Submission < ApplicationRecord
|
|||||||
random: 'random',
|
random: 'random',
|
||||||
preserved: 'preserved'
|
preserved: 'preserved'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
|
def audit_trail_url
|
||||||
|
audit_trail&.url
|
||||||
|
end
|
||||||
|
alias audit_log_url audit_trail_url
|
||||||
end
|
end
|
||||||
+16
-15
@@ -4,21 +4,22 @@
|
|||||||
#
|
#
|
||||||
# Table name: submitters
|
# Table name: submitters
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# completed_at :datetime
|
# application_key :string
|
||||||
# email :string
|
# completed_at :datetime
|
||||||
# ip :string
|
# email :string
|
||||||
# name :string
|
# ip :string
|
||||||
# opened_at :datetime
|
# name :string
|
||||||
# phone :string
|
# opened_at :datetime
|
||||||
# sent_at :datetime
|
# phone :string
|
||||||
# slug :string not null
|
# sent_at :datetime
|
||||||
# ua :string
|
# slug :string not null
|
||||||
# uuid :string not null
|
# ua :string
|
||||||
# values :text not null
|
# uuid :string not null
|
||||||
# created_at :datetime not null
|
# values :text not null
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# submission_id :bigint not null
|
# updated_at :datetime not null
|
||||||
|
# submission_id :bigint not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
+14
-13
@@ -4,19 +4,20 @@
|
|||||||
#
|
#
|
||||||
# Table name: templates
|
# Table name: templates
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# application_key :string
|
||||||
# fields :text not null
|
# deleted_at :datetime
|
||||||
# name :string not null
|
# fields :text not null
|
||||||
# schema :text not null
|
# name :string not null
|
||||||
# slug :string not null
|
# schema :text not null
|
||||||
# source :text not null
|
# slug :string not null
|
||||||
# submitters :text not null
|
# source :text not null
|
||||||
# created_at :datetime not null
|
# submitters :text not null
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :bigint not null
|
# updated_at :datetime not null
|
||||||
# author_id :bigint not null
|
# account_id :bigint not null
|
||||||
# folder_id :bigint not null
|
# author_id :bigint not null
|
||||||
|
# folder_id :bigint not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class User < ApplicationRecord
|
|||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
has_many :user_configs, dependent: :destroy
|
has_many :user_configs, dependent: :destroy
|
||||||
|
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
||||||
|
|
||||||
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||||
@@ -75,6 +76,14 @@ class User < ApplicationRecord
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.sign_in_after_reset_password
|
||||||
|
if PasswordsController::Current.user.present?
|
||||||
|
!PasswordsController::Current.user.otp_required_for_login
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initials
|
def initials
|
||||||
[first_name&.first, last_name&.first].compact_blank.join.upcase
|
[first_name&.first, last_name&.first].compact_blank.join.upcase
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
<div class="flex items-center h-full">
|
<div class="flex items-center h-full">
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<div class="max-w-xl mx-auto">
|
<div class="max-w-xl mx-auto">
|
||||||
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to DocuSeal</h1>
|
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-lg mx-auto">
|
<div class="max-w-lg mx-auto">
|
||||||
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
|
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
|
||||||
|
|||||||
@@ -21,6 +21,5 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'devise/shared/links' %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,5 +12,4 @@
|
|||||||
<%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -41,6 +41,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<button id="talk_to_sales_button" class="hidden" type="submit" formaction="<%= enquiries_path %>" formnovalidate="true" formmethod="post"></button>
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" name="talk_to_sales" class="base-checkbox" onchange="window.talk_to_sales_button.click()">
|
||||||
|
<span class="label">I would like to talk to sales</span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="max-w-lg mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
<h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
|
||||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
<% if params[:redir].present? %>
|
<% if params[:redir].present? %>
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<% if devise_mapping.omniauthable? %>
|
||||||
<%= button_to button_title(title: 'Log in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
<%= button_to button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-lg mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
<h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
|
||||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
<%= f.hidden_field :email %>
|
<%= f.hidden_field :email %>
|
||||||
<%= f.hidden_field :password %>
|
<%= f.hidden_field :password %>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="flex justify-between mt-4">
|
<div class="flex justify-between mt-4">
|
||||||
<%- if controller_name != 'sessions' %>
|
<%- if controller_name != 'sessions' %>
|
||||||
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
|
<%= link_to 'Already have an account?', new_session_path(resource_name), class: 'link link-hover mx-auto' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||||
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
|
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
|
||||||
@@ -8,10 +8,4 @@
|
|||||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||||
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
|
||||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: 'link link-hover' %>
|
|
||||||
<% end %>
|
|
||||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
|
||||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
||||||
|
<path d="M12 7v5l3 3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 362 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M9 6l11 0" />
|
||||||
|
<path d="M9 12l11 0" />
|
||||||
|
<path d="M9 18l11 0" />
|
||||||
|
<path d="M5 6l0 .01" />
|
||||||
|
<path d="M5 12l0 .01" />
|
||||||
|
<path d="M5 18l0 .01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 440 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="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
|
||||||
|
<path d="M20 4v5h-5"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 409 B |
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-xl mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to Docuseal</h1>
|
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to <%= Docuseal.product_name %></h1>
|
||||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
<%= render 'devise/shared/error_messages', resource: %>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<%= javascript_pack_tag 'application', defer: true %>
|
<%= javascript_pack_tag 'application', defer: true %>
|
||||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||||
|
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<turbo-frame id="modal"></turbo-frame>
|
<turbo-frame id="modal"></turbo-frame>
|
||||||
|
|||||||
Whitespace-only changes.
@@ -21,6 +21,7 @@
|
|||||||
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= render 'email_configs' %>
|
||||||
<p class="text-2xl font-bold mt-8 mb-4">Signature</p>
|
<p class="text-2xl font-bold mt-8 mb-4">Signature</p>
|
||||||
<% signature = UserConfigs.load_signature(current_user) %>
|
<% signature = UserConfigs.load_signature(current_user) %>
|
||||||
<% if signature %>
|
<% if signature %>
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
@@ -1 +1 @@
|
|||||||
<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter] %>
|
<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path] %>
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
<div class="w-full text-center">
|
<div class="w-full text-center">
|
||||||
<span class="font-bold">Demo Environment</span>
|
<span class="font-bold">Demo Environment</span>
|
||||||
<br>
|
<br>
|
||||||
Feel free to
|
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or
|
||||||
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
|
|
||||||
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
|
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
---
|
---
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
Sent using <a href="<%= Docuseal::PRODUCT_URL %>/start"><%= Docuseal.product_name %></a> free document signing.
|
||||||
</p>
|
</p>
|
||||||
@@ -3,21 +3,22 @@
|
|||||||
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
|
||||||
<%= render 'shared/title' %>
|
<%= render 'shared/title' %>
|
||||||
</a>
|
</a>
|
||||||
<%= render 'shared/github' %>
|
<span class="hidden sm:inline">
|
||||||
|
<%= render 'shared/github' %>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? %>
|
<% if signed_in? %>
|
||||||
<div class="space-x-4 flex items-center">
|
<div class="space-x-4 flex items-center">
|
||||||
<% if Docuseal.demo? %>
|
<% if Docuseal.demo? %>
|
||||||
<%= render 'shared/github_button' %>
|
<a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
||||||
|
Sign Up
|
||||||
|
</a>
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
<%= render 'shared/github_button' %>
|
||||||
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center justify-center space-x-4 mr-1">
|
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||||
<% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
|
<%= render 'shared/navbar_buttons' %>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
|
|
||||||
Console
|
|
||||||
<span class="badge badge-warning ml-1">New</span>
|
|
||||||
<% end %>
|
|
||||||
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -58,18 +59,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex space-x-6">
|
<div class="flex space-x-2">
|
||||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
<% if request.path != new_user_session_path %>
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
|
||||||
<span>Sign in</span>
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
<% if Docuseal.multitenant? %>
|
|
||||||
<%= link_to registration_path, class: 'font-medium text-lg hidden md:inline' do %>
|
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<span class="flex items-center justify-center space-x-1">
|
||||||
<%= svg_icon('user_plus', class: 'w-6 h-6') %>
|
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||||
<span>Try for Free</span>
|
<span>Sign in</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
|
||||||
|
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
|
||||||
|
<span class="flex items-center justify-center space-x-1">
|
||||||
|
<span class="hidden sm:block">Create free Account</span>
|
||||||
|
<span class="md:hidden">Sign Up</span>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
|
||||||
|
Upgrade
|
||||||
|
<% end %>
|
||||||
|
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script defer data-domain="<%= ENV.fetch('PLAUSIBLE_DOMAIN', nil) %>" src="https://plausible.io/js/script.manual.js"></script>
|
||||||
|
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("turbo:load", function (e) {
|
||||||
|
if (e.detail.url.match(/sign_in|sign_up|password/)) {
|
||||||
|
plausible('pageview')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -9,5 +9,5 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
Powered by
|
Powered by
|
||||||
<% end %>
|
<% end %>
|
||||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
<a href="<%= Docuseal::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Docuseal.product_name %></a> - open source documents software
|
||||||
</div>
|
</div>
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex">
|
<form action="<%= url_for %>" method="get" class="items-center hidden md:flex">
|
||||||
<div class="relative">
|
<% if params[:status].present? %>
|
||||||
<% if params[:q].present? %>
|
<input name="status" value="<%= params[:status] %>" class="hidden">
|
||||||
<a href="<%= request.path %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
<% end %>
|
||||||
|
<% if params[:q].present? %>
|
||||||
|
<div class="relative">
|
||||||
|
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||||
×
|
×
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
<% 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? %>">
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<div class="block w-full md:w-52 flex-none">
|
<div class="block w-full md:w-52 flex-none">
|
||||||
<menu-active>
|
<menu-active>
|
||||||
<ul class="menu px-0">
|
<ul class="menu px-0">
|
||||||
<li class="menu-title py-0"><span>Settings</span></li>
|
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">← Back</a></li>
|
||||||
|
<li class="menu-title py-0 !bg-transparent"><span class="!bg-transparent">Settings</span></li>
|
||||||
<li></li>
|
<li></li>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
@@ -41,6 +42,13 @@
|
|||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Team', 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)) %>
|
||||||
|
<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>
|
||||||
@@ -61,12 +69,19 @@
|
|||||||
<%= render 'shared/settings_nav_extra' %>
|
<%= render 'shared/settings_nav_extra' %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
Console
|
Plans
|
||||||
<span class="badge badge-warning">New</span>
|
<span class="badge badge-warning">New</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
|
Console
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</menu-active>
|
</menu-active>
|
||||||
<% if !can?(:manage, :tenants) %>
|
<% if !can?(:manage, :tenants) %>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="alert">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Single Sign On with SAML 2.0</p>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Unlock with DocuSeal Enterprise
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">SMS</h1>
|
||||||
|
<%= render 'placeholder' %>
|
||||||
|
</div>
|
||||||
|
<div class="w-0 md:w-52"></div>
|
||||||
|
</div>
|
||||||
@@ -24,6 +24,9 @@
|
|||||||
<%= button_to button_title(title: 'Send copy to Email', disabled_with: '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: 'Send copy to Email', disabled_with: '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>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
@@ -24,10 +24,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% unless @template.deleted_at? %>
|
<% unless @template.deleted_at? %>
|
||||||
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
|
||||||
<div class="form-control !mt-0">
|
<div class="form-control !mt-0">
|
||||||
<%= f.label :email, class: 'label' %>
|
<%= f.label :email, class: 'label' %>
|
||||||
<%= f.email_field :email, value: current_user&.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
|
<%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
|
||||||
@@ -36,4 +36,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
Whitespace-only changes.
@@ -41,6 +41,7 @@
|
|||||||
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %>
|
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %>
|
||||||
</autoresize-textarea>
|
</autoresize-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'message_fields' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,8 +28,6 @@
|
|||||||
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
||||||
<%= Array.wrap(value).join(', ') %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,4 +23,5 @@
|
|||||||
<%= render 'detailed_form', template: @template %>
|
<%= render 'detailed_form', template: @template %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= content_for(:modal_extra) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -44,12 +44,16 @@
|
|||||||
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
||||||
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
|
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
||||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||||
<% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
<% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
|
<% lazyload_metadata = document.preview_images.first.metadata %>
|
||||||
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>" loading="lazy">
|
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||||
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||||
<span><%= Docuseal::PRODUCT_NAME %></span>
|
<span><%= Docuseal.product_name %></span>
|
||||||
</a>
|
</a>
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
@@ -19,7 +19,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
|
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
|
||||||
<%= button_to button_title(title: 'Send copy to Email', disabled_with: '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: 'Send copy to Email', disabled_with: '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' %>
|
||||||
<div class="divider">OR</div>
|
<% if @submitter.submission.template.submitters.size != 1 %>
|
||||||
|
<div class="divider">OR</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="py-2"></div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
@@ -32,6 +36,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</download-button>
|
</download-button>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @submitter.submission.template.submitters.size == 1 %>
|
||||||
|
<div class="divider">OR</div>
|
||||||
|
<div>
|
||||||
|
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
||||||
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
|
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
@@ -10,23 +11,27 @@
|
|||||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||||
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
|
<% lazyload_metadata = document.preview_images.last.metadata %>
|
||||||
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
||||||
<div class="relative my-4 shadow-md">
|
<div class="relative my-4 shadow-md">
|
||||||
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
|
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= 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']] %>
|
<% 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) %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/attribution' %>
|
<%= render 'shared/attribution', link_path: '/start' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
<p>Alternatively, you can download the copy using:</p>
|
<p>Alternatively, you can download your copy using:</p>
|
||||||
<% @documents.each do |document| %>
|
<% @documents.each do |document| %>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form</p>
|
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form.</p>
|
||||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -52,9 +52,9 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !template.deleted_at? && can?(:destroy, template) %>
|
<% if (!template.deleted_at? || !Docuseal.multitenant?) && can?(:destroy, template) %>
|
||||||
<span class="tooltip tooltip-left" data-tip="Archive">
|
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Remove' : 'Archive' %>">
|
||||||
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
||||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
|
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
<%= f.text_field :name, required: true, placeholder: 'Template Name', class: 'base-input' %>
|
<%= f.text_field :name, required: true, placeholder: 'Document Template Name', class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 mb-4 flex items-center justify-between">
|
<div class="mt-3 mb-4 flex items-center justify-between">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
|
||||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
</a>
|
</a>
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
<input id="folder_name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
<input id="folder_name" placeholder="Folder Name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5">
|
||||||
Change Folder
|
Change Folder
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<%= render 'title', template: @template %>
|
<%= render 'title', template: @template %>
|
||||||
|
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
|
||||||
<% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 || params[:q].present? %>
|
<% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 || params[:q].present? %>
|
||||||
<div class="flex justify-between mb-6 md:items-end flex-col md:flex-row">
|
<div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
|
||||||
<p class="text-3xl font-bold">Submissions</p>
|
<p class="text-3xl font-bold">Submissions</p>
|
||||||
<div class="flex space-x-2 mt-3 md:mt-0">
|
<div class="flex md:space-x-2 mt-3 md:mt-0">
|
||||||
<% if params[:q].present? || @pagy.pages > 1 %>
|
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
|
||||||
<%= render 'shared/search_input' %>
|
<%= render 'shared/search_input' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
|
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
|
||||||
@@ -24,6 +25,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
<div class="badge badge-neutral badge-outline font-medium">
|
||||||
|
<%= params[:status].blank? ? @pagy.count : @base_submissions.count %>
|
||||||
|
</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>
|
||||||
|
<div class="badge badge-neutral badge-outline font-medium">
|
||||||
|
<%= params[:status] == 'pending' ? @pagy.count : @base_submissions.pending.count %>
|
||||||
|
</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>
|
||||||
|
<div class="badge badge-neutral badge-outline font-medium">
|
||||||
|
<%= params[:status] == 'completed' ? @pagy.count : @base_submissions.completed.count %>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if @submissions.present? %>
|
<% if @submissions.present? %>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<%= render partial: 'submission', collection: @submissions %>
|
<%= render partial: 'submission', collection: @submissions %>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="h-screen">
|
||||||
|
<div
|
||||||
|
class="text-center p-8 h-full flex items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %>
|
||||||
|
<span>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<submit-form>
|
||||||
|
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %>
|
||||||
|
<button type="submit"></button>
|
||||||
|
<input name="url" value="<%= params[:url] %>">
|
||||||
|
<input name="filename" value="<%= params[:filename] %>">
|
||||||
|
<% end %>
|
||||||
|
</submit-form>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p>Hello <%= @user.first_name %>,</p>
|
<p>Hello <%= @user.first_name %>,</p>
|
||||||
<p>You have been invited to <%= @user.account.name %> DocuSeal. Please sign up using the link below:</p>
|
<p>You have been invited to <%= @user.account.name %> <%= Docuseal.product_name %>. Please sign up using the link below:</p>
|
||||||
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user