Compare commits

..
77 Commits
Author SHA1 Message Date
Pete Matsyburka e6785b0afd fix sign in form without oauth 2024-01-22 09:32:19 +02:00
Pete Matsyburka 009033a411 fix params validation 2024-01-22 09:32:18 +02:00
Alex Turchyn fdc3d7742a fix recipients switch 2024-01-21 18:07:29 +02:00
Alex Turchyn f9ccca1376 add base API error messages 2024-01-21 15:19:16 +02:00
Alex Turchyn 0a00fe6ab8 fix minor mobile UI issues 2024-01-21 13:36:58 +02:00
Pete Matsyburka 6253e80a70 add testing environment 2024-01-21 11:20:34 +02:00
Pete Matsyburka 3dcb7c813f fix rspec 2024-01-19 12:45:43 +02:00
Pete Matsyburka 39cdc41a67 fix erblint 2024-01-19 12:41:36 +02:00
Pete Matsyburka d3000487f4 prevent builder submitter click # nav 2024-01-19 10:15:21 +02:00
Pete Matsyburka 80254da1c1 adjust consider json middleware 2024-01-19 09:54:49 +02:00
Pete Matsyburka 73fc2d92f7 fix annotations 2024-01-19 01:15:13 +02:00
Pete Matsyburka f69aa33f3d i18n builder confirm 2024-01-18 21:12:08 +02:00
Pete Matsyburka e16202c9f7 ajust 2fa message 2024-01-18 10:33:25 +02:00
Pete Matsyburka 7453d0d274 reduce image size 2024-01-17 22:22:50 +02:00
Pete Matsyburka c102828c93 fix manual send button 2024-01-17 13:52:38 +02:00
Alex Turchyn afeb6dd318 optimize submissions show page for mobile devices 2024-01-17 01:45:44 +02:00
Pete Matsyburka 6156623f0e remove welcome message 2024-01-17 01:45:44 +02:00
Pete Matsyburka ccc06fcc3d allow to remove template permanently 2024-01-17 01:45:40 +02:00
Pete Matsyburka bbe3942056 validate sender email 2024-01-16 23:46:41 +02:00
Pete Matsyburka 49cea59b94 do not authenticate archived users 2024-01-16 00:18:58 +02:00
Pete Matsyburka ce1323e849 sign in-person button 2024-01-15 17:43:27 +02:00
Pete Matsyburka fc3c1e65e3 reduce image size 2024-01-15 02:30:10 +02:00
Pete Matsyburka f1ebd8f912 text utils for RTL 2024-01-15 02:22:53 +02:00
Pete Matsyburka 98f5bba832 update pdf processor 2024-01-15 02:22:53 +02:00
Alex Turchyn e864e8a47f process password protected pdf files 2024-01-15 02:22:40 +02:00
Alex Turchyn a573545ead fix right-to-left text direction for legacy Outlook clients 2024-01-15 00:02:44 +02:00
Pete Matsyburka 15ff4386e5 remove via linkk tab if 2+ parties 2024-01-14 14:09:23 +02:00
Pete Matsyburka 7a4aa3bf87 fix success page 2024-01-14 12:49:54 +02:00
Pete Matsyburka c1ba6288c8 audit log toggle in documents email 2024-01-14 12:38:08 +02:00
Pete Matsyburka d0ba180729 add submitter completed redirect url 2024-01-14 12:11:13 +02:00
Pete Matsyburka 2edc76414b do not show archived submission 2024-01-14 11:11:02 +02:00
Pete Matsyburka 452db5dc39 allow to disable audit log PDF attachment 2024-01-14 01:59:13 +02:00
Pete Matsyburka 21c4c53feb update readme docker compose command 2024-01-14 01:11:16 +02:00
Pete Matsyburka 6292789cac return error if template is blank 2024-01-14 00:49:07 +02:00
Pete Matsyburka d445bbc02d fix accepted builder field types 2024-01-14 00:43:14 +02:00
Pete Matsyburka 8627f58cbc add via link recipients tab 2024-01-14 00:43:13 +02:00
Pete Matsyburka 66ab0059bb accept field title and description 2024-01-13 13:52:58 +02:00
Pete Matsyburka 3f06932277 send bcc_completed as multiple to 2024-01-12 23:53:42 +02:00
Pete Matsyburka 59cf1b435e enable_starttls_auto true when using ENV 2024-01-12 22:24:03 +02:00
Pete Matsyburka c9a5b0e66e add submitter slug variable 2024-01-12 18:22:09 +02:00
Pete Matsyburka bcd5e1da98 fix auth token rotate 2024-01-12 09:45:40 +02:00
Pete Matsyburka f74ad050b4 add field types prop 2024-01-10 23:18:30 +02:00
Pete Matsyburka b6f43c9b23 add onSave builder event prop 2024-01-10 17:54:35 +02:00
Pete Matsyburka 298c64a119 fix RTL reverse 2024-01-10 10:28:06 +02:00
Pete Matsyburka 8b80679b3a add bcc completed submission param 2024-01-10 02:06:31 +02:00
Pete Matsyburka 82a2bc561a remove talk to sales checkbox 2024-01-09 11:31:13 +02:00
Pete Matsyburka e019542d3f fix template 2024-01-09 10:49:51 +02:00
Pete Matsyburka 9356247ef0 add signer user forms translations 2024-01-09 01:32:39 +02:00
Pete Matsyburka 4db77c18e3 add Hebrew form translations 2024-01-08 20:18:57 +02:00
Pete Matsyburka 0d0b248988 use arabic letter connector 2024-01-08 19:32:20 +02:00
Pete Matsyburka 44f3910266 fix build 2024-01-07 18:18:24 +02:00
Pete Matsyburka 35c6798a80 fix RTL form area 2024-01-07 15:09:36 +02:00
Pete Matsyburka 0995c7ee63 adjust RTL regexp 2024-01-07 14:52:10 +02:00
Pete Matsyburka 0a25f78329 touch .version 2024-01-07 14:31:10 +02:00
Pete Matsyburka bb8fffc491 link to releases page 2024-01-07 14:25:38 +02:00
Alex Turchyn 6f6c51fb32 add tooltips to personalize emails 2024-01-07 14:22:43 +02:00
Alex Turchyn 39d14c15d6 add display of app version in UI 2024-01-07 14:20:34 +02:00
Pete Matsyburka 17de28965b add LTR script support 2024-01-07 14:19:21 +02:00
Pete Matsyburka ae7865e508 add reorder parties button 2024-01-06 22:25:13 +02:00
Pete Matsyburka dc8e8cc5c9 draw field on field type click 2024-01-06 21:16:27 +02:00
Pete Matsyburka 5208470c38 make current field area more visible 2024-01-06 19:55:20 +02:00
Pete Matsyburka e7ef7b552f show field area pages list inthe form 2024-01-06 17:21:15 +02:00
Pete Matsyburka a478112b07 fix multiple area fields draw 2024-01-06 16:19:40 +02:00
Pete Matsyburka 7d0f32e1f9 handle multiple checkboxes required 2024-01-06 15:38:03 +02:00
Pete Matsyburka d35d8bead3 support checkbox option without name 2024-01-06 15:05:15 +02:00
Pete Matsyburka ab9cc52bcc add rotate api token button 2024-01-06 12:34:20 +02:00
Pete Matsyburka 7dc342a6f9 make it possible to remove default signature 2024-01-05 22:48:55 +02:00
Pete Matsyburka 80a3ef9193 add submission URL to webhooks serializer 2024-01-05 20:27:38 +02:00
Pete Matsyburka bb723d20cb add builder props 2024-01-05 18:40:45 +02:00
Pete Matsyburka 0c24a2016c use Pro instead of Enterprise 2024-01-04 15:28:46 +02:00
Pete Matsyburka 2124965996 add builder i18n 2024-01-04 01:44:16 +02:00
Pete Matsyburka 6f1e104067 add builder autosave prop 2024-01-04 01:44:11 +02:00
Pete Matsyburka b20f40b5a3 fix webhook data fields order 2024-01-03 20:43:47 +02:00
Pete Matsyburka 1093ca5fc4 add md breakpoint prop to form 2024-01-03 18:19:44 +02:00
Pete Matsyburka 0d99c55d3f disable pdf validate on write 2024-01-02 22:59:34 +02:00
Pete Matsyburka 86dc1a88ed Revert "do not use puma pidfile"
This reverts commit 8961294a29.
2024-01-02 22:39:23 +02:00
Pete Matsyburka 968c20d8ca completed email bcc sending via to address 2024-01-02 11:41:28 +02:00
137 changed files with 2201 additions and 507 deletions

No files matched your search

+2 -1
View File
@@ -8,7 +8,8 @@
}, },
"rules": { "rules": {
"vue/no-deprecated-html-element-is": 0, "vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0 "vue/no-mutating-props": 0,
"vue/no-v-html": 0
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2022, "ecmaVersion": 2022,
+3
View File
@@ -30,6 +30,9 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
View File
Whitespace-only changes.
+12 -3
View File
@@ -1,3 +1,11 @@
FROM ruby:3.2.2-alpine3.18 as fonts
WORKDIR /fonts
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
FROM ruby:3.2.2-alpine3.18 as webpack FROM ruby:3.2.2-alpine3.18 as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
@@ -31,11 +39,11 @@ 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 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 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 && mkdir /fonts
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
COPY ./bin ./bin COPY ./bin ./bin
COPY ./app ./app COPY ./app ./app
@@ -45,8 +53,9 @@ COPY ./log ./log
COPY ./lib ./lib COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts RUN ln -s /fonts /app/public/fonts
+3
View File
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
ruby '3.2.2' ruby '3.2.2'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false gem 'bootsnap', require: false
@@ -25,6 +26,7 @@ gem 'pagy'
gem 'pdf-reader' gem 'pdf-reader'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'pretender'
gem 'puma' gem 'puma'
gem 'rack' gem 'rack'
gem 'rails' gem 'rails'
@@ -40,6 +42,7 @@ gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false gem 'sqlite3', require: false
gem 'strip_attributes' gem 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
group :development, :test do group :development, :test do
+12
View File
@@ -82,6 +82,7 @@ GEM
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.874.0) aws-partitions (1.874.0)
@@ -124,6 +125,7 @@ GEM
bullet (7.1.4) bullet (7.1.4)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
cancancan (3.5.0) cancancan (3.5.0)
capybara (3.39.2) capybara (3.39.2)
addressable addressable
@@ -135,6 +137,7 @@ GEM
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.2)
@@ -351,6 +354,8 @@ GEM
actionmailer (>= 3) actionmailer (>= 3)
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pry (0.14.2) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
@@ -526,6 +531,10 @@ GEM
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.0)
camertron-eprun
cldr-plurals-runtime-rb (~> 1.1)
tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4) tzinfo-data (1.2023.4)
@@ -558,6 +567,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
annotate annotate
arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
azure-storage-blob azure-storage-blob
better_html better_html
@@ -588,6 +598,7 @@ DEPENDENCIES
pdf-reader pdf-reader
pg pg
premailer-rails premailer-rails
pretender
pry-rails pry-rails
puma puma
rack rack
@@ -610,6 +621,7 @@ DEPENDENCIES
sqlite3 sqlite3
strip_attributes strip_attributes
turbo-rails turbo-rails
twitter_cldr
tzinfo-data tzinfo-data
web-console web-console
webmock webmock
+1 -1
View File
@@ -73,7 +73,7 @@ curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy): Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
```sh ```sh
HOST=your-domain-name.com docker-compose up sudo HOST=your-domain-name.com docker compose up
``` ```
## For Businesses ## For Businesses
+7 -1
View File
@@ -8,11 +8,17 @@ module Api
DEFAULT_LIMIT = 10 DEFAULT_LIMIT = 10
MAX_LIMIT = 100 MAX_LIMIT = 100
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
wrap_parameters false wrap_parameters false
before_action :authenticate_user! before_action :authenticate_user!
check_authorization check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
end
if Rails.env.production? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -44,7 +50,7 @@ module Api
if request.headers['X-Auth-Token'].present? if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token']) sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).find_by(access_token: { sha256: }) User.joins(:access_token).active.find_by(access_token: { sha256: })
end end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
+12 -3
View File
@@ -63,8 +63,16 @@ module Api
end end
def create def create
Params::SubmissionCreateValidator.call(params, dry_run: true)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank?
Rollbar.error("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
end
params[:send_email] = true unless params.key?(:send_email) params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms) params[:send_sms] = false unless params.key?(:send_sms)
@@ -136,12 +144,13 @@ module Api
params.permit( params.permit(
key => [ key => [
[:send_email, :send_sms, { [:send_email, :send_sms, :bcc_completed, :completed_redirect_url, {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body], { values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}] }]
] ]
).fetch(key, []) ).fetch(key, [])
+5 -1
View File
@@ -38,7 +38,11 @@ module Api
end end
def destroy def destroy
@template.update!(archived_at: Time.current) if params[:permanently] == 'true' && !Docuseal.multitenant?
@template.destroy!
else
@template.update!(archived_at: Time.current)
end
render json: @template.as_json(only: %i[id archived_at]) render json: @template.as_json(only: %i[id archived_at])
end end
@@ -22,6 +22,8 @@ module Api
} }
) )
} }
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end end
end end
end end
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
def index def index
authorize!(:read, current_user.access_token) authorize!(:read, current_user.access_token)
end end
def create
authorize!(:manage, current_user.access_token)
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
end
end end
+29 -1
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
include ActiveStorage::SetCurrent include ActiveStorage::SetCurrent
include Pagy::Backend include Pagy::Backend
@@ -14,6 +16,8 @@ class ApplicationController < ActionController::Base
:current_account, :current_account,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do rescue_from Pagy::OverflowError do
redirect_to request.path redirect_to request.path
end end
@@ -22,7 +26,7 @@ class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
redirect_back(fallback_location: root_path, alert: e.message) redirect_to root_path, alert: e.message
end end
end end
@@ -30,8 +34,32 @@ class ApplicationController < ActionController::Base
Docuseal.default_url_options Docuseal.default_url_options
end end
def impersonate_user(user)
raise ArgumentError unless user
raise Pretender::Error unless true_user
@impersonated_user = user
request.session[:impersonated_user_id] = user.uuid
end
private private
def with_browser_locale(&)
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
locale =
if locale.starts_with?('en-') && locale != 'en-US'
'en-GB'
else
locale.split('-').first.presence || 'en-GB'
end
locale = 'en-GB' unless I18n.locale_available?(locale)
I18n.with_locale(locale, &)
end
def sign_in_for_demo def sign_in_for_demo
sign_in(User.active.order('random()').take) unless signed_in? sign_in(User.active.order('random()').take) unless signed_in?
end end
@@ -5,9 +5,9 @@ class ConsoleRedirectController < ApplicationController
skip_authorization_check skip_authorization_check
def index def index
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank? return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: current_user.uuid, auth = JsonWebToken.encode(uuid: true_user.uuid,
scope: :console, scope: :console,
exp: 1.minute.from_now.to_i) exp: 1.minute.from_now.to_i)
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing' 'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/templates/html', '/templates/html',
@@ -7,18 +7,28 @@ class PersonalizationSettingsController < ApplicationController
def create def create
account_config = account_config =
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key]) current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
authorize!(:create, account_config) authorize!(:create, account_config)
account_config.update!(encrypted_config_params) account_config.update!(account_config_params)
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.') redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end end
private private
def encrypted_config_params def account_config_params
params.require(:account_config).permit! attrs = params.require(:account_config).permit!
attrs[:value]&.transform_values! do |value|
if value.in?(%w[true false])
value == 'true'
else
value
end
end
attrs
end end
end end
+1
View File
@@ -6,6 +6,7 @@ class StartFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
before_action :load_template before_action :load_template
def show def show
+4 -1
View File
@@ -3,6 +3,7 @@
class SubmitFormController < ApplicationController class SubmitFormController < ApplicationController
layout 'form' layout 'form'
around_action :with_browser_locale, only: %i[show completed success]
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
@@ -32,7 +33,7 @@ class SubmitFormController < ApplicationController
cookies[:submitter_sid] = @submitter.signed_id cookies[:submitter_sid] = @submitter.signed_id
render @submitter.submission.template.archived_at? ? :archived : :show render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
end end
def update def update
@@ -58,4 +59,6 @@ class SubmitFormController < ApplicationController
def completed def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end end
def success; end
end end
@@ -11,9 +11,7 @@ class SubmittersSendEmailController < ApplicationController
alert: 'Email has been sent already.') alert: 'Email has been sent already.')
end end
SubmitterMailer.invitation_email(@submitter).deliver_later! SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
@submitter.sent_at ||= Time.current @submitter.sent_at ||= Time.current
@submitter.save! @submitter.save!
@@ -10,12 +10,7 @@ class TemplatesUploadsController < ApplicationController
def create def create
url_params = create_file_params_from_url if params[:url].present? url_params = create_file_params_from_url if params[:url].present?
@template.account = current_account save_template!(@template, url_params)
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
@template.save!
documents = Templates::CreateAttachments.call(@template, url_params || params) documents = Templates::CreateAttachments.call(@template, url_params || params)
@@ -24,6 +19,8 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
rescue StandardError => e rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -32,6 +29,17 @@ class TemplatesUploadsController < ApplicationController
private private
def save_template!(template, url_params)
template.account = current_account
template.author = current_user
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
template.save!
template
end
def create_file_params_from_url def create_file_params_from_url
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class TestingAccountsController < ApplicationController
skip_authorization_check only: :destroy
def show
authorize!(:manage, current_account)
authorize!(:manage, current_user)
impersonate_user(Accounts.find_or_create_testing_user(current_account))
redirect_back(fallback_location: root_path)
end
def destroy
stop_impersonating_user
redirect_back(fallback_location: root_path)
end
end
@@ -0,0 +1,12 @@
# frozen_string_literal: true
class TestingApiSettingsController < ApplicationController
def index
authorize!(:manage, current_user.access_token)
@webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_config)
end
end
@@ -28,6 +28,12 @@ class UserSignaturesController < ApplicationController
end end
end end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
end
private private
def load_user_config def load_user_config
+2
View File
@@ -19,6 +19,7 @@ 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 SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -51,6 +52,7 @@ 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) window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword)
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) => {
@@ -32,6 +32,8 @@ export default class extends HTMLElement {
render = (item) => { render = (item) => {
const div = document.createElement('div') const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item.name div.textContent = item.name
return div return div
@@ -0,0 +1,19 @@
export default class extends HTMLElement {
connectedCallback () {
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'password'
input.value = prompt('Enter PDF password')
this.form.append(input)
this.form.requestSubmit()
this.remove()
}
get form () {
return this.closest('form')
}
}
@@ -56,6 +56,8 @@ export default class extends HTMLElement {
render = (item) => { render = (item) => {
const div = document.createElement('div') const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item[this.dataset.field] div.textContent = item[this.dataset.field]
return div return div
+1
View File
@@ -19,6 +19,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton), completedButton: JSON.parse(this.dataset.completedButton),
completedRedirectUrl: this.dataset.completedRedirectUrl,
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields) fields: JSON.parse(this.dataset.fields)
}) })
@@ -0,0 +1,52 @@
<template>
<div
v-if="uniqueAreas.length > 1"
dir="auto"
class="mb-2"
>
<div class="flex space-x-1 text-sm ml-1 -mt-3">
<span>
{{ t('appears_on') }}:
</span>
<a
v-for="area in uniqueAreas"
:key="area.uuid"
href="#"
class="link pr-0.5"
@click.prevent="scrollIntoArea(area)"
>
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'AppearsOn',
inject: ['t', 'scrollIntoArea'],
props: {
field: {
type: Object,
required: true
}
},
computed: {
isMultipleDocs () {
return this.attachmentUuids.length > 1
},
attachmentUuids () {
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
},
uniqueAreas () {
const areas = {}
this.field.areas?.forEach((area) => {
areas[area.attachment_uuid + area.page] ||= area
})
return Object.values(areas).slice(0, 6)
}
}
}
</script>
+37 -15
View File
@@ -1,8 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base" class="flex absolute lg:text-base"
dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed border-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -21,15 +22,20 @@
</span> </span>
</div> </div>
<div <div
v-if="isActive && withLabel && !area.option_uuid" v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
> >
{{ field.name || fieldNames[field.type] }} <template v-if="area.option_uuid && !option.value">
<template v-if="field.type === 'checkbox' && !field.name"> {{ optionValue(option) }}
{{ fieldIndex + 1 }}
</template> </template>
<template v-else-if="!field.required && field.type !== 'checkbox'"> <template v-else>
(optional) {{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
</template>
</template> </template>
</div> </div>
<div <div
@@ -102,11 +108,11 @@
:value="false" :value="false"
class="aspect-square base-radio" class="aspect-square base-radio"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" :checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)" @click="$emit('update:model-value', optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" v-else-if="!!modelValue && modelValue === optionValue(option)"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -121,11 +127,11 @@
:value="false" :value="false"
class="aspect-square base-checkbox" class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" :checked="!!modelValue && modelValue.includes(optionValue(option))"
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)" @change="updateMultipleSelectValue(optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -146,7 +152,8 @@
<div <div
v-else v-else
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" dir="auto"
class="flex items-center px-0.5 w-full"
> >
<span v-if="Array.isArray(modelValue)"> <span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }} {{ modelValue.join(', ') }}
@@ -171,6 +178,7 @@ export default {
IconPaperclip, IconPaperclip,
IconCheck IconCheck
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -241,6 +249,9 @@ export default {
payment: 'Payment' payment: 'Payment'
} }
}, },
option () {
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,
@@ -335,6 +346,17 @@ export default {
} }
}, },
methods: { methods: {
optionValue (option) {
if (option) {
if (option.value) {
return option.value
} else {
const index = this.field.options.indexOf(option)
return `${this.t('option')} ${index + 1}`
}
}
},
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { const monthFormats = {
M: 'numeric', M: 'numeric',
+15 -12
View File
@@ -83,22 +83,25 @@ export default {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`) return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
}, },
scrollIntoField (field) { scrollIntoField (field) {
this.areaRefs.find((area) => { if (field?.areas) {
if (area.field === field) { this.scrollIntoArea(field.areas[0])
const root = this.$root.$el.parentNode.getRootNode() }
const container = root.body || root.querySelector('div') },
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
if (container.style.overflow === 'hidden') { if (areaRef) {
this.scrollInContainer(area.$el) const root = this.$root.$el.parentNode.getRootNode()
} else { const container = root.body || root.querySelector('div')
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
return true if (container.style.overflow === 'hidden') {
this.scrollInContainer(areaRef.$el)
} else { } else {
return null areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
} }
})
return true
}
}, },
scrollInContainer (target) { scrollInContainer (target) {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
+4 -1
View File
@@ -1,5 +1,8 @@
<template> <template>
<div class="mx-auto max-w-md flex flex-col"> <div
class="mx-auto max-w-md flex flex-col"
dir="auto"
>
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto"> <p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck <IconCircleCheck
class="inline text-green-600" class="inline text-green-600"
+5 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
:for="field.uuid" :for="field.uuid"
@@ -15,6 +15,7 @@
{{ t('set_today') }} {{ t('set_today') }}
</button> </button>
</div> </div>
<AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
ref="input" ref="input"
@@ -31,11 +32,13 @@
<script> <script>
import { IconCalendarCheck } from '@tabler/icons-vue' import { IconCalendarCheck } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'DateStep', name: 'DateStep',
components: { components: {
IconCalendarCheck IconCalendarCheck,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
+59 -23
View File
@@ -24,7 +24,8 @@
<div <div
v-show="isFormVisible" v-show="isFormVisible"
id="form_container" id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded" class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<button <button
@@ -45,7 +46,7 @@
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
class="md:mx-16" :class="{ 'md:mx-16': isBreakpointMd }"
@submit.prevent="submitStep" @submit.prevent="submitStep"
> >
<input <input
@@ -64,7 +65,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
</div> </div>
<DateStep <DateStep
@@ -72,12 +73,13 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="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"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -86,13 +88,15 @@
v-else v-else
class="py-1" class="py-1"
/> />
<AppearsOn :field="currentField" />
<select <select
:id="currentField.uuid" :id="currentField.uuid"
dir="auto"
:required="currentField.required" :required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
> >
<option <option
value="" value=""
@@ -114,6 +118,7 @@
<label <label
v-if="currentField.name" v-if="currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -121,7 +126,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="option in currentField.options" v-for="(option, index) in currentField.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -134,11 +139,11 @@
type="radio" type="radio"
class="base-radio !h-7 !w-7" class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
:value="option.value" :value="option.value || `${t('option')} ${index + 1}`"
:required="currentField.required" :required="currentField.required"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ option.value || `${t('option')} ${index + 1}` }}
</span> </span>
</label> </label>
</div> </div>
@@ -197,9 +202,17 @@
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`" :onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required" :required="field.required"
:checked="!!values[field.uuid]" :checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]" @click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
v-if="field.title"
class="text-xl"
v-html="field.title"
/>
<span
v-else
class="text-xl"
> >
<span class="text-xl">
{{ field.name || field.type + ' ' + (index + 1) }} {{ field.name || field.type + ' ' + (index + 1) }}
</span> </span>
</label> </label>
@@ -215,7 +228,7 @@
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<SignatureStep <SignatureStep
v-else-if="currentField.type === 'signature'" v-else-if="currentField.type === 'signature'"
@@ -229,7 +242,7 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<InitialsStep <InitialsStep
@@ -243,8 +256,8 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<AttachmentStep <AttachmentStep
@@ -255,7 +268,7 @@
:field="currentField" :field="currentField"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<PhoneStep <PhoneStep
v-else-if="currentField.type === 'phone'" v-else-if="currentField.type === 'phone'"
@@ -265,7 +278,7 @@
:field="currentField" :field="currentField"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
<PaymentStep <PaymentStep
@@ -276,7 +289,7 @@
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
</div> </div>
@@ -319,9 +332,9 @@
v-else v-else
:is-demo="isDemo" :is-demo="isDemo"
:attribution="attribution" :attribution="attribution"
:completed-button="completedButton" :completed-button="completedRedirectUrl ? {} : completedButton"
:with-send-copy-button="withSendCopyButton" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
:with-download-button="withDownloadButton" :with-download-button="withDownloadButton && !completedRedirectUrl"
:with-confetti="withConfetti" :with-confetti="withConfetti"
:can-send-email="canSendEmail && !!submitter.email" :can-send-email="canSendEmail && !!submitter.email"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -362,6 +375,7 @@ import TextStep from './text_step'
import DateStep from './date_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 AppearsOn from './appears_on'
import { t } from './i18n' import { t } from './i18n'
export default { export default {
@@ -370,6 +384,7 @@ export default {
FieldAreas, FieldAreas,
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AppearsOn,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
@@ -385,6 +400,7 @@ export default {
provide () { provide () {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea,
t: this.t t: this.t
} }
}, },
@@ -420,6 +436,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
isBreakpointMd: {
type: Boolean,
required: false,
default: true
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@@ -475,6 +496,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
completedRedirectUrl: {
type: String,
required: false,
default: ''
},
completedButton: { completedButton: {
type: Object, type: Object,
required: false, required: false,
@@ -521,7 +547,7 @@ export default {
isButtonDisabled () { isButtonDisabled () {
if (this.recalculateButtonDisabledKey) { if (this.recalculateButtonDisabledKey) {
return this.isSubmitting || return this.isSubmitting ||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) || (this.currentField.required && ['image', 'file', 'multiple'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) || (this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted) (this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
} else { } else {
@@ -681,7 +707,7 @@ export default {
this.recalculateButtonDisabledKey = Math.random() this.recalculateButtonDisabledKey = Math.random()
if (scrollToArea) { if (scrollToArea) {
this.$refs.areas.scrollIntoField(step[0]) this.scrollIntoField(step[0])
} }
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus() this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
@@ -709,6 +735,12 @@ export default {
}) })
} }
}, },
scrollIntoField (field) {
return this.$refs.areas.scrollIntoField(field)
},
scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area)
},
async submitStep () { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
@@ -718,7 +750,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid] return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && (this.submittedValues[fields[0].uuid] === true ? false : !this.submittedValues[fields[0].uuid]?.length)
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
@@ -757,6 +789,10 @@ export default {
if (respData) { if (respData) {
this.onComplete(JSON.parse(respData)) this.onComplete(JSON.parse(respData))
} }
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
}
} }
}).catch(error => { }).catch(error => {
console.error(error) console.error(error)
+77 -10
View File
@@ -1,7 +1,10 @@
const en = { const en = {
submit_form: 'Submit Form', submit_form: 'Submit Form',
type_here: 'Type here', type_here_: 'Type here...',
optional: 'optional', optional: 'optional',
option: 'Option',
appears_on: 'Appears on',
page: 'Page',
select_your_option: 'Select your option', select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click', complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
submit: 'submit', submit: 'submit',
@@ -45,8 +48,10 @@ const en = {
const es = { const es = {
submit_form: 'Enviar Formulario', submit_form: 'Enviar Formulario',
type_here: 'Escribe aquí', type_here_: 'Escribe aquí...',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece en',
page: 'Página',
select_your_option: 'Selecciona tu opción', select_your_option: 'Selecciona tu opción',
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic', complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
submit: 'enviar', submit: 'enviar',
@@ -90,8 +95,10 @@ const es = {
const it = { const it = {
submit_form: 'Invia Modulo', submit_form: 'Invia Modulo',
type_here: 'Digita qui', type_here_: 'Digita qui...',
optional: 'opzionale', optional: 'opzionale',
appears_on: 'Compare su',
page: 'Pagina',
select_your_option: 'Seleziona la tua opzione', select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic', complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia', submit: 'invia',
@@ -135,8 +142,10 @@ const it = {
const de = { const de = {
submit_form: 'Formular absenden', submit_form: 'Formular absenden',
type_here: 'Hier eingeben', type_here_: 'Hier eingeben...',
optional: 'optional', optional: 'optional',
appears_on: 'Erscheint auf',
page: 'Seite',
select_your_option: 'Wähle deine Option', select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken', complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden', submit: 'absenden',
@@ -180,8 +189,10 @@ const de = {
const fr = { const fr = {
submit_form: 'Envoyer le Formulaire', submit_form: 'Envoyer le Formulaire',
type_here: 'Tapez ici', type_here_: 'Tapez ici...',
optional: 'facultatif', optional: 'facultatif',
appears_on: 'Apparaît sur',
page: 'Page',
select_your_option: 'Sélectionnez votre option', select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez', complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer', submit: 'envoyer',
@@ -225,8 +236,10 @@ const fr = {
const pl = { const pl = {
submit_form: 'Wyślij Formularz', submit_form: 'Wyślij Formularz',
type_here: 'Wpisz tutaj', type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny', optional: 'opcjonalny',
appears_on: 'Pojawia się na',
page: 'Strona',
select_your_option: 'Wybierz swoją opcję', select_your_option: 'Wybierz swoją opcję',
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij', complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij', submit: 'wyślij',
@@ -270,8 +283,10 @@ const pl = {
const uk = { const uk = {
submit_form: 'Надіслати Форму', submit_form: 'Надіслати Форму',
type_here: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
select_your_option: 'Виберіть варіант', select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть', complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати', submit: 'надіслати',
@@ -315,8 +330,10 @@ const uk = {
const cs = { const cs = {
submit_form: 'Odeslat formulář', submit_form: 'Odeslat formulář',
type_here: 'Zadejte zde', type_here_: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu', select_your_option: 'Vyberte svou volbu',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na', complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat', submit: 'odeslat',
@@ -360,8 +377,10 @@ const cs = {
const pt = { const pt = {
submit_form: 'Enviar Formulário', submit_form: 'Enviar Formulário',
type_here: 'Digite aqui', type_here_: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece em',
page: 'Página',
select_your_option: 'Selecione sua opção', select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique', complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar', submit: 'enviar',
@@ -403,7 +422,55 @@ const pt = {
pay_with_strip: 'Pagar com Stripe' pay_with_strip: 'Pagar com Stripe'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt } const he = {
submit_form: 'שלח טופס',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
appears_on: 'מופיע ב',
page: 'עמוד',
select_your_option: 'בחר את האפשרות שלך',
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
submit: 'שליחה',
next: 'הבא',
click_to_upload: 'לחץ להעלאה',
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
send_copy_via_email: 'שלח עותק באמצעות אימייל',
download: 'הורדה',
signature: 'חתימה',
initials: 'ציוני ראשי תיבות',
clear: 'נקה',
redraw: 'צייר מחדש',
draw_initials: 'צייר ציוני ראשי תיבות',
type_signature_here: 'הקלד חתימה כאן',
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
form_has_been_completed: 'הטופס הושלם',
create_a_free_account: 'צור חשבון חינם',
signed_with: 'חתום באמצעות',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
six_digits_code: 'קוד משתמש שש ספרות',
change_phone_number: 'שינוי מספר טלפון',
sending: 'שולח',
resend_code: 'שלח מחדש קוד',
verification_code_has_been_resent: 'קוד האימות נשלח מחדש בSMS',
please_fill_all_required_fields: 'אנא מלא את כל השדות הדרושים',
set_today: 'קבע היום',
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
draw_signature: 'צייר חתימה',
type_initial: 'הקלד ציוני ראשי תיבות',
draw: 'צייר',
type: 'הקלד',
type_text: 'הקלד טקסט',
date: 'תאריך',
email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד',
pay_with_strip: 'שלם עם סטרייפ'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he }
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0] const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else v-else
class="tooltip" class="tooltip ml-2"
:data-tip="t('draw_initials')" :data-tip="t('draw_initials')"
> >
<a <a
@@ -70,6 +70,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -103,10 +104,12 @@
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconTextSize, IconTextSize,
IconSignature, IconSignature,
@@ -2,12 +2,20 @@
<label <label
v-if="field.name" v-if="field.name"
:for="field.uuid" :for="field.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }}</label> >{{ field.name }}</label>
<div class="flex w-full"> <div class="flex w-full">
<input
v-if="modelValue.length === 0"
type="text"
:name="`values[${field.uuid}][]`"
:value="''"
class="hidden"
>
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="option in field.options" v-for="(option, index) in field.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -19,13 +27,13 @@
:ref="setInputRef" :ref="setInputRef"
type="checkbox" type="checkbox"
:name="`values[${field.uuid}][]`" :name="`values[${field.uuid}][]`"
:value="option.value" :value="optionValue(option, index)"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(option.value)" :checked="(modelValue || []).includes(optionValue(option, index))"
@change="onChange" @change="onChange"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ optionValue(option, index) }}
</span> </span>
</label> </label>
</div> </div>
@@ -36,6 +44,7 @@
<script> <script>
export default { export default {
name: 'MultiSelectStep', name: 'MultiSelectStep',
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -62,8 +71,15 @@ export default {
this.inputRefs.push(el) this.inputRefs.push(el)
} }
}, },
optionValue (option, index) {
if (option.value) {
return option.value
} else {
return `${this.t('option')} ${index + 1}`
}
},
onChange () { onChange () {
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value)) this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
} }
} }
} }
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else-if="withTypedSignature" v-else-if="withTypedSignature"
class="tooltip" class="tooltip ml-2"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
<a <a
@@ -89,6 +89,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -122,12 +123,14 @@
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
let isFontLoaded = false let isFontLoaded = false
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconCamera, IconCamera,
IconTextSize, IconTextSize,
+18 -5
View File
@@ -2,26 +2,36 @@
<label <label
v-if="field.name" v-if="field.name"
:for="field.uuid" :for="field.uuid"
class="label text-2xl mb-2" dir="auto"
>{{ field.name }} class="label text-2xl"
:class="{ 'mb-2': !field.description }"
><template v-if="field.title"><span v-html="field.title" /></template>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<div <div
v-else v-else
class="py-1" class="py-1"
/> />
<div
v-if="field.description"
class="mb-3 px-1 text-lg"
v-html="field.description"
/>
<AppearsOn :field="field" />
<div class="items-center flex"> <div class="items-center flex">
<input <input
v-if="!isTextArea" v-if="!isTextArea"
:id="field.uuid" :id="field.uuid"
v-model="text" v-model="text"
:maxlength="cellsMaxLegth" :maxlength="cellsMaxLegth"
dir="auto"
class="base-input !text-2xl w-full !pr-11 -mr-10" class="base-input !text-2xl w-full !pr-11 -mr-10"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''" :oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''" :oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
type="text" type="text"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@focus="$emit('focus')" @focus="$emit('focus')"
@@ -31,8 +41,9 @@
:id="field.uuid" :id="field.uuid"
ref="textarea" ref="textarea"
v-model="text" v-model="text"
dir="auto"
class="base-textarea !text-2xl w-full" class="base-textarea !text-2xl w-full"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
:required="field.required" :required="field.required"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@input="resizeTextarea" @input="resizeTextarea"
@@ -56,11 +67,13 @@
<script> <script>
import { IconAlignBoxLeftTop } from '@tabler/icons-vue' import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'TextStep', name: 'TextStep',
components: { components: {
IconAlignBoxLeftTop IconAlignBoxLeftTop,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
+3 -2
View File
@@ -61,6 +61,7 @@
v-if="field.type !== 'checkbox' || field.name" v-if="field.type !== 'checkbox' || field.name"
ref="name" ref="name"
:contenteditable="editable && !defaultField" :contenteditable="editable && !defaultField"
dir="auto"
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"
@@ -83,7 +84,7 @@
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >{{ t('required') }}</label>
</div> </div>
<button <button
v-else-if="editable" v-else-if="editable"
@@ -149,7 +150,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save'], inject: ['template', 'selectedAreaRef', 'save', 't'],
props: { props: {
area: { area: {
type: Object, type: Object,
+100 -16
View File
@@ -43,11 +43,11 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Sign Yourself {{ t('sign_yourself') }}
</span> </span>
</a> </a>
<a <a
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowEmptyTemplateAlert"
@@ -57,7 +57,7 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Send {{ t('send') }}
</span> </span>
</a> </a>
<button <button
@@ -76,7 +76,7 @@
width="22" width="22"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Save {{ t('save') }}
</span> </span>
</button> </button>
</template> </template>
@@ -112,7 +112,7 @@
/> />
<div <div
class="sticky bottom-0 py-2" class="sticky bottom-0 py-2"
:style="withStickySubmitters ? { backgroundColor } : {}" :style="{ backgroundColor }"
> >
<Upload <Upload
v-if="sortedDocuments.length && editable && withUploadButton" v-if="sortedDocuments.length && editable && withUploadButton"
@@ -147,6 +147,7 @@
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="defaultFields"
:allow-draw="!onlyDefinedFields"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -178,6 +179,7 @@
v-if="withUploadButton" v-if="withUploadButton"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:accept-file-types="acceptFileTypes"
@success="updateFromUpload" @success="updateFromUpload"
/> />
</div> </div>
@@ -198,6 +200,7 @@
v-if="sortedDocuments.length && !drawField && editable" v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields" :fields="template.fields"
:default-fields="defaultFields" :default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
/> />
@@ -214,16 +217,24 @@
> >
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
<p> <p>
Draw {{ drawField.name }} field on the document {{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
</p> </p>
<p> <div>
<button <button
class="base-button" class="base-button"
@click="[drawField = null, drawOption = null]" @click="clearDrawField"
> >
Cancel {{ t('cancel') }}
</button> </button>
</p> <a
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
href="#"
class="link block mt-3 text-sm"
@click.prevent="[drawField = null, drawOption = null]"
>
{{ t('or_add_field_without_drawing') }}
</a>
</div>
</div> </div>
</div> </div>
<div> <div>
@@ -232,9 +243,12 @@
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:default-fields="defaultFields" :default-fields="defaultFields"
:field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:only-defined-fields="onlyDefinedFields"
:editable="editable" :editable="editable"
@set-draw="[drawField = $event.field, drawOption = $event.option]" @set-draw="[drawField = $event.field, drawOption = $event.option]"
@set-drag="dragField = $event" @set-drag="dragField = $event"
@@ -262,6 +276,7 @@ import MobileFields from './mobile_fields'
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue' import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { en as i18nEn } from './i18n'
export default { export default {
name: 'TemplateBuilder', name: 'TemplateBuilder',
@@ -285,7 +300,9 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withPayment: this.withPayment, withPayment: this.withPayment,
@@ -302,6 +319,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
i18n: {
type: Object,
required: false,
default: () => ({})
},
backgroundColor: { backgroundColor: {
type: String, type: String,
required: false, required: false,
@@ -312,11 +334,26 @@ export default {
required: false, required: false,
default: true default: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
autosave: {
type: Boolean,
required: false,
default: true
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
default: () => [] default: () => []
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -344,6 +381,13 @@ export default {
return () => {} return () => {}
} }
}, },
onSave: {
type: Function,
required: false,
default () {
return () => {}
}
},
withStickySubmitters: { withStickySubmitters: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -379,6 +423,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: false
},
fetchOptions: { fetchOptions: {
type: Object, type: Object,
required: false, required: false,
@@ -470,6 +519,9 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
t (key) {
return this.i18n[key] || i18nEn[key] || key
},
startFieldDraw ({ name, type }) { startFieldDraw ({ name, type }) {
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name) const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
@@ -549,10 +601,21 @@ export default {
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }) ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, },
clearDrawField () {
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
const fieldIndex = this.template.fields.indexOf(this.drawField)
if (fieldIndex !== -1) {
this.template.fields.splice(fieldIndex, 1)
}
}
this.drawField = null
this.drawOption = null
},
onKeyUp (e) { onKeyUp (e) {
if (e.code === 'Escape') { if (e.code === 'Escape') {
this.drawField = null this.clearDrawField()
this.drawOption = null
this.selectedAreaRef.value = null this.selectedAreaRef.value = null
} }
@@ -645,7 +708,16 @@ export default {
} }
this.drawField.areas ||= [] this.drawField.areas ||= []
this.drawField.areas.push(area)
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
})
if (insertBeforeAreaIndex !== -1) {
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
} else {
this.drawField.areas.push(area)
}
if (this.template.fields.indexOf(this.drawField) === -1) { if (this.template.fields.indexOf(this.drawField) === -1) {
this.template.fields.push(this.drawField) this.template.fields.push(this.drawField)
@@ -661,7 +733,11 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const pageMask = documentRef.pageRefs[area.page].$refs.mask const pageMask = documentRef.pageRefs[area.page].$refs.mask
const type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text' let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
type = this.fieldTypes[0]
}
if (type === 'checkbox') { if (type === 'checkbox') {
const previousField = [...this.template.fields].reverse().find((f) => f.type === type) const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
@@ -808,7 +884,7 @@ export default {
this.save() this.save()
}, },
onDocumentRemove (item) { onDocumentRemove (item) {
if (window.confirm('Are you sure?')) { if (window.confirm(this.t('are_you_sure'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
@@ -887,7 +963,11 @@ export default {
headers: { ...this.fetchOptions.headers, ...options.headers } headers: { ...this.fetchOptions.headers, ...options.headers }
}) })
}, },
save () { save ({ force } = { force: false }) {
if (!this.autosave && !force) {
return Promise.resolve({})
}
this.$nextTick(() => { this.$nextTick(() => {
if (this.$el.closest('template-builder')) { if (this.$el.closest('template-builder')) {
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template) this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
@@ -907,6 +987,10 @@ export default {
} }
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(() => {
if (this.onSave) {
this.onSave(this.template)
}
}) })
} }
} }
@@ -5,6 +5,7 @@
> >
<span <span
ref="contenteditable" ref="contenteditable"
dir="auto"
:contenteditable="editable" :contenteditable="editable"
style="min-width: 2px" style="min-width: 2px"
:class="iconInline ? 'inline' : 'block'" :class="iconInline ? 'inline' : 'block'"
@@ -7,6 +7,7 @@
:number="index" :number="index"
:editable="editable" :editable="editable"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
:default-fields="defaultFields" :default-fields="defaultFields"
:draw-field="drawField" :draw-field="drawField"
@@ -41,6 +42,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
+6 -6
View File
@@ -29,7 +29,7 @@
{{ message }} {{ message }}
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span class="font-medium">Click to upload</span> or drag and drop files <span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
</div> </div>
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@ export default {
IconCloudUpload, IconCloudUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -91,13 +91,13 @@ export default {
}, },
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return 'Uploading...' return this.t('uploading')
} else if (this.isProcessing) { } else if (this.isProcessing) {
return 'Processing...' return this.t('processing_')
} else if (this.acceptFileTypes === 'image/*, application/pdf') { } else if (this.acceptFileTypes === 'image/*, application/pdf') {
return 'Add PDF documents or images' return this.t('add_pdf_documents_or_images')
} else { } else {
return 'Add documents or images' return this.t('add_documents_or_images')
} }
} }
}, },
+19 -16
View File
@@ -47,7 +47,7 @@
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >{{ t('required') }}</label>
</template> </template>
</div> </div>
<div <div
@@ -101,7 +101,8 @@
<input <input
v-model="field.default_value" v-model="field.default_value"
type="text" type="text"
placeholder="Default value" :placeholder="t('default_value')"
dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save" @blur="save"
> >
@@ -111,7 +112,7 @@
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
> >
Default value {{ t('default_value') }}
</label> </label>
</div> </div>
<div <div
@@ -121,7 +122,7 @@
> >
<select <select
v-model="field.preferences.format" v-model="field.preferences.format"
placeholder="Format" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="save" @change="save"
> >
@@ -138,7 +139,7 @@
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
> >
Format {{ t('format') }}
</label> </label>
</div> </div>
<li <li
@@ -152,7 +153,7 @@
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">Required</span> <span class="label-text">{{ t('required') }}</span>
</label> </label>
</li> </li>
<li <li
@@ -166,7 +167,7 @@
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">Read-only</span> <span class="label-text">{{ t('read_only') }}</span>
</label> </label>
</li> </li>
<hr class="pb-0.5 mt-0.5"> <hr class="pb-0.5 mt-0.5">
@@ -183,7 +184,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Page {{ area.page + 1 }} {{ t('page') }} {{ area.page + 1 }}
</a> </a>
</li> </li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"> <li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
@@ -196,7 +197,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Draw New Area {{ t('draw_new_area') }}
</a> </a>
</li> </li>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)"> <li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
@@ -209,7 +210,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Copy to All Pages {{ t('copy_to_all_pages') }}
</a> </a>
</li> </li>
</ul> </ul>
@@ -249,8 +250,9 @@
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6" class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
type="text" type="text"
dir="auto"
required required
:placeholder="`Option ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
@blur="save" @blur="save"
> >
<button <button
@@ -268,9 +270,10 @@
v-else v-else
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`Option ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
type="text" type="text"
required required
dir="auto"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="save"
> >
@@ -287,7 +290,7 @@
class="text-center text-sm w-full pb-1" class="text-center text-sm w-full pb-1"
@click="addOption" @click="addOption"
> >
+ Add option + {{ t('add_option') }}
</button> </button>
</div> </div>
</div> </div>
@@ -313,7 +316,7 @@ export default {
IconCopy, IconCopy,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'], inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -366,7 +369,7 @@ export default {
} else { } else {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field) const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field' const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}` return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
} }
@@ -419,7 +422,7 @@ export default {
this.template.documents.forEach((attachment) => { this.template.documents.forEach((attachment) => {
attachment.preview_images.forEach((page) => { attachment.preview_images.forEach((page) => {
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) { if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
field.areas.push({ ...JSON.parse(areaString), page: parseInt(page.filename) }) field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page: parseInt(page.filename) })
} }
}) })
}) })
@@ -61,7 +61,7 @@
<button <button
v-if="submitters.length > 1 && editable" v-if="submitters.length > 1 && editable"
class="px-2" class="px-2"
@click.stop="remove(submitter)" @click.prevent.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <IconTrashX :width="18" />
</button> </button>
@@ -78,7 +78,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -105,6 +105,7 @@
</label> </label>
<label <label
v-else v-else
ref="label"
tabindex="0" tabindex="0"
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between" class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
> >
@@ -155,13 +156,36 @@
{{ submitter.name }} {{ submitter.name }}
</span> </span>
</span> </span>
<button <div
v-if="!compact && submitters.length > 1 && editable" v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2" class="flex"
@click.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
</button> <button
title="Up"
class="relative w-2"
style="font-size: 10px; margin-bottom: -4px"
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
>
▲
</button>
<button
title="Down"
class="relative w-2"
style="font-size: 10px; margin-top: -4px"
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
>
▼
</button>
</div>
<button
v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2"
@click.prevent.stop="remove(submitter)"
>
<IconTrashX :width="18" />
</button>
</div>
</a> </a>
</li> </li>
<li v-if="submitters.length < 10 && editable"> <li v-if="submitters.length < 10 && editable">
@@ -175,7 +199,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -197,6 +221,7 @@ export default {
IconTrashX, IconTrashX,
IconChevronUp IconChevronUp
}, },
inject: ['t', 'save'],
props: { props: {
submitters: { submitters: {
type: Array, type: Array,
@@ -250,16 +275,16 @@ export default {
}, },
names () { names () {
return [ return [
'First Party', this.t('first_party'),
'Second Party', this.t('second_party'),
'Third Party', this.t('third_party'),
'Fourth Party', this.t('fourth_party'),
'Fifth Party', this.t('fifth_party'),
'Sixth Party', this.t('sixth_party'),
'Seventh Party', this.t('seventh_party'),
'Eighth Party', this.t('eighth_party'),
'Ninth Party', this.t('ninth_party'),
'Tenth Party' this.t('tenth_party')
] ]
}, },
selectedSubmitter () { selectedSubmitter () {
@@ -271,10 +296,27 @@ export default {
this.$emit('update:model-value', submitter.uuid) this.$emit('update:model-value', submitter.uuid)
}, },
remove (submitter) { remove (submitter) {
if (window.confirm('Are you sure?')) { if (window.confirm(this.t('are_you_sure'))) {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
move (submitter, direction) {
const currentIndex = this.submitters.indexOf(submitter)
this.submitters.splice(currentIndex, 1)
if (currentIndex + direction > this.submitters.length) {
this.submitters.unshift(submitter)
} else if (currentIndex + direction < 0) {
this.submitters.push(submitter)
} else {
this.submitters.splice(currentIndex + direction, 0, submitter)
}
this.selectSubmitter(submitter)
this.save()
},
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.submitters.length],
+16 -16
View File
@@ -29,7 +29,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
> >
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -54,7 +54,7 @@ import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckb
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment'], inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -90,20 +90,20 @@ export default {
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
text: 'Text', text: this.t('text'),
signature: 'Signature', signature: this.t('signature'),
initials: 'Initials', initials: this.t('initials'),
date: 'Date', date: this.t('date'),
image: 'Image', image: this.t('image'),
file: 'File', file: this.t('file'),
select: 'Select', select: this.t('select'),
checkbox: 'Checkbox', checkbox: this.t('checkbox'),
multiple: 'Multiple', multiple: this.t('multiple'),
radio: 'Radio', radio: this.t('radio'),
cells: 'Cells', cells: this.t('cells'),
stamp: 'Stamp', stamp: this.t('stamp'),
payment: 'Payment', payment: this.t('payment'),
phone: 'Phone' phone: this.t('phone')
} }
}, },
fieldIcons () { fieldIcons () {
+22 -7
View File
@@ -64,7 +64,7 @@
</template> </template>
</div> </div>
<div <div
v-if="editable" v-if="editable && !onlyDefinedFields"
class="grid grid-cols-3 gap-1 pb-2" class="grid grid-cols-3 gap-1 pb-2"
> >
<template <template
@@ -72,7 +72,7 @@
:key="type" :key="type"
> >
<button <button
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
draggable="true" draggable="true"
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative" class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
@@ -91,7 +91,7 @@
</div> </div>
</button> </button>
<div <div
v-else-if="type == 'phone'" v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex" class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }" :class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }"
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification." data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
@@ -120,7 +120,7 @@
</template> </template>
</div> </div>
<div <div
v-if="fields.length < 4 && editable" v-if="fields.length < 4 && editable && withHelp"
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">
@@ -131,7 +131,7 @@
Drag &amp; drop any other field type on the page Drag &amp; drop any other field type on the page
</li> </li>
<li> <li>
Click on the field type above to add it to the form without drawing it on the document Click on the field type above to start drawing it
</li> </li>
</ul> </ul>
</div> </div>
@@ -154,12 +154,17 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'], inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -170,6 +175,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: true
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -180,6 +190,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
submitters: { submitters: {
type: Array, type: Array,
required: true required: true
@@ -285,7 +300,7 @@ export default {
this.fields.push(field) this.fields.push(field)
if (['signature', 'initials', 'cells', 'stamp'].includes(type)) { if (!['payment', 'file'].includes(type)) {
this.$emit('set-draw', { field }) this.$emit('set-draw', { field })
} }
+58
View File
@@ -0,0 +1,58 @@
const en = {
are_you_sure: 'Are you sure?',
sign_yourself: 'Sign Yourself',
send: 'Send',
save: 'Save',
cancel: 'Cancel',
draw_field_on_the_document: 'Draw {field} field on the document',
click_to_upload: 'Click to upload',
or_drag_and_drop_files: 'or drag and drop files',
uploading: 'Uploading',
processing_: 'Processing...',
add_pdf_documents_or_images: 'Add PDF documents or images',
add_documents_or_images: 'Add documents or images',
required: 'Required',
default_value: 'Default value',
format: 'Format',
read_only: 'Read-only',
page: 'Page',
draw_new_area: 'Draw New Area',
copy_to_all_pages: 'Copy to All Pages',
add_option: 'Add option',
option: 'Option',
first_party: 'First Party',
second_party: 'Second Party',
third_party: 'Third Party',
fourth_party: 'Fourth Party',
fifth_party: 'Fifth Party',
sixth_party: 'Sixth Party',
seventh_party: 'Seventh Party',
eighth_party: 'Eighth Party',
ninth_party: 'Ninth Party',
tenth_party: 'Tenth Party',
add: 'Add',
or_add_field_without_drawing: 'Or add field without drawing',
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
field: 'Field',
group: 'Group',
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
draw_field: 'Draw {field} Field',
replace: 'Replace',
uploading_: 'Uploading...',
add_document: 'Add Document'
}
export { en }
@@ -9,14 +9,14 @@
class="inline" class="inline"
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span> Draw {{ fieldNames[drawField.type] }} Field </span> <span>{{ t('draw_field').replace('{field}', fieldNames[drawField.type]) }}</span>
</div> </div>
<a <a
href="#" href="#"
class="link block text-center" class="link block text-center"
@click.prevent="$emit('cancel')" @click.prevent="$emit('cancel')"
> >
Cancel {{ t('cancel') }}
</a> </a>
</span> </span>
<FieldSubmitter <FieldSubmitter
@@ -43,7 +43,7 @@ export default {
Field, Field,
FieldSubmitter FieldSubmitter
}, },
inject: ['save'], inject: ['save', 't'],
props: { props: {
drawField: { drawField: {
type: Object, type: Object,
@@ -48,7 +48,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
> >
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -78,7 +78,7 @@ export default {
IconPlus, IconPlus,
IconX IconX
}, },
inject: ['withPhone', 'withPayment', 'backgroundColor'], inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -94,6 +94,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
+18 -1
View File
@@ -33,7 +33,7 @@
<FieldArea <FieldArea
v-if="newArea" v-if="newArea"
:is-draw="true" :is-draw="true"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || defaultFieldType }"
:area="newArea" :area="newArea"
/> />
</div> </div>
@@ -60,6 +60,7 @@ export default {
components: { components: {
FieldArea FieldArea
}, },
inject: ['fieldTypes'],
props: { props: {
image: { image: {
type: Object, type: Object,
@@ -75,6 +76,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
@@ -110,6 +116,13 @@ export default {
} }
}, },
computed: { computed: {
defaultFieldType () {
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
return this.fieldTypes[0]
} else {
return 'text'
}
},
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
@@ -149,6 +162,10 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (!this.allowDraw) {
return
}
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField) {
return return
} }
+4 -4
View File
@@ -26,7 +26,7 @@ import Upload from './upload'
export default { export default {
name: 'ReplaceDocument', name: 'ReplaceDocument',
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -56,11 +56,11 @@ export default {
}, },
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return 'Uploading...' return this.t('uploading_')
} else if (this.isProcessing) { } else if (this.isProcessing) {
return 'Processing...' return this.t('processing_')
} else { } else {
return 'Replace' return this.t('replace')
} }
} }
}, },
+55 -10
View File
@@ -15,13 +15,13 @@
width="20" width="20"
/> />
<span v-if="isLoading"> <span v-if="isLoading">
Uploading... {{ t('uploading_') }}
</span> </span>
<span v-else-if="isProcessing"> <span v-else-if="isProcessing">
Processing... {{ t('processing_') }}
</span> </span>
<span v-else> <span v-else>
Add Document {{ t('add_document') }}
</span> </span>
</label> </label>
<form <form
@@ -50,7 +50,7 @@ export default {
IconUpload, IconUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -123,9 +123,30 @@ export default {
method: 'POST', method: 'POST',
body: JSON.stringify({ blobs }), body: JSON.stringify({ blobs }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => { }).then((resp) => {
this.$emit('success', data) if (resp.ok) {
this.$refs.input.value = '' resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else {
alert('Wrong password')
}
})
}
})
}
}).finally(() => { }).finally(() => {
this.isProcessing = false this.isProcessing = false
}) })
@@ -133,9 +154,33 @@ export default {
this.baseFetch(`/api/templates/${this.templateId}/documents`, { this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST', method: 'POST',
body: new FormData(this.$refs.form) body: new FormData(this.$refs.form)
}).then(resp => resp.json()).then((data) => { }).then((resp) => {
this.$emit('success', data) if (resp.ok) {
this.$refs.input.value = '' resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
const formData = new FormData(this.$refs.form)
formData.append('password', prompt('Enter PDF password'))
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: formData
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else {
alert('Wrong password')
}
})
}
})
}
}).finally(() => { }).finally(() => {
this.isLoading = false this.isLoading = false
}) })
+18 -7
View File
@@ -22,22 +22,33 @@ class ProcessSubmitterCompletionJob < ApplicationJob
end end
def enqueue_completed_emails(submitter) def enqueue_completed_emails(submitter)
user = submitter.submission.created_by_user || submitter.template.author submission = submitter.submission
user = 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) &&
submitter.submission.preferences['send_email'] != false submission.preferences['send_email'] != false
bcc = submitter.submission.template.account.account_configs SubmitterMailer.completed_email(submitter, user).deliver_later!
.find_by(key: AccountConfig::BCC_EMAILS)&.value
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later! bcc = submission.preferences['bcc_completed'].presence ||
submission.template.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
end
end end
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } to = build_to_addresses(submitter)
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present? SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
end end
def build_to_addresses(submitter)
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter)
next_submitter_item = next_submitter_item =
submitter.submission.template_submitters.find do |e| submitter.submission.template_submitters.find do |e|
@@ -4,7 +4,11 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
SubmitterMailer.invitation_email(submitter).deliver_now! mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail)
mail.deliver_now!
SubmissionEvent.create!(submitter:, event_type: 'send_email') SubmissionEvent.create!(submitter:, event_type: 'send_email')
+11 -8
View File
@@ -33,7 +33,7 @@ class SubmitterMailer < ApplicationMailer
) )
end end
def completed_email(submitter, user, bcc: nil) def completed_email(submitter, user, to: nil)
@current_account = submitter.submission.template.account @current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
@submission = submitter.submission @submission = submitter.submission
@@ -43,7 +43,9 @@ class SubmitterMailer < ApplicationMailer
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
add_completed_email_attachments!(submitter) add_completed_email_attachments!(
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
)
subject = subject =
if @email_config if @email_config
@@ -55,8 +57,7 @@ class SubmitterMailer < ApplicationMailer
end end
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name, to: to || (user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name),
bcc:,
subject:) subject:)
end end
@@ -66,10 +67,12 @@ class SubmitterMailer < ApplicationMailer
Submissions::EnsureResultGenerated.call(@submitter) Submissions::EnsureResultGenerated.call(@submitter)
@documents = add_completed_email_attachments!(submitter)
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@documents = add_completed_email_attachments!(
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
)
subject = subject =
if @email_config if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:) ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
@@ -84,13 +87,13 @@ class SubmitterMailer < ApplicationMailer
private private
def add_completed_email_attachments!(submitter) def add_completed_email_attachments!(submitter, with_audit_log: true)
documents = Submitters.select_attachments_for_download(submitter) documents = Submitters.select_attachments_for_download(submitter)
total_size = 0 total_size = 0
audit_trail_data = nil audit_trail_data = nil
if submitter.submission.audit_trail.present? if with_audit_log && submitter.submission.audit_trail.present?
audit_trail_data = submitter.submission.audit_trail.download audit_trail_data = submitter.submission.audit_trail.download
total_size = audit_trail_data.size total_size = audit_trail_data.size
+1 -1
View File
@@ -25,7 +25,7 @@ class AccessToken < ApplicationRecord
belongs_to :user belongs_to :user
before_validation :set_sha256, on: :create before_validation :set_sha256
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) } attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
+14
View File
@@ -22,12 +22,26 @@ class Account < ApplicationRecord
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
has_many :submissions, through: :templates has_many :submissions, through: :templates
has_many :submitters, through: :submissions has_many :submitters, through: :submissions
has_many :account_linked_accounts, dependent: :destroy
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
class_name: 'AccountLinkedAccount',
inverse_of: :account
has_one :linked_account_account, dependent: :destroy,
foreign_key: :linked_account_id,
class_name: 'AccountLinkedAccount',
inverse_of: :linked_account
has_many :linked_accounts, through: :account_linked_accounts
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
has_many :active_users, -> { active }, dependent: :destroy, has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User' inverse_of: :account, class_name: 'User'
attribute :timezone, :string, default: 'UTC' attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US' attribute :locale, :string, default: 'en-US'
def testing?
linked_account_account&.testing?
end
def default_template_folder def default_template_folder
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME, super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
author_id: users.minimum(:id)).tap(&:save!) author_id: users.minimum(:id)).tap(&:save!)
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_linked_accounts
#
# id :bigint not null, primary key
# account_type :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# linked_account_id :bigint not null
#
# Indexes
#
# idx_on_account_id_linked_account_id_48ab9f79d2 (account_id,linked_account_id) UNIQUE
# index_account_linked_accounts_on_account_id (account_id)
# index_account_linked_accounts_on_linked_account_id (linked_account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (linked_account_id => accounts.id)
#
class AccountLinkedAccount < ApplicationRecord
belongs_to :account
belongs_to :linked_account, class_name: 'Account'
attribute :account_type, :string, default: 'testing'
scope :testing, -> { where(account_type: :testing) }
def testing?
account_type == 'testing'
end
end
+2 -2
View File
@@ -59,7 +59,7 @@ class User < ApplicationRecord
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig' has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
attribute :role, :string, default: ADMIN_ROLE attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
@@ -72,7 +72,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
!archived_at? super && !archived_at?
end end
def remember_me def remember_me
+2 -2
View File
@@ -6,7 +6,7 @@
<%= f.fields_for current_account do |ff| %> <%= f.fields_for current_account do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %> <%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %> <%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
@@ -22,7 +22,7 @@
</div> </div>
<% end %> <% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %> <% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
<%= f.fields_for encrypted_config do |ff| %> <%= f.fields_for encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %> <%= ff.label :value, 'App URL', class: 'label' %>
+10 -4
View File
@@ -1,13 +1,19 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">API</h1> <div class="flex justify-between">
<h1 class="text-4xl font-bold mb-4">API</h1>
<%= render 'shared/test_mode_toggle' %>
</div>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label> <label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-row space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <div class="flex w-full space-x-4">
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
</div> </div>
</div> </div>
</div> </div>
+69 -89
View File
@@ -1,100 +1,80 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<% if @pagy.count > 0 || params[:q].present? || @template_folders.present? %> <div class="flex justify-between mb-4 items-center">
<div class="flex justify-between mb-4 items-center"> <h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1> <div class="flex space-x-2">
<div class="flex space-x-2"> <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %> <%= render 'shared/search_input' %>
<%= render 'shared/search_input' %> <% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% end %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %>
<% end %>
</div>
</div> </div>
<% view_archived_html = capture do %> </div>
<% if current_account.templates.where.not(archived_at: nil).exists? %> <% view_archived_html = capture do %>
<div> <% if current_account.templates.where.not(archived_at: nil).exists? %>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a> <div>
</div> <a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div> </div>
<% end %> <% end %>
<% if @templates.present? %> <% end %>
<div class="grid gap-4 md:grid-cols-3"> <% if @template_folders.present? %>
<%= render partial: 'templates/template', collection: @templates %> <div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
</div> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
<% end %> </div>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% end %>
<%= form_for '', url: templates_upload_path, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %> <% if @templates.present? %>
<button type="submit" class="hidden"></button> <div class="grid gap-4 md:grid-cols-3">
<file-dropzone data-submit-on-upload="true" class="w-full"> <%= render partial: 'templates/template', collection: @templates %>
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed"> </div>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <% end %>
<div class="flex flex-col items-center"> <% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<span data-target="file-dropzone.icon" class="flex flex-col items-center"> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<span> <input type="hidden" name="form_id" value="<%= form_id %>">
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %> <button type="submit" class="hidden"></button>
</span> <file-dropzone data-submit-on-upload="true" class="w-full">
<div class="font-medium mb-1"> <label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
Upload New Document <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
</div> <div class="flex flex-col items-center">
<div class="text-xs"> <span data-target="file-dropzone.icon" class="flex flex-col items-center">
<span class="font-medium">Click to upload</span> or drag and drop <span>
</div> <%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</span> </span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden"> <div class="font-medium mb-1">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %> Upload New Document
<div class="font-medium mb-1"> </div>
Uploading... <div class="text-xs">
</div> <span class="font-medium">Click to upload</span> or drag and drop
</span> </div>
</div> </span>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple> <span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
</div> <%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
</label> <div class="font-medium mb-1">
</file-dropzone> Uploading...
<% end %> </div>
<% end %> </span>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div>
</div>
<% end %>
<% else %>
<div class="card bg-base-200 h-96 mb-2">
<div class="card-body text-center">
<div class="flex items-center h-full">
<div class="mx-auto">
<div class="max-w-xl mx-auto">
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
</div>
<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>
<%= link_to new_template_path, class: 'base-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1">Create Template</span>
<% end %>
</div> </div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
</div> </div>
</div> </label>
</file-dropzone>
<% end %>
<% end %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div> </div>
</div> </div>
<% end %> <% end %>
@@ -41,13 +41,6 @@
</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' %>
+12 -8
View File
@@ -17,15 +17,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+12 -8
View File
@@ -21,15 +21,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -8,7 +8,7 @@
<% end %> <% end %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :otp_attempt, 'Two-Factor Code', class: 'label' %> <%= f.label :otp_attempt, 'Two-Factor Code from Authenticator App', class: 'label' %>
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %> <%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div> </div>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 13h5" />
<path d="M12 16v-8h3a2 2 0 0 1 2 2v1a2 2 0 0 1 -2 2h-3" />
<path d="M20 8v8" />
<path d="M9 16v-5.5a2.5 2.5 0 0 0 -5 0v5.5" />
</svg>

After

Width:  |  Height:  |  Size: 439 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 6l-6 6l6 6" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8.5 13.5l-1.5 -1.5l1.5 -1.5" />
<path d="M15.5 10.5l1.5 1.5l-1.5 1.5" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M13 9.5l-2 5.5" />
</svg>

After

Width:  |  Height:  |  Size: 458 B

+1 -1
View File
@@ -6,7 +6,7 @@
<style> <style>
</style> </style>
</head> </head>
<body> <body dir="<%= yield&.match?(TextUtils::RTL_REGEXP) ? 'rtl' : 'auto' %>">
<%= yield %> <%= yield %>
<%= render partial: 'shared/mailer_attribution' %> <%= render partial: 'shared/mailer_attribution' %>
</body> </body>
+1 -1
View File
@@ -3,7 +3,7 @@
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below. Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
</p> </p>
<div> <div>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %> <%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'md:w-80 md:h-80 my-4 mx-auto' }) %>
</div> </div>
<div class="form-control my-6 space-y-2"> <div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %> <%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
@@ -2,7 +2,7 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %> <% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
<div class="flex space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, 'First reminder in', class: 'label' %> <%= ff.label :first_duration, 'First reminder in', class: 'label' %>
+9 -3
View File
@@ -4,11 +4,16 @@
<div class="text-center"> <div class="text-center">
<div class="relative flex flex-col items-center"> <div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %> <%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-8"> <h1 class="text-6xl font-bold mt-4 mb-4">
DocuSeal DocuSeal
</h1> </h1>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<h2 class="mb-16 text-lg text-center text-gray-600"> <h2 class="mt-4 mb-16 text-lg text-center text-gray-600">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing. A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2> </h2>
</div> </div>
@@ -62,7 +67,8 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3> <h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>Open-source contributors are always ready to help! Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>
Open-source contributors are always ready to help!
</p> </p>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -8,17 +8,28 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<div class="flex items-center justify-between py-2.5 mx-1">
<span>
Attach audit log PDF
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
@@ -11,7 +11,7 @@
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :title, 'Button title', class: 'label' %> <%= ff.label :title, 'Button title', class: 'label' %>
<%= ff.text_field :title, class: 'base-input' %> <%= ff.text_field :title, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :url, 'Button URL', class: 'label' %> <%= ff.label :url, 'Button URL', class: 'label' %>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -11,12 +11,17 @@
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<% end %> <% end %>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -8,17 +8,33 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <div class="flex items-center">
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.label :subject, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, 'subject').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<div class="flex items-center justify-between py-2.5 mx-1">
<span>
Attach audit log PDF
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
+4 -3
View File
@@ -6,11 +6,11 @@
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
<%= f.label :first_name, class: 'label' %> <%= f.label :first_name, class: 'label' %>
<%= f.text_field :first_name, required: true, class: 'base-input' %> <%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :last_name, class: 'label' %> <%= f.label :last_name, class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input' %> <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -25,7 +25,8 @@
<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 %>
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: 'Remove', disabled_with: 'Removing'), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>"> <img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>">
</div> </div>
<% end %> <% end %>
@@ -1,4 +1,8 @@
<%= 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 %> <%= 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 Upgrade
<% end %> <% end %>
<% if signed_in? && current_user != true_user %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<%= render 'shared/test_alert' %>
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span> <span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
+22 -5
View File
@@ -11,17 +11,17 @@
<%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% unless Docuseal.multitenant? %> <% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li> <li>
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -43,7 +43,7 @@
</li> </li>
<% end %> <% end %>
<% unless Docuseal.multitenant? %> <% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -88,10 +88,22 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<li>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
Test Environment
</span>
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
</li>
<% end %>
<% end %> <% end %>
</ul> </ul>
</menu-active> </menu-active>
<% if !can?(:manage, :tenants) %> <% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div class="mx-4 border-t border-base-300 hidden md:block"> <div class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3"> <div class="text-sm mt-3">
Need help? Ask a question: Need help? Ask a question:
@@ -116,6 +128,11 @@
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center"> <a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %> <%= Docuseal::SUPPORT_EMAIL %>
</a> </a>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
+17
View File
@@ -0,0 +1,17 @@
<% if signed_in? && current_user != true_user && current_account.testing? %>
<div class="alert py-1 text-sm font-medium gap-x-2 flex whitespace-nowrap">
<a href="<%= testing_api_settings_path %>" data-turbo-frame="modal" class="link font-semibold flex">
<%= svg_icon('code_circle', class: 'w-5 h-5 mr-1') %>Testing Environment
</a>
<span>
|
</span>
<%= button_to testing_account_path, method: :delete, class: 'inline flex' do %>
<% title = capture do %>
<span class="link">Exit</span>
<span>&times;</span>
<% end %>
<%= button_title(title:, disabled_with: 'Leave') %>
<% end %>
</div>
<% end %>
@@ -0,0 +1,10 @@
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
<label class="flex items-center justify-between py-2.5" for="testing_toggle">
<span class="mr-2 text-lg">
Test Environment
</span>
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
<% end %>
+1 -1
View File
@@ -3,7 +3,7 @@
<div> <div>
<p class="font-bold">Send signature requests via SMS</p> <p class="font-bold">Send signature requests via SMS</p>
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Enterprise Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p> </p>
+1 -1
View File
@@ -3,7 +3,7 @@
<div> <div>
<p class="font-bold">Single Sign On with SAML 2.0</p> <p class="font-bold">Single Sign On with SAML 2.0</p>
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Enterprise Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p> </p>
+6 -6
View File
@@ -10,23 +10,23 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %></p> <p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center text-2xl font-semibold"> <div dir="auto" class="text-center text-2xl font-semibold">
Form has been submitted already <%= t('form_has_been_submitted_already') %>
</div> </div>
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %> <% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
<div> <div>
<%= 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: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div> </div>
<% end %> <% end %>
<% if @template.submitters.to_a.size == 1 %> <% if @template.submitters.to_a.size == 1 %>
<div> <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: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div> </div>
<% end %> <% end %>
</div> </div>
+8 -8
View File
@@ -4,7 +4,7 @@
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'banner' %>
<% unless @template.archived_at? %> <% unless @template.archived_at? %>
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p> <p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
<% end %> <% end %>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
@@ -15,9 +15,9 @@
<div> <div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p> <p class="text-lg font-bold mb-1"><%= @template.name %></p>
<% if @template.archived_at? %> <% if @template.archived_at? %>
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @template.account.name %></span>.</p> <p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
<% else %> <% else %>
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p> <p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -25,13 +25,13 @@
</div> </div>
<% unless @template.archived_at? %> <% unless @template.archived_at? %>
<%= 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| %> <%= 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 dir="auto" class="form-control !mt-0">
<%= f.label :email, class: 'label' %> <%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %> <%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t('provide_your_email_to_start') %>
<%= @error_message %> <%= @error_message %>
</div> </div>
<div class="form-control"> <div dir="auto" class="form-control">
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %> <%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@@ -18,7 +18,7 @@
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '' %> <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '', dir: 'auto' %>
</submitters-autocomplete> </submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>"> <div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
+187
View File
@@ -0,0 +1,187 @@
<div>
<div class="">
<input id="api_key" type="text" value="<%= start_form_url(slug: template.slug) %>" class="input font-mono input-bordered w-full bg-white" autocomplete="off" readonly>
<div class="mt-4">
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: template.slug), class: 'base-button w-full block', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<div class="divider uppercase normal-case mt-5 mb-6 text-neutral-700">Or embed on your website</div>
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst="">
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
<div class="mt-4">
<a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a>
</div>
</div>
+2 -2
View File
@@ -23,14 +23,14 @@
</submitters-autocomplete> </submitters-autocomplete>
<% if template.submitters.size > 1 %> <% if template.submitters.size > 1 %>
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
</submitters-autocomplete> </submitters-autocomplete>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size == 1 %> <% if template.submitters.size == 1 %>
<div class="form-control flex"> <div class="form-control flex">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
</submitters-autocomplete> </submitters-autocomplete>
</div> </div>
<% end %> <% end %>
+8 -3
View File
@@ -33,12 +33,17 @@
<div class="form-control space-y-2"> <div class="form-control space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :subject, class: 'label' %> <%= f.label :subject, class: 'label' %>
<%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full' %> <%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :message, 'Body', class: 'label' %> <div class="flex items-center">
<%= f.label :message, 'Body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= 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, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<%= render 'message_fields' %> <%= render 'message_fields' %>
+1 -1
View File
@@ -1,4 +1,4 @@
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%"> <field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %> <% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %> <% elsif field['type'].in?(['file', 'payment']) %>
+7 -2
View File
@@ -1,11 +1,11 @@
<%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %> <%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %>
<% options = [['via Email', 'email'], ['via Phone', 'phone'], ['Detailed', 'detailed']] %> <% options = [['via Email', 'email'], ['via Phone', 'phone'], %w[Detailed detailed], (Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 ? ['via Link', 'link'] : nil)].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block">
<div class="join"> <div class="join">
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<span> <span>
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item w-28 peer-checked:btn-active normal-case"> <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-28 peer-checked:btn-active normal-case">
<%= label %> <%= label %>
</label> </label>
</span> </span>
@@ -22,6 +22,11 @@
<div id="detailed" class="hidden"> <div id="detailed" class="hidden">
<%= render 'detailed_form', template: @template %> <%= render 'detailed_form', template: @template %>
</div> </div>
<% if Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 %>
<div id="link" class="hidden">
<%= render 'link_form', template: @template %>
</div>
<% end %>
</div> </div>
<%= content_for(:modal_extra) %> <%= content_for(:modal_extra) %>
<% end %> <% end %>
+19 -8
View File
@@ -35,13 +35,13 @@
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<div class="pb-2 pt-1.5 text-center"> <div class="pb-2 pt-1.5 text-center" dir="auto">
<%= item['name'].presence || document.filename.base %> <%= item['name'].presence || document.filename.base %>
</div> </div>
</a> </a>
<% end %> <% end %>
</div> </div>
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"> <div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div class="pr-3.5 pl-0.5"> <div class="pr-3.5 pl-0.5">
<% 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) } %>
@@ -71,16 +71,17 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space"> <div id="parties_view" class="hidden md:block relative w-full md:w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %> <% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %> <% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% submitter_field_counters = Hash.new { 0 } %>
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %> <% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %> <% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10"> <div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
<div class="border border-base-300 rounded-md px-2 py-1 mb-1"> <div class="border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span> <span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
<span class="text-lg"> <span class="text-lg" dir="auto">
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span> </span>
</div> </div>
@@ -125,23 +126,22 @@
<% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %> <% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>"> <a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
Submit Form Sign In-person
</a> </a>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="px-1.5 mb-4"> <div class="px-1.5 mb-4">
<% submitter_field_counters = Hash.new { 0 } %>
<% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %> <% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %>
<% submitter_field_counters[field['type']] += 1 %> <% submitter_field_counters[field['type']] += 1 %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% next if value.blank? %> <% next if value.blank? %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
<div class="text-xs font-medium uppercase mb-0.5"> <div class="text-xs font-medium uppercase mb-0.5" dir="auto">
<%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %>
</div> </div>
<div> <div dir="auto">
<% if field['type'].in?(%w[signature initials]) %> <% if field['type'].in?(%w[signature initials]) %>
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
@@ -173,5 +173,16 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<label class="md:hidden btn btn-sm btn-neutral text-white text-base z-10 fixed bottom-2 right-2 h-16 shadow-lg">
<input type="checkbox" class="peer hidden" onclick="[document_view.classList.toggle('hidden'), parties_view.classList.toggle('hidden')]">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('users', class: 'w-8 h-8') %>
<span>Signers</span>
</span>
<span class="hidden peer-checked:flex items-center">
<%= svg_icon('chevron_left', class: 'w-8 h-8') %>
<span>Back</span>
</span>
</label>
</div> </div>
<%= render 'scripts/autosize_field' %> <%= render 'scripts/autosize_field' %>
@@ -1,4 +1,4 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %> <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% configs = Submitters::FormConfigs.call(submitter) %> <% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
+2 -2
View File
@@ -9,9 +9,9 @@
<div class="mr-3"> <div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @submitter.submission.template.account.name %></span>.</p> <p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.submission.template.account.name) %></p>
</div> </div>
</div> </div>
</div> </div>
+8 -8
View File
@@ -10,17 +10,17 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm">Signed on <%= l(@submitter.completed_at.to_date, format: :long, locale: @submitter.submission.template.account.locale) %></p> <p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<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: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
<% if @submitter.submission.template.submitters.size != 1 %> <% if @submitter.submission.template.submitters.size != 1 %>
<div class="divider">OR</div> <div class="divider uppercase"><%= t('or') %></div>
<% else %> <% else %>
<div class="py-2"></div> <div class="py-2"></div>
<% end %> <% end %>
@@ -28,18 +28,18 @@
<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">
<%= svg_icon('download', class: 'w-6 h-6') %> <%= svg_icon('download', class: 'w-6 h-6') %>
<span>Download documents</span> <span><%= t('download_documents') %></span>
</span> </span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> <span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %> <%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
<span>Downloading</span> <span><%= t('downloading') %></span>
</span> </span>
</download-button> </download-button>
</div> </div>
<% if @submitter.submission.template.submitters.size == 1 %> <% if @submitter.submission.template.submitters.size == 1 %>
<div class="divider">OR</div> <div class="divider uppercase"><%= t('or') %></div>
<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: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div> </div>
<% end %> <% end %>
</div> </div>
+17
View File
@@ -0,0 +1,17 @@
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center p-2 mb-4 justify-center">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('circle_check', class: 'w-10 h-10 text-green-600') %>
</div>
<div class="text-2xl">
<%= t('completed_successfully') %>
</div>
</div>
</div>
</div>
</div>
</div>
<%= render 'shared/attribution', link_path: '/start' %>
+1 -1
View File
@@ -1,7 +1,7 @@
<%= render 'shared/turbo_modal', title: 'Rename Folder' do %> <%= render 'shared/turbo_modal', title: 'Rename Folder' do %>
<%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> <%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<div class="form-control my-6"> <div class="form-control my-6">
<%= f.text_field :name, required: true, placeholder: 'Folder Name...', class: 'base-input w-full', autofocus: true %> <%= f.text_field :name, required: true, placeholder: 'Folder Name...', class: 'base-input w-full', autofocus: true, dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Rename', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Rename', disabled_with: 'Saving'), class: 'base-button' %>
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more