Compare commits

...

43 Commits

Author SHA1 Message Date
Alex Turchyn a2d8b85549
Merge from docusealco/wip
17 hours ago
Pete Matsyburka 4d68783b82 add counters
20 hours ago
Pete Matsyburka b05c3b2030 fix autocomplete
20 hours ago
Pete Matsyburka 784ae9aa4b fix dynamic document preview
22 hours ago
Pete Matsyburka 03b5956cbc bcc limit
22 hours ago
Pete Matsyburka 2876c9a18e adjust pagination
3 days ago
Pete Matsyburka c4b7be3925 fix mobile tabs
4 days ago
Pete Matsyburka e2ffbdac1d update gem
4 days ago
Pete Matsyburka 0d197bb2eb fix mobile modal
4 days ago
Pete Matsyburka 55089bee7f adjust pagination
4 days ago
Pete Matsyburka b96fe74db1 adjust filters
4 days ago
Pete Matsyburka 2fc1ba3037 fix layout
4 days ago
Pete Matsyburka c67d37207e update deps
4 days ago
Pete Matsyburka dba50cd1bf mobile fixes
4 days ago
Pete Matsyburka edf2c0d9ed update gem
4 days ago
Pete Matsyburka 35f7c36f28 rename settings index
4 days ago
Pete Matsyburka 3a24ecac63 Revert "break long names in template and folder titles"
4 days ago
Pete Matsyburka 50ec7682b5 fix open modal mobile
4 days ago
Pete Matsyburka 3667993caf center settings footer
5 days ago
Alex Turchyn 68e8cd1188 fix settings nav offset
5 days ago
Alex Turchyn 3eebf6d2cf unify tooltip and breakpoint classes
5 days ago
Alex Turchyn 34d0e00910 use numeric keyboard for otp code inputs
5 days ago
Alex Turchyn b8ab7190d2 use dvh for modal and drawer
5 days ago
Alex Turchyn 59fe8a7917 break long names in template and folder titles
5 days ago
Alex Turchyn d56070827b fix broken svg
5 days ago
Alex Turchyn 74dbd4bed3 fix trusted signature text
5 days ago
Alex Turchyn 807e82b21e open modals as pages on mobile devices
5 days ago
Alex Turchyn 66862e93c8 unify breakpoint classes
5 days ago
Alex Turchyn 270461ad3b add mobile filters
5 days ago
Alex Turchyn 3edd1ab32d fix invalid classes and html attributes
5 days ago
Alex Turchyn e77998e53c scale down settings titles on mobile
5 days ago
Alex Turchyn 1421bc2bce fix horizontal scroll from preferences tooltip
5 days ago
Alex Turchyn cbbbfdda80 add mobile settings navigation
5 days ago
Alex Turchyn 2d510081be fix esign signature row layout
5 days ago
Alex Turchyn 704893ee1d show view archived link in mobile pagination
5 days ago
Alex Turchyn 88612913a6 wrap smtp security radios on small screens
5 days ago
Alex Turchyn ce6823ed38 hide tooltip wrappers with hidden content
5 days ago
Alex Turchyn 9074d23b3c fix uneven template action buttons
5 days ago
Alex Turchyn 76d0e76fd5 no touch tooltips on nav elements
5 days ago
Pete Matsyburka 5ec8e4ccc7 text formula
5 days ago
Alex Turchyn b8c29324b8 paypal configs
5 days ago
Pete Matsyburka 972f38242e update rails
5 days ago
Pete Matsyburka 2c6ddd7f90 rate limit remove 2fa
1 week ago

@ -1,31 +1,31 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
action_text-trix (2.1.18) action_text-trix (2.1.19)
railties railties
actioncable (8.1.3) actioncable (8.1.3.1)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.1.3) actionmailbox (8.1.3.1)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
activejob (= 8.1.3) activejob (= 8.1.3.1)
activerecord (= 8.1.3) activerecord (= 8.1.3.1)
activestorage (= 8.1.3) activestorage (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.1.3) actionmailer (8.1.3.1)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
actionview (= 8.1.3) actionview (= 8.1.3.1)
activejob (= 8.1.3) activejob (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.1.3) actionpack (8.1.3.1)
actionview (= 8.1.3) actionview (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@ -33,36 +33,36 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.1.3) actiontext (8.1.3.1)
action_text-trix (~> 2.1.15) action_text-trix (~> 2.1.15)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
activerecord (= 8.1.3) activerecord (= 8.1.3.1)
activestorage (= 8.1.3) activestorage (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.1.3) actionview (8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.1.3) activejob (8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.1.3) activemodel (8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
activerecord (8.1.3) activerecord (8.1.3.1)
activemodel (= 8.1.3) activemodel (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.1.3) activestorage (8.1.3.1)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
activejob (= 8.1.3) activejob (= 8.1.3.1)
activerecord (= 8.1.3) activerecord (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.1.3) activesupport (8.1.3.1)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
@ -144,7 +144,7 @@ GEM
cldr-plurals-runtime-rb (1.1.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.3.7) concurrent-ruby (1.3.8)
connection_pool (3.0.2) connection_pool (3.0.2)
crack (1.0.1) crack (1.0.1)
bigdecimal bigdecimal
@ -179,7 +179,7 @@ GEM
dotenv (3.2.0) dotenv (3.2.0)
drb (2.2.3) drb (2.2.3)
email_typo (0.2.3) email_typo (0.2.3)
erb (6.0.4) erb (6.0.6)
erb_lint (0.9.0) erb_lint (0.9.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
@ -217,7 +217,7 @@ GEM
foreman (0.90.0) foreman (0.90.0)
thor (~> 1.4) thor (~> 1.4)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.3.0) globalid (1.4.0)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (1.0.2) google-apis-core (1.0.2)
addressable (~> 2.8, >= 2.8.7) addressable (~> 2.8, >= 2.8.7)
@ -262,7 +262,7 @@ GEM
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
strscan (>= 3.1.2) strscan (>= 3.1.2)
i18n (1.14.8) i18n (1.15.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
io-console (0.8.2) io-console (0.8.2)
irb (1.18.0) irb (1.18.0)
@ -271,7 +271,7 @@ GEM
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.19.9) json (2.21.1)
jwt (3.2.0) jwt (3.2.0)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
@ -296,24 +296,24 @@ GEM
loofah (2.25.2) loofah (2.25.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.9.0) mail (2.9.1)
logger logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.1.0) marcel (1.2.1)
matrix (0.4.3) matrix (0.4.3)
method_source (1.1.0) method_source (1.1.0)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (6.0.6) minitest (6.0.6)
drb (~> 2.0) drb (~> 2.0)
prism (~> 1.5) prism (~> 1.5)
msgpack (1.8.0) msgpack (1.8.4)
multi_json (1.19.1) multi_json (1.19.1)
net-http (0.9.1) net-http (0.9.1)
uri (>= 0.11.1) uri (>= 0.11.1)
net-imap (0.6.4.1) net-imap (0.6.6)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -344,7 +344,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
package_json (0.2.0) package_json (0.2.0)
pagy (43.4.4) pagy (43.6.1)
json json
uri uri
yaml yaml
@ -357,7 +357,7 @@ GEM
pg (1.6.3-arm64-darwin) pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-linux) pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl) pg (1.6.3-x86_64-linux-musl)
pp (0.6.3) pp (0.6.4)
prettyprint prettyprint
pretender (1.0.0) pretender (1.0.0)
actionpack (>= 7.2) actionpack (>= 7.2)
@ -369,9 +369,6 @@ GEM
reline (>= 0.6.0) reline (>= 0.6.0)
pry-rails (0.3.11) pry-rails (0.3.11)
pry (>= 0.13.0) pry (>= 0.13.0)
psych (5.3.1)
date
stringio
public_suffix (7.0.5) public_suffix (7.0.5)
puma (8.0.2) puma (8.0.2)
nio4r (~> 2.0) nio4r (~> 2.0)
@ -386,20 +383,20 @@ GEM
rack (>= 1.3) rack (>= 1.3)
rackup (2.3.1) rackup (2.3.1)
rack (>= 3) rack (>= 3)
rails (8.1.3) rails (8.1.3.1)
actioncable (= 8.1.3) actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3) actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3) actionmailer (= 8.1.3.1)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
actiontext (= 8.1.3) actiontext (= 8.1.3.1)
actionview (= 8.1.3) actionview (= 8.1.3.1)
activejob (= 8.1.3) activejob (= 8.1.3.1)
activemodel (= 8.1.3) activemodel (= 8.1.3.1)
activerecord (= 8.1.3) activerecord (= 8.1.3.1)
activestorage (= 8.1.3) activestorage (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.1.3) railties (= 8.1.3.1)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -410,9 +407,9 @@ GEM
rails-i18n (8.1.0) rails-i18n (8.1.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
railties (8.1.3) railties (8.1.3.1)
actionpack (= 8.1.3) actionpack (= 8.1.3.1)
activesupport (= 8.1.3) activesupport (= 8.1.3.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -421,9 +418,14 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.4.2) rake (13.4.2)
rdoc (7.2.0) rbs (4.1.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb erb
psych (>= 4.0.0) prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort tsort
redis-client (0.29.0) redis-client (0.29.0)
connection_pool connection_pool
@ -525,12 +527,11 @@ GEM
simplecov-html (0.13.2) simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.9.2-aarch64-linux-gnu) sqlite3 (2.9.5-aarch64-linux-gnu)
sqlite3 (2.9.2-aarch64-linux-musl) sqlite3 (2.9.5-aarch64-linux-musl)
sqlite3 (2.9.2-arm64-darwin) sqlite3 (2.9.5-arm64-darwin)
sqlite3 (2.9.2-x86_64-linux-gnu) sqlite3 (2.9.5-x86_64-linux-gnu)
sqlite3 (2.9.2-x86_64-linux-musl) sqlite3 (2.9.5-x86_64-linux-musl)
stringio (3.2.0)
strip_attributes (2.0.1) strip_attributes (2.0.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
strscan (3.1.8) strscan (3.1.8)
@ -570,14 +571,14 @@ GEM
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2) webrick (1.9.2)
websocket-driver (0.8.0) websocket-driver (0.8.2)
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yaml (0.4.0) yaml (0.4.0)
zeitwerk (2.7.5) zeitwerk (2.8.2)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux

@ -37,7 +37,7 @@ class AccountConfigsController < ApplicationController
end end
def destroy def destroy
raise InvalidKey unless allowed_keys.include?(@account_config.key) raise InvalidKey unless allowed_destroy_keys.include?(@account_config.key)
@account_config.destroy! @account_config.destroy!
@ -50,6 +50,10 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS ALLOWED_KEYS
end end
def allowed_destroy_keys
ALLOWED_KEYS
end
def load_account_config def load_account_config
raise InvalidKey unless allowed_keys.include?(account_config_params[:key]) raise InvalidKey unless allowed_keys.include?(account_config_params[:key])

@ -20,6 +20,8 @@ class MfaSetupController < ApplicationController
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured') redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
else else
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
@ -34,6 +36,8 @@ class MfaSetupController < ApplicationController
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed') redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
else else
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content

@ -0,0 +1,7 @@
# frozen_string_literal: true
class SettingsController < ApplicationController
skip_authorization_check
def index; end
end

@ -24,5 +24,7 @@ class SubmissionEventsController < ApplicationController
load_and_authorize_resource :submission load_and_authorize_resource :submission
def index; end def index
render :index, layout: 'plain'
end
end end

@ -33,6 +33,8 @@ class SubmissionsController < ApplicationController
def new def new
authorize!(:new, Submission) authorize!(:new, Submission)
render :new, layout: 'plain'
end end
def create def create

@ -8,6 +8,8 @@ class SubmittersController < ApplicationController
if @submitter.preferences['email_message_uuid'].present? if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid']) @submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end end
render :edit, layout: 'plain'
end end
def update def update

@ -25,7 +25,9 @@ class TemplatesController < ApplicationController
redirect_to root_path redirect_to root_path
end end
def new; end def new
render :new, layout: 'plain'
end
def edit def edit
@template_data = Templates.serialize_for_builder(@template) @template_data = Templates.serialize_for_builder(@template)

@ -13,11 +13,13 @@ class TemplatesFormPreviewController < ApplicationController
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) } @submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(@submitter.submission, preview: true)
Submissions.preload_with_pages(@submitter.submission) Submissions.preload_with_pages(@submitter.submission)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments) @attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter) @form_configs = Submitters::FormConfigs.call(@submitter, SubmitFormController::CONFIG_KEYS)
end end
end end

@ -14,7 +14,9 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_body] completed_notification_email_body]
}.freeze }.freeze
def show; end def show
render :show, layout: 'plain'
end
def create def create
authorize!(:update, @template) authorize!(:update, @template)

@ -3,7 +3,9 @@
class TemplatesShareLinkController < ApplicationController class TemplatesShareLinkController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def show; end def show
render :show, layout: 'plain'
end
def create def create
authorize!(:update, @template) authorize!(:update, @template)

@ -56,6 +56,9 @@ import OpenModal from './elements/open_modal'
import BarChart from './elements/bar_chart' import BarChart from './elements/bar_chart'
import FieldCondition from './elements/field_condition' import FieldCondition from './elements/field_condition'
import ConfirmUpload from './elements/confirm_upload' import ConfirmUpload from './elements/confirm_upload'
import ScrollFade from './elements/scroll_fade'
import OpenModalMobile from './elements/open_modal_mobile'
import HistoryBack from './elements/history_back'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@ -150,10 +153,14 @@ safeRegisterElement('open-modal', OpenModal)
safeRegisterElement('bar-chart', BarChart) safeRegisterElement('bar-chart', BarChart)
safeRegisterElement('field-condition', FieldCondition) safeRegisterElement('field-condition', FieldCondition)
safeRegisterElement('confirm-upload', ConfirmUpload) safeRegisterElement('confirm-upload', ConfirmUpload)
safeRegisterElement('scroll-fade', ScrollFade)
safeRegisterElement('open-modal-mobile', OpenModalMobile)
safeRegisterElement('history-back', HistoryBack)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmit) document.addEventListener('turbo:submit-end', this.onSubmit)
document.addEventListener('turbo:before-cache', this.onBeforeCache)
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
@ -181,7 +188,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withCustomFields: true, withCustomFields: true,
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true', isStripeConnected: this.dataset.isPaymentConnected === 'true' || this.dataset.isStripeConnected === 'true',
withStripe: this.dataset.withStripe !== 'false',
withPaypal: this.dataset.withPaypal === 'true',
isPaypalConnected: this.dataset.isPaypalConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',
withSendButton: this.dataset.withSendButton !== 'false', withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
@ -234,8 +244,14 @@ safeRegisterElement('template-builder', class extends HTMLElement {
} }
} }
onBeforeCache = () => {
this.app?.unmount()
this.appElem?.remove()
}
disconnectedCallback () { disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmit) document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
this.app?.unmount() this.app?.unmount()
this.appElem?.remove() this.appElem?.remove()
@ -244,6 +260,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
safeRegisterElement('import-list', class extends HTMLElement { safeRegisterElement('import-list', class extends HTMLElement {
connectedCallback () { connectedCallback () {
document.addEventListener('turbo:before-cache', this.onBeforeCache)
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
this.app = createApp(ImportList, { this.app = createApp(ImportList, {
@ -258,7 +276,14 @@ safeRegisterElement('import-list', class extends HTMLElement {
this.appendChild(this.appElem) this.appendChild(this.appElem)
} }
onBeforeCache = () => {
this.app?.unmount()
this.appElem?.remove()
}
disconnectedCallback () { disconnectedCallback () {
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
this.app?.unmount() this.app?.unmount()
this.appElem?.remove() this.appElem?.remove()
} }

@ -114,6 +114,13 @@ button[disabled] .enabled, button.btn-disabled .enabled {
bottom: auto; bottom: auto;
} }
@media (hover: none), (pointer: coarse) {
.tooltip-no-touch:before,
.tooltip-no-touch:after {
display: none;
}
}
.autocomplete { .autocomplete {
background: white; background: white;
z-index: 1000; z-index: 1000;

@ -0,0 +1,15 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', this.onClick)
}
disconnectedCallback () {
this.removeEventListener('click', this.onClick)
}
onClick = (e) => {
e.preventDefault()
window.history.back()
}
}

@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.disabled === 'true') return
if (!this.isMobile()) return
if (window.innerWidth >= 768) return
this.querySelectorAll('a[data-turbo-frame]').forEach((link) => {
link.dataset.turboFrame = '_top'
})
}
isMobile () {
const isTouchWebkit = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit|android/i.test(navigator.userAgent)
return isTouchWebkit || /iPhone|iPad|iPod/i.test(navigator.userAgent)
}
}

@ -0,0 +1,40 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('scroll', this.updateFade, { passive: true })
window.addEventListener('resize', this.updateFade)
this.updateFade()
}
disconnectedCallback () {
window.removeEventListener('resize', this.updateFade)
}
updateFade = () => {
const maxScroll = this.scrollWidth - this.clientWidth
const fadeGradient = {
right: 'linear-gradient(to right, black calc(100% - 24px), transparent)',
left: 'linear-gradient(to right, transparent, black 24px)',
both: 'linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent)'
}
let state = 'none'
if (maxScroll > 4) {
if (this.scrollLeft <= 4) {
state = 'right'
} else if (this.scrollLeft >= maxScroll - 4) {
state = 'left'
} else {
state = 'both'
}
}
this.applyFade(fadeGradient[state] || '')
}
applyFade (gradient) {
this.style.maskImage = gradient
this.style.webkitMaskImage = gradient
}
}

@ -6,7 +6,7 @@ export default actionable(class extends HTMLElement {
document.addEventListener('keyup', this.onEscKey) document.addEventListener('keyup', this.onEscKey)
document.addEventListener('turbo:before-cache', this.close) document.addEventListener('turbo:before-cache', this.onBeforeCache)
if (this.dataset.closeAfterSubmit !== 'false') { if (this.dataset.closeAfterSubmit !== 'false') {
document.addEventListener('turbo:submit-end', this.onSubmit) document.addEventListener('turbo:submit-end', this.onSubmit)
@ -18,7 +18,7 @@ export default actionable(class extends HTMLElement {
document.removeEventListener('keyup', this.onEscKey) document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.onSubmit) document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.close) document.removeEventListener('turbo:before-cache', this.onBeforeCache)
} }
onSubmit = (e) => { onSubmit = (e) => {
@ -33,6 +33,12 @@ export default actionable(class extends HTMLElement {
} }
} }
onBeforeCache = () => {
if (this.closest('turbo-frame')?.src) {
this.remove()
}
}
close = (e) => { close = (e) => {
e?.preventDefault() e?.preventDefault()

@ -35,6 +35,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
scrollPadding: this.dataset.scrollPadding || '-80px', scrollPadding: this.dataset.scrollPadding || '-80px',
signatureText: this.dataset.signatureText, signatureText: this.dataset.signatureText,
language: this.dataset.language, language: this.dataset.language,
paymentProvider: this.dataset.paymentProvider,
dryRun: this.dataset.dryRun === 'true', dryRun: this.dataset.dryRun === 'true',
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null, expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
withSignatureId: this.dataset.withSignatureId === 'true', withSignatureId: this.dataset.withSignatureId === 'true',

@ -69,8 +69,8 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
/> />
<FieldArea <FieldArea
v-else-if="item.type === 'formula_area' && isMathLoaded" v-else-if="item.type === 'formula_area' && (isMathLoaded || item.field.type === 'text')"
:model-value="calculateFormula(item.field)" :model-value="item.field.type === 'text' ? evalTextFormula(item.field) : calculateFormula(item.field)"
:is-inline-size="isInlineSize" :is-inline-size="isInlineSize"
:field="item.field" :field="item.field"
:area="item.area" :area="item.area"
@ -289,6 +289,7 @@ export default {
methods: { methods: {
normalizeFormula: FormulaAreas.methods.normalizeFormula, normalizeFormula: FormulaAreas.methods.normalizeFormula,
calculateFormula: FormulaAreas.methods.calculateFormula, calculateFormula: FormulaAreas.methods.calculateFormula,
evalTextFormula: FormulaAreas.methods.evalTextFormula,
scrollInContainer: FieldAreas.methods.scrollInContainer, scrollInContainer: FieldAreas.methods.scrollInContainer,
scrollIntoArea: FieldAreas.methods.scrollIntoArea, scrollIntoArea: FieldAreas.methods.scrollIntoArea,
scrollIntoField: FieldAreas.methods.scrollIntoField, scrollIntoField: FieldAreas.methods.scrollIntoField,

@ -560,6 +560,7 @@
:fields="formulaFields" :fields="formulaFields"
:values="values" :values="values"
:readonly-values="readonlyFieldValues" :readonly-values="readonlyFieldValues"
:provider="paymentProvider"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="!isSubmitting && submitStep()"
@ -1000,6 +1001,11 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
paymentProvider: {
type: String,
required: false,
default: ''
},
values: { values: {
type: Object, type: Object,
required: false, required: false,

@ -12,8 +12,8 @@
:to="findPageElementForArea(area)" :to="findPageElementForArea(area)"
> >
<FieldArea <FieldArea
v-if="isMathLoaded" v-if="isMathLoaded || field.type === 'text'"
:model-value="calculateFormula(field)" :model-value="field.type === 'text' ? evalTextFormula(field) : calculateFormula(field)"
:is-inline-size="isInlineSize" :is-inline-size="isInlineSize"
:field="field" :field="field"
:area="area" :area="area"
@ -95,6 +95,25 @@ export default {
}) })
return this.math.evaluate(transformedFormula.toLowerCase()) return this.math.evaluate(transformedFormula.toLowerCase())
},
evalTextFormula (field, depth = 0) {
if (depth > 10) return ''
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
const formulaField = this.fieldsUuidIndex[uuid]
if (formulaField?.preferences?.formula) {
if (formulaField.type === 'text') {
return this.evalTextFormula(formulaField, depth + 1)
} else if (this.isMathLoaded) {
return this.calculateFormula(formulaField)
}
}
const value = this.readonlyValues[uuid] ?? this.values[uuid]
return Array.isArray(value) ? value.join(', ') : (value ?? '')
})
} }
} }
} }

@ -107,6 +107,7 @@ const en = {
email_has_been_sent: 'Email has been sent', email_has_been_sent: 'Email has been sent',
processing: 'Processing', processing: 'Processing',
pay_with_stripe: 'Pay with Stripe', pay_with_stripe: 'Pay with Stripe',
pay_with_paypal: 'Pay with PayPal',
reupload: 'Reupload', reupload: 'Reupload',
upload: 'Upload', upload: 'Upload',
files: 'Files', files: 'Files',
@ -225,6 +226,7 @@ const es = {
email_has_been_sent: 'El correo electrónico ha sido enviado', email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando', processing: 'Procesando',
pay_with_stripe: 'Pagar con Stripe', pay_with_stripe: 'Pagar con Stripe',
pay_with_paypal: 'Pagar con PayPal',
reupload: 'Volver a subir', reupload: 'Volver a subir',
upload: 'Subir', upload: 'Subir',
files: 'Archivos', files: 'Archivos',
@ -343,6 +345,7 @@ const it = {
email_has_been_sent: "L'email è stata inviata", email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione', processing: 'Elaborazione',
pay_with_stripe: 'Paga con Stripe', pay_with_stripe: 'Paga con Stripe',
pay_with_paypal: 'Paga con PayPal',
reupload: 'Ricarica', reupload: 'Ricarica',
upload: 'Carica', upload: 'Carica',
files: 'File', files: 'File',
@ -461,6 +464,7 @@ const de = {
email_has_been_sent: 'E-Mail wurde gesendet', email_has_been_sent: 'E-Mail wurde gesendet',
processing: 'Verarbeitung', processing: 'Verarbeitung',
pay_with_stripe: 'Mit Stripe bezahlen', pay_with_stripe: 'Mit Stripe bezahlen',
pay_with_paypal: 'Mit PayPal bezahlen',
reupload: 'Erneut hochladen', reupload: 'Erneut hochladen',
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien', files: 'Dateien',
@ -579,6 +583,7 @@ const fr = {
email_has_been_sent: "L'e-mail a été envoyé", email_has_been_sent: "L'e-mail a été envoyé",
processing: 'Traitement en cours', processing: 'Traitement en cours',
pay_with_stripe: 'Payer avec Stripe', pay_with_stripe: 'Payer avec Stripe',
pay_with_paypal: 'Payer avec PayPal',
reupload: 'Retéléverser', reupload: 'Retéléverser',
upload: 'Téléverser', upload: 'Téléverser',
files: 'Fichiers', files: 'Fichiers',
@ -697,6 +702,7 @@ const pl = {
email_has_been_sent: 'E-mail został wysłany', email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie', processing: 'Przetwarzanie',
pay_with_stripe: 'Zapłać za pomocą Stripe', pay_with_stripe: 'Zapłać za pomocą Stripe',
pay_with_paypal: 'Zapłać za pomocą PayPal',
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki',
@ -815,6 +821,7 @@ const uk = {
email_has_been_sent: 'Електронний лист був відправлений', email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка', processing: 'Обробка',
pay_with_stripe: 'Сплатити за допомогою Stripe', pay_with_stripe: 'Сплатити за допомогою Stripe',
pay_with_paypal: 'Сплатити за допомогою PayPal',
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли', files: 'Файли',
@ -933,6 +940,7 @@ const cs = {
email_has_been_sent: 'E-mail byl odeslán', email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování', processing: 'Zpracování',
pay_with_stripe: 'Zaplatit přes Stripe', pay_with_stripe: 'Zaplatit přes Stripe',
pay_with_paypal: 'Zaplatit přes PayPal',
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory', files: 'Soubory',
@ -1051,6 +1059,7 @@ const pt = {
email_has_been_sent: 'Email enviado', email_has_been_sent: 'Email enviado',
processing: 'Processamento', processing: 'Processamento',
pay_with_stripe: 'Pagar com Stripe', pay_with_stripe: 'Pagar com Stripe',
pay_with_paypal: 'Pagar com PayPal',
reupload: 'Recarregar', reupload: 'Recarregar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos', files: 'Arquivos',
@ -1169,6 +1178,7 @@ const he = {
email_has_been_sent: 'האימייל נשלח', email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד', processing: 'מעבד',
pay_with_stripe: 'שלם עם סטרייפ', pay_with_stripe: 'שלם עם סטרייפ',
pay_with_paypal: 'שלם עם פייפאל',
reupload: 'העלה שוב', reupload: 'העלה שוב',
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים', files: 'קבצים',
@ -1287,6 +1297,7 @@ const nl = {
email_has_been_sent: 'E-mail is verzonden', email_has_been_sent: 'E-mail is verzonden',
processing: 'Verwerken', processing: 'Verwerken',
pay_with_stripe: 'Betalen met Stripe', pay_with_stripe: 'Betalen met Stripe',
pay_with_paypal: 'Betalen met PayPal',
reupload: 'Opnieuw uploaden', reupload: 'Opnieuw uploaden',
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden', files: 'Bestanden',
@ -1405,6 +1416,7 @@ const ar = {
email_has_been_sent: 'تم إرسال البريد الإلكتروني', email_has_been_sent: 'تم إرسال البريد الإلكتروني',
processing: 'جارٍ المعالجة', processing: 'جارٍ المعالجة',
pay_with_stripe: 'الدفع بواسطة Stripe', pay_with_stripe: 'الدفع بواسطة Stripe',
pay_with_paypal: 'الدفع بواسطة PayPal',
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات', files: 'الملفات',
@ -1523,6 +1535,7 @@ const ko = {
email_has_been_sent: '이메일이 전송되었습니다', email_has_been_sent: '이메일이 전송되었습니다',
processing: '처리 중', processing: '처리 중',
pay_with_stripe: '스트라이프로 결제', pay_with_stripe: '스트라이프로 결제',
pay_with_paypal: 'PayPal로 결제',
reupload: '다시 업로드', reupload: '다시 업로드',
upload: '업로드', upload: '업로드',
files: '파일', files: '파일',
@ -1641,6 +1654,7 @@ const ja = {
email_has_been_sent: 'メールが送信されました', email_has_been_sent: 'メールが送信されました',
processing: '処理中', processing: '処理中',
pay_with_stripe: 'Stripeで支払う', pay_with_stripe: 'Stripeで支払う',
pay_with_paypal: 'PayPalで支払う',
reupload: '再アップロード', reupload: '再アップロード',
upload: 'アップロード', upload: 'アップロード',
files: 'ファイル', files: 'ファイル',

@ -1,6 +1,6 @@
<template> <template>
<label <label
v-if="!modelValue && !sessionId" v-if="!modelValue && !isProcessing"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
> >
<MarkdownContent <MarkdownContent
@ -25,14 +25,14 @@
class="hidden" class="hidden"
> >
<div <div
v-if="modelValue && !sessionId" v-if="modelValue && !isProcessing"
class=" text-2xl mb-2" class=" text-2xl mb-2"
> >
{{ t('already_paid') }} {{ t('already_paid') }}
</div> </div>
<div v-else> <div v-else>
<button <button
v-if="sessionId" v-if="isProcessing"
type="button" type="button"
disabled disabled
class="base-button w-full modal-save-button" class="base-button w-full modal-save-button"
@ -49,8 +49,8 @@
v-else v-else
:id="field.uuid" :id="field.uuid"
type="button" type="button"
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full" class="btn text-white text-lg w-full"
:class="{ disabled: isCreatingCheckout }" :class="[isPaypal ? 'bg-[#003087] hover:bg-[#012169]' : 'bg-[#7B73FF] hover:bg-[#0A2540]', { disabled: isCreatingCheckout }]"
:disabled="isCreatingCheckout" :disabled="isCreatingCheckout"
@click.prevent="postCheckout" @click.prevent="postCheckout"
> >
@ -59,12 +59,16 @@
width="22" width="22"
class="animate-spin" class="animate-spin"
/> />
<IconBrandPaypal
v-else-if="isPaypal"
width="22"
/>
<IconBrandStripe <IconBrandStripe
v-else v-else
width="22" width="22"
/> />
<span> <span>
{{ t('pay_with_stripe') }} {{ isPaypal ? t('pay_with_paypal') : t('pay_with_stripe') }}
</span> </span>
</button> </button>
</div> </div>
@ -72,13 +76,14 @@
</template> </template>
<script> <script>
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue' import { IconBrandStripe, IconBrandPaypal, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
export default { export default {
name: 'PaymentStep', name: 'PaymentStep',
components: { components: {
IconBrandStripe, IconBrandStripe,
IconBrandPaypal,
MarkdownContent, MarkdownContent,
IconInnerShadowTop, IconInnerShadowTop,
IconLoader IconLoader
@ -111,12 +116,18 @@ export default {
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
},
provider: {
type: String,
required: false,
default: ''
} }
}, },
emits: ['focus', 'submit', 'update:model-value', 'attached'], emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () { data () {
return { return {
isCreatingCheckout: false, isCreatingCheckout: false,
isProcessing: false,
isMathLoaded: false isMathLoaded: false
} }
}, },
@ -131,8 +142,14 @@ export default {
queryParams () { queryParams () {
return new URLSearchParams(window.location.search) return new URLSearchParams(window.location.search)
}, },
isPaypal () {
return this.provider === 'paypal'
},
paymentEndpoint () {
return this.isPaypal ? '/api/paypal_payments' : '/api/stripe_payments'
},
sessionId () { sessionId () {
return this.queryParams.get('stripe_session_id') return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
}, },
defaultName () { defaultName () {
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) { if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
@ -159,6 +176,8 @@ export default {
}, },
async mounted () { async mounted () {
if (this.sessionId) { if (this.sessionId) {
this.isProcessing = true
this.$emit('submit') this.$emit('submit')
} }
@ -175,6 +194,21 @@ export default {
} }
}, },
methods: { methods: {
normalizedPaymentUrl () {
const url = new URL(window.location.href)
const keys = ['stripe_session_id', 'paypal_order_id', 'field_uuid']
if (this.isPaypal) {
keys.push('token', 'PayerID')
}
keys.forEach((key) => url.searchParams.delete(key))
return url.toString()
},
normalizedPaymentParams () {
window.history.replaceState({}, document.title, this.normalizedPaymentUrl())
},
calculateFormula () { calculateFormula () {
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => { const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
return this.readonlyValues[uuid] || this.values[uuid] || 0.0 return this.readonlyValues[uuid] || this.values[uuid] || 0.0
@ -195,7 +229,7 @@ export default {
}, },
async submit () { async submit () {
if (this.sessionId) { if (this.sessionId) {
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, { return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug submitter_slug: this.submitterSlug
@ -203,16 +237,22 @@ export default {
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) { if (resp.status === 422 || resp.status === 500) {
this.isProcessing = false
const data = await resp.json() const data = await resp.json()
alert(data.error || 'Unexpected error') if (data.error) {
alert(data.error)
}
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
const attachment = await resp.json() const attachment = await resp.json()
window.history.replaceState({}, document.title, window.location.pathname) this.isProcessing = false
this.normalizedPaymentParams()
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment) this.$emit('attached', attachment)
@ -226,13 +266,13 @@ export default {
postCheckout ({ checkStatus } = {}) { postCheckout ({ checkStatus } = {}) {
this.isCreatingCheckout = true this.isCreatingCheckout = true
fetch(this.baseUrl + '/api/stripe_payments', { fetch(this.baseUrl + this.paymentEndpoint, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug, submitter_slug: this.submitterSlug,
field_uuid: this.field.uuid, field_uuid: this.field.uuid,
check_status: checkStatus, check_status: checkStatus,
referer: document.location.href referer: this.normalizedPaymentUrl()
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {

@ -39,7 +39,7 @@
required required
maxlength="6" maxlength="6"
autofocus autofocus
inputmode="decimal" inputmode="numeric"
@input="onInputCode" @input="onInputCode"
> >
<div class="flex justify-between mt-2 -mb-2 md:-mb-4"> <div class="flex justify-between mt-2 -mb-2 md:-mb-4">

@ -164,7 +164,7 @@
v-if="withSendButton" v-if="withSendButton"
id="send_button" id="send_button"
:href="`/templates/${template.id}/submissions/new?with_link=true`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" :data-turbo-frame="isMobile && isBreakpointLg ? '_top' : 'modal'"
class="white-button md:!px-6" class="white-button md:!px-6"
@click="maybeShowErrorTemplateAlert" @click="maybeShowErrorTemplateAlert"
> >
@ -202,7 +202,7 @@
</button> </button>
<div <div
class="dropdown dropdown-end" class="dropdown dropdown-end"
:class="{ 'dropdown-open': isDownloading }" :class="{ 'dropdown-open': isDownloading || isPreviewLoading }"
> >
<label <label
tabindex="0" tabindex="0"
@ -221,15 +221,23 @@
:href="`/templates/${template.id}/form`" :href="`/templates/${template.id}/form`"
data-turbo="false" data-turbo="false"
class="flex items-center justify-center space-x-2" class="flex items-center justify-center space-x-2"
@click.exact="isPreviewLoading = true"
> >
<IconEye class="w-6 h-6 flex-shrink-0" /> <IconInnerShadowTop
v-if="isPreviewLoading"
class="animate-spin w-6 h-6 flex-shrink-0"
/>
<IconEye
v-else
class="w-6 h-6 flex-shrink-0"
/>
<span class="whitespace-nowrap">{{ t('save_and_preview') }}</span> <span class="whitespace-nowrap">{{ t('save_and_preview') }}</span>
</a> </a>
</li> </li>
<li> <li>
<a <a
:href="`/templates/${template.id}/preferences`" :href="`/templates/${template.id}/preferences`"
data-turbo-frame="modal" :data-turbo-frame="isMobile && isBreakpointLg ? '_top' : 'modal'"
class="flex space-x-2" class="flex space-x-2"
@click="closeDropdown" @click="closeDropdown"
> >
@ -281,6 +289,10 @@
:href="`/templates/${template.id}`" :href="`/templates/${template.id}`"
class="base-button" class="base-button"
> >
<IconX
width="20"
class="md:hidden stroke-2"
/>
<span class="hidden md:inline"> <span class="hidden md:inline">
{{ t('back') }} {{ t('back') }}
</span> </span>
@ -779,7 +791,10 @@ export default {
withVerification: this.withVerification, withVerification: this.withVerification,
withKba: this.withKba, withKba: this.withKba,
withPayment: this.withPayment, withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected, isStripeConnected: this.isStripeConnected,
withStripe: this.withStripe,
withPaypal: this.withPaypal,
isPaypalConnected: this.isPaypalConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
withConditions: this.withConditions, withConditions: this.withConditions,
withCustomFields: this.withCustomFields, withCustomFields: this.withCustomFields,
@ -1049,7 +1064,22 @@ export default {
required: false, required: false,
default: false default: false
}, },
isPaymentConnected: { isStripeConnected: {
type: Boolean,
required: false,
default: false
},
withStripe: {
type: Boolean,
required: false,
default: true
},
withPaypal: {
type: Boolean,
required: false,
default: false
},
isPaypalConnected: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
@ -1100,6 +1130,7 @@ export default {
documentRefs: [], documentRefs: [],
isBreakpointLg: false, isBreakpointLg: false,
isDownloading: false, isDownloading: false,
isPreviewLoading: false,
isLoadingBlankPage: false, isLoadingBlankPage: false,
isSaving: false, isSaving: false,
isDetectingPageFields: false, isDetectingPageFields: false,
@ -1241,7 +1272,7 @@ export default {
return formulaFields.reduce((acc, f) => { return formulaFields.reduce((acc, f) => {
if (this.conditionalFieldIndex[f.uuid] !== false) { if (this.conditionalFieldIndex[f.uuid] !== false) {
acc[f.uuid] = this.calculateFormula(f) acc[f.uuid] = f.type === 'text' ? this.evalTextFormula(f) : this.calculateFormula(f)
} }
return acc return acc
@ -1363,6 +1394,10 @@ export default {
if (document.location.search?.includes('stripe_connect_success')) { if (document.location.search?.includes('stripe_connect_success')) {
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus() document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
} }
if (document.location.search?.includes('paypal_connect_success')) {
document.querySelector('a[href^="/auth/paypal_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
}
}) })
this.template.schema.forEach((item) => { this.template.schema.forEach((item) => {
@ -1504,6 +1539,25 @@ export default {
return this.math.evaluate(transformedFormula.toLowerCase()) return this.math.evaluate(transformedFormula.toLowerCase())
}, },
evalTextFormula (field, depth = 0) {
if (depth > 10) return ''
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
const formulaField = this.fieldsUuidIndex[uuid]
if (formulaField?.preferences?.formula) {
if (formulaField.type === 'text') {
return this.evalTextFormula(formulaField, depth + 1)
} else if (this.isMathLoaded) {
return this.calculateFormula(formulaField)
}
}
const value = formulaField?.default_value
return Array.isArray(value) ? value.join(', ') : (value ?? '')
})
},
hasFormulaDependencyValue (field) { hasFormulaDependencyValue (field) {
const normalized = this.normalizeFormula(field.preferences.formula) const normalized = this.normalizeFormula(field.preferences.formula)

@ -494,6 +494,7 @@ import FieldType from './field_type.vue'
import FieldSettings from './field_settings.vue' import FieldSettings from './field_settings.vue'
import ContextSubmenu from './field_context_submenu.vue' import ContextSubmenu from './field_context_submenu.vue'
import ContextModal from './field_context_modal.vue' import ContextModal from './field_context_modal.vue'
import { stripeConnectedRef } from './payment_settings'
export default { export default {
name: 'FieldContextMenu', name: 'FieldContextMenu',
@ -610,7 +611,7 @@ export default {
return this.withCondition && !['stamp', 'heading'].includes(this.field.type) return this.withCondition && !['stamp', 'heading'].includes(this.field.type)
}, },
showFormula () { showFormula () {
return this.field.type === 'number' || this.field.type === 'payment' return this.field.type === 'number' || this.field.type === 'payment' || !!this.field.preferences?.formula
}, },
showRequired () { showRequired () {
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type) return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
@ -716,12 +717,18 @@ export default {
currentCurrency () { currentCurrency () {
return this.field.preferences?.currency || 'USD' return this.field.preferences?.currency || 'USD'
}, },
isStripeConnected: () => stripeConnectedRef.value,
priceTypeOptions () { priceTypeOptions () {
return [ const options = [
{ value: 'one_off', label: this.t('fixed') }, { value: 'one_off', label: this.t('fixed') },
{ value: 'formula', label: this.t('formula') }, { value: 'formula', label: this.t('formula') }
{ value: 'payment_link', label: this.t('payment_link') }
] ]
if (this.isStripeConnected) {
options.push({ value: 'payment_link', label: this.t('payment_link') })
}
return options
}, },
currentPriceType () { currentPriceType () {
if (this.field.preferences?.formula) { if (this.field.preferences?.formula) {

@ -500,7 +500,7 @@
</label> </label>
</li> </li>
<li <li
v-if="field.type == 'number'" v-if="field.type == 'number' || field.preferences?.formula"
class="field-settings-formula" class="field-settings-formula"
> >
<label <label

@ -61,7 +61,10 @@
@input="resizeTextarea" @input="resizeTextarea"
/> />
</div> </div>
<div class="mb-3 mt-1"> <div
v-if="field.type !== 'text'"
class="mb-3 mt-1"
>
<div <div
target="blank" target="blank"
class="text-sm mb-2 inline space-x-2 font-mono" class="text-sm mb-2 inline space-x-2 font-mono"
@ -161,7 +164,9 @@ export default {
computed: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.field && this.isNumberField(f) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) { const isAllowed = this.field.type === 'text' ? this.isTextField(f) : this.isNumberField(f)
if (f !== this.field && isAllowed && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
acc.push(f) acc.push(f)
} }
@ -179,6 +184,9 @@ export default {
isNumberField (field) { isNumberField (field) {
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/))) return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
}, },
isTextField (field) {
return ['text', 'number', 'select', 'radio', 'cells', 'phone'].includes(field.type)
},
humanizeFormula (text) { humanizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, uuid) => { return text.replace(/{{(.*?)}}/g, (match, uuid) => {
const foundField = this.template.fields.find((f) => f.uuid === uuid) const foundField = this.template.fields.find((f) => f.uuid === uuid)

@ -1,7 +1,7 @@
<template> <template>
<span <span
class="dropdown dropdown-end field-settings-dropdown" class="dropdown dropdown-end field-settings-dropdown"
:class="{ 'dropdown-open': withForceOpen && ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }" :class="{ 'dropdown-open': withForceOpen && !field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id && !isLoading }"
> >
<label <label
tabindex="0" tabindex="0"
@ -21,7 +21,7 @@
@click="closeDropdown" @click="closeDropdown"
> >
<div <div
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)" v-if="isPaypal || (!('price_id' in field.preferences) && !('payment_link_id' in field.preferences))"
class="field-settings-currency py-1.5 px-1 relative" class="field-settings-currency py-1.5 px-1 relative"
@click.stop @click.stop
> >
@ -52,14 +52,14 @@
@click.stop @click.stop
> >
<input <input
v-if="'payment_link_id' in field.preferences" v-if="isStripe && 'payment_link_id' in field.preferences"
v-model="field.preferences.payment_link_id" v-model="field.preferences.payment_link_id"
placeholder="plink_XXXXX" placeholder="plink_XXXXX"
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"
> >
<input <input
v-else-if="'price_id' in field.preferences" v-else-if="isStripe && 'price_id' in field.preferences"
v-model="field.preferences.price_id" v-model="field.preferences.price_id"
placeholder="Price ID: price_XXXXX" placeholder="Price ID: price_XXXXX"
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"
@ -89,7 +89,10 @@
> >
{{ 'payment_link_id' in field.preferences ? t('payment_link') : t('price') }} {{ 'payment_link_id' in field.preferences ? t('payment_link') : t('price') }}
</label> </label>
<div class="flex items-center justify-center"> <div
v-if="isStripe && stripeConnected"
class="flex items-center justify-center"
>
<a <a
href="#" href="#"
class="hover:underline" class="hover:underline"
@ -120,21 +123,22 @@
</div> </div>
</div> </div>
<div <div
v-if="!isConnected || isOauthSuccess" v-if="!stripeConnected && !paypalConnected"
class="field-settings-stripe-connect py-1.5 px-1 relative" class="flex flex-col gap-1 py-1.5 px-1"
@click.stop @click.stop
> >
<div <a
v-if="isConnected && isOauthSuccess" v-if="withPaypal"
class="text-sm text-center" :href="paypalConnectUrl"
target="_blank"
data-turbo="false"
class="btn bg-[#003087] hover:bg-[#012169] btn-sm text-white w-full"
> >
<IconCircleCheck <IconBrandPaypal class="w-4 h-4 inline" />
class="inline text-green-600 w-4 h-4" <span>Connect PayPal</span>
/> </a>
Stripe Connected
</div>
<form <form
v-if="!isConnected" v-if="withStripe"
data-turbo="false" data-turbo="false"
action="/auth/stripe_connect" action="/auth/stripe_connect"
accept-charset="UTF-8" accept-charset="UTF-8"
@ -170,33 +174,25 @@
:disabled="isLoading" :disabled="isLoading"
class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full" class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full"
> >
<span <IconInnerShadowTop
v-if="isLoading" v-if="isLoading"
class="flex items-center space-x-1" class="w-4 h-4 animate-spin inline"
> />
<IconInnerShadowTop <IconBrandStripe
class="w-4 h-4 animate-spin inline"
/>
<span>
Connect Stripe
</span>
</span>
<span
v-else v-else
class="flex items-center space-x-1" class="w-4 h-4 inline"
> />
<IconBrandStripe <span>Connect Stripe</span>
class="w-4 h-4 inline"
/>
<span>
Connect Stripe
</span>
</span>
</button> </button>
</form> </form>
</div>
<div
v-if="isStripe && !stripeConnected"
class="px-1 pb-1.5"
@click.stop
>
<a <a
v-if="!isConnected" class="block link text-center text-xs"
class="block link text-center mt-1"
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease" href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
target="_blank" target="_blank"
data-turbo="false" data-turbo="false"
@ -272,24 +268,25 @@
</template> </template>
<script> <script>
import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue' import { IconMathFunction, IconSettings, IconInfoCircle, IconBrandStripe, IconBrandPaypal, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue'
import { ref } from 'vue' import { ref } from 'vue'
const isConnected = ref(false) export const stripeConnectedRef = ref(false)
export const paypalConnectedRef = ref(false)
export default { export default {
name: 'PaymentSettings', name: 'PaymentSettings',
components: { components: {
IconSettings, IconSettings,
IconCircleCheck,
IconRouteAltLeft, IconRouteAltLeft,
IconInfoCircle, IconInfoCircle,
IconForms, IconForms,
IconMathFunction, IconMathFunction,
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe,
IconBrandPaypal
}, },
inject: ['backgroundColor', 'save', 'currencies', 't', 'isPaymentConnected'], inject: ['backgroundColor', 'save', 'currencies', 't', 'isStripeConnected', 'isPaypalConnected', 'withStripe', 'withPaypal'],
props: { props: {
field: { field: {
type: Object, type: Object,
@ -318,13 +315,27 @@ export default {
} }
}, },
computed: { computed: {
isConnected: () => isConnected.value, stripeConnected: () => stripeConnectedRef.value,
isOauthSuccess () { paypalConnected: () => paypalConnectedRef.value,
return document.location.search?.includes('stripe_connect_success') provider () {
if (this.paypalConnected && !this.stripeConnected) return 'paypal'
if (this.stripeConnected && !this.paypalConnected) return 'stripe'
if (this.withPaypal && !this.withStripe) return 'paypal'
return 'stripe'
},
isStripe () {
return this.provider === 'stripe'
},
isPaypal () {
return this.provider === 'paypal'
}, },
redirectUri () { redirectUri () {
return document.location.origin + '/auth/stripe_connect/callback' return document.location.origin + '/auth/stripe_connect/callback'
}, },
paypalConnectUrl () {
return `/auth/paypal_connect?redir=${encodeURIComponent(document.location.href)}`
},
defaultCurrencies () { defaultCurrencies () {
return ['USD', 'EUR', 'GBP', 'CAD', 'AUD'] return ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
}, },
@ -363,26 +374,53 @@ export default {
mounted () { mounted () {
this.field.preferences.currency ||= this.defaultCurrency this.field.preferences.currency ||= this.defaultCurrency
isConnected.value ||= this.isPaymentConnected stripeConnectedRef.value ||= this.isStripeConnected
paypalConnectedRef.value ||= this.isPaypalConnected
if (this.withStripe && !this.stripeConnected && !this.paypalConnected) {
this.checkStripeStatus()
}
if (!this.isConnected) { if (this.withPaypal && !this.paypalConnected && !this.stripeConnected) {
this.checkStatus() this.checkPaypalStatus()
window.addEventListener('focus', this.onWindowFocus)
} }
}, },
beforeUnmount () {
window.removeEventListener('focus', this.onWindowFocus)
},
methods: { methods: {
checkStatus () { onWindowFocus () {
if (this.withPaypal && !this.paypalConnected) {
this.checkPaypalStatus()
}
},
checkStripeStatus () {
this.isLoading = true this.isLoading = true
fetch('/api/stripe_connect').then(async (resp) => { fetch('/api/stripe_connect').then(async (resp) => {
const { status } = await resp.json() const { status } = await resp.json()
if (status === 'connected') { if (status === 'connected') {
isConnected.value = true stripeConnectedRef.value = true
window.removeEventListener('focus', this.onWindowFocus)
} }
}).finally(() => { }).finally(() => {
this.isLoading = false this.isLoading = false
}) })
}, },
checkPaypalStatus () {
fetch('/api/paypal_connect').then(async (resp) => {
const { status } = await resp.json()
if (status === 'connected') {
paypalConnectedRef.value = true
window.removeEventListener('focus', this.onWindowFocus)
}
})
},
closeDropdown () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
} }

@ -3,6 +3,9 @@
class ProcessSubmitterCompletionJob class ProcessSubmitterCompletionJob
include Sidekiq::Job include Sidekiq::Job
BCC_LIMIT = 14
TooManyBcc = Class.new(StandardError)
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
submission = submitter.submission submission = submitter.submission
@ -133,16 +136,24 @@ class ProcessSubmitterCompletionJob
true true
end end
build_bcc_addresses(submission).each do |to| enqueue_bcc_completed_emails(submitter, user, is_sent_to_user)
next if is_sent_to_user && to == user.email
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
end
end end
maybe_enqueue_copy_emails(submitter) maybe_enqueue_copy_emails(submitter)
end end
def enqueue_bcc_completed_emails(submitter, user, is_sent_to_user)
bcc_addresses = build_bcc_addresses(submitter.submission)
raise TooManyBcc, submitter.account_id if Docuseal.multitenant? && bcc_addresses.size > BCC_LIMIT
bcc_addresses.each do |to|
next if is_sent_to_user && to == user.email
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
end
end
def maybe_enqueue_copy_emails(submitter) def maybe_enqueue_copy_emails(submitter)
return if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false return if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4"> <h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('account') %> <%= t('account') %>
</h1> </h1>
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
@ -53,8 +53,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('force_2fa_with_authenticator_app') %></span> <span class="text-left"><%= t('force_2fa_with_authenticator_app') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_two_factor_authentication_2fa_with_an_authenticator_app_e_g_google_authenticator_authy_all_users_signing_documents_must_pass_the_second_factor_verification_using_a_secure_code_in_addition_to_their_password') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('require_two_factor_authentication_2fa_with_an_authenticator_app_e_g_google_authenticator_authy_all_users_signing_documents_must_pass_the_second_factor_verification_using_a_secure_code_in_addition_to_their_password') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -70,8 +70,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('add_signature_id_to_the_documents') %></span> <span class="text-left"><%= t('add_signature_id_to_the_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -87,8 +87,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_signing_reason') %></span> <span class="text-left"><%= t('require_signing_reason') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -104,8 +104,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_typed_text_signatures') %></span> <span class="text-left"><%= t('allow_typed_text_signatures') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_create_signatures_by_typing_their_name_instead_of_drawing_or_uploading_one') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_signers_to_create_signatures_by_typing_their_name_instead_of_drawing_or_uploading_one') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -121,8 +121,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_resubmit_completed_forms') %></span> <span class="text-left"><%= t('allow_to_resubmit_completed_forms') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_resubmit_forms_after_completion_useful_when_corrections_or_multiple_submissions_are_needed') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_signers_to_resubmit_forms_after_completion_useful_when_corrections_or_multiple_submissions_are_needed') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -138,8 +138,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_decline_documents') %></span> <span class="text-left"><%= t('allow_to_decline_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_decline_signing_a_document_the_decline_reason_notification_will_be_sent_to_the_signature_requester') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_recipients_to_decline_signing_a_document_the_decline_reason_notification_will_be_sent_to_the_signature_requester') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<% if !Docuseal.multitenant? || can?(:manage, :disable_decline) %> <% if !Docuseal.multitenant? || can?(:manage, :disable_decline) %>
@ -147,7 +147,7 @@
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %> <%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form> </submit-form>
<% else %> <% else %>
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" data-on="change" class="flex tooltip"> <a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" data-on="change" class="flex tooltip tooltip-no-touch">
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value != false, disabled: true %> <%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value != false, disabled: true %>
</a> </a>
<% end %> <% end %>
@ -161,8 +161,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_delegate_documents') %></span> <span class="text-left"><%= t('allow_to_delegate_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_delegate_documents') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_recipients_to_delegate_documents') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<% if !Docuseal.multitenant? || can?(:manage, :delegate_form) %> <% if !Docuseal.multitenant? || can?(:manage, :delegate_form) %>
@ -170,7 +170,7 @@
<%= f.check_box :value, class: 'toggle', checked: account_config.value == true %> <%= f.check_box :value, class: 'toggle', checked: account_config.value == true %>
</submit-form> </submit-form>
<% else %> <% else %>
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" class="flex tooltip"> <a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" class="flex tooltip tooltip-no-touch">
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value == true, disabled: true %> <%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value == true, disabled: true %>
</a> </a>
<% end %> <% end %>
@ -184,8 +184,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('remember_and_pre_fill_signatures') %></span> <span class="text-left"><%= t('remember_and_pre_fill_signatures') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('save_a_users_signature_and_automatically_pre_fill_it_in_future_signing_sessions') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('save_a_users_signature_and_automatically_pre_fill_it_in_future_signing_sessions') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -201,8 +201,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('expirable_file_download_links') %></span> <span class="text-left"><%= t('expirable_file_download_links') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_document_download_links_expire_after_40_minutes_to_prevent_long_term_access_and_enhance_security') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('make_document_download_links_expire_after_40_minutes_to_prevent_long_term_access_and_enhance_security') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -218,8 +218,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_authentication_for_file_download_links') %></span> <span class="text-left"><%= t('require_authentication_for_file_download_links') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_authentication_with_user_login_or_api_key_to_access_the_document_download_links') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('require_authentication_with_user_login_or_api_key_to_access_the_document_download_links') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -235,8 +235,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('combine_completed_documents_and_audit_log') %></span> <span class="text-left"><%= t('combine_completed_documents_and_audit_log') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('combine_signed_documents_and_the_audit_log_into_a_single_pdf_file_for_easier_recordkeeping_and_compliance') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('combine_signed_documents_and_the_audit_log_into_a_single_pdf_file_for_easier_recordkeeping_and_compliance') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
@ -253,8 +253,8 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('always_enforce_signing_order') %></span> <span class="text-left"><%= t('always_enforce_signing_order') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_the_recipients_signing_order_always_enforced_so_that_the_second_signer_can_start_signing_their_part_only_after_the_first_signer_has_completed_signing') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('make_the_recipients_signing_order_always_enforced_so_that_the_second_signer_can_start_signing_their_part_only_after_the_first_signer_has_completed_signing') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">

@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">API</h1> <h1 class="text-3xl md:text-4xl font-bold">API</h1>
<%= render 'shared/test_mode_toggle' %> <%= render 'shared/test_mode_toggle' %>
</div> </div>
<div class="card bg-base-200"> <div class="card bg-base-200">

@ -1,10 +1,10 @@
<form action="<%= root_path %>" method="get" id="templates_submissions_toggle" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap"> <form action="<%= root_path %>" method="get" id="templates_submissions_toggle" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>"> <toggle-cookies data-value="templates" data-key="dashboard_view" class="tooltip tooltip-top tooltip-no-touch" data-tip="<%= t('templates') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>"> <button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
</button> </button>
</toggle-cookies> </toggle-cookies>
<toggle-cookies data-value="submissions" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('submissions') %>"> <toggle-cookies data-value="submissions" data-key="dashboard_view" class="tooltip tooltip-top tooltip-no-touch" data-tip="<%= t('submissions') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>"> <button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2') %>
</button> </button>

@ -1,4 +1,4 @@
<div class="form-control"> <div class="form-control">
<%= label_tag 'user[otp_attempt]', t('two_factor_code_from_authenticator_app'), class: 'label' %> <%= label_tag 'user[otp_attempt]', t('two_factor_code_from_authenticator_app'), class: 'label' %>
<%= text_field_tag 'user[otp_attempt]', nil, autofocus: true, autocomplete: 'off', placeholder: 'XXX-XXX', required: true, class: 'base-input' %> <%= text_field_tag 'user[otp_attempt]', nil, autofocus: true, autocomplete: 'off', inputmode: 'numeric', placeholder: 'XXXXXX', required: true, class: 'base-input' %>
</div> </div>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto min-w-0">
<h1 class="text-4xl font-bold mb-4">Email SMTP</h1> <h1 class="text-3xl md:text-4xl font-bold mb-4">Email SMTP</h1>
<% value = @encrypted_config.value || {} %> <% value = @encrypted_config.value || {} %>
<%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.fields_for :value do |ff| %> <%= f.fields_for :value do |ff| %>
@ -38,7 +38,7 @@
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %> <% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="form-control"> <div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %> <%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6"> <div class="flex flex-wrap items-center gap-x-6">
<% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %> <% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %> <%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %> <%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>

@ -1,15 +1,15 @@
<tr scope="row" class="group"> <tr class="group">
<td class="flex items-center space-x-1"> <td class="flex items-center space-x-1">
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 text-green-500') %> <%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %>
<span class="flex items-center"> <span class="flex items-center">
<%= t('docuseal_trusted_signature') %> <span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span>
<div class="tooltip ml-1" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>"> <div class="tooltip tooltip-top ml-1 shrink-0" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1') %> <%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %>
</div> </div>
</span> </span>
</td> </td>
<td> <td>
<a href="<%= "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>" class="btn btn-neutral btn-sm text-white"> <a href="<%= "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>" class="btn btn-neutral btn-sm text-white whitespace-nowrap">
<%= t('unlock_with_docuseal_pro') %> <%= t('unlock_with_docuseal_pro') %>
</a> </a>
</td> </td>

@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="md:flex-grow"> <div class="md:flex-grow">
<div class="max-w-xl"> <div class="max-w-xl">
<h1 class="text-4xl font-bold mb-4"> <h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('pdf_signature') %> <%= t('pdf_signature') %>
</h1> </h1>
<div id="result"> <div id="result">
@ -41,7 +41,7 @@
<% end %> <% end %>
</div> </div>
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 mt-8"> <div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 mt-8">
<h2 class="text-3xl font-bold"> <h2 class="text-2xl md:text-3xl font-bold">
<%= t('signing_certificates') %> <%= t('signing_certificates') %>
</h2> </h2>
<% if can?(:create, @encrypted_config) %> <% if can?(:create, @encrypted_config) %>
@ -72,7 +72,7 @@
<tbody> <tbody>
<% @pkcs_list.each do |item| %> <% @pkcs_list.each do |item| %>
<% next if item['pkcs'].blank? %> <% next if item['pkcs'].blank? %>
<tr scope="row" class="group"> <tr class="group">
<td> <td>
<%= item['name'] %> <%= item['name'] %>
</td> </td>
@ -109,7 +109,7 @@
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
<div class="flex-grow max-w-xl"> <div class="flex-grow max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8"> <div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold"> <h2 class="text-2xl md:text-3xl font-bold">
<%= t('timestamp_server') %> <%= t('timestamp_server') %>
</h2> </h2>
</div> </div>
@ -137,7 +137,7 @@
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<div class="px-1 mt-8 max-w-xl"> <div class="px-1 mt-8 max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8"> <div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold"> <h2 class="text-2xl md:text-3xl font-bold">
<%= t('preferences') %> <%= t('preferences') %>
</h2> </h2>
</div> </div>

@ -1 +1 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-google-drive"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 10l-6 10l-3 -5l6 -10z" /><path d="M9 15h12l-3 5h-12" /><path d="M15 15l-6 -10h6l6 10z" /></svg> <svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 10l-6 10l-3 -5l6 -10z" /><path d="M9 15h12l-3 5h-12" /><path d="M15 15l-6 -10h6l6 10z" /></svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 381 B

@ -1,8 +1,7 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10z" /> <path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10z" />
<path d="M7 20h10" /><path d="M9 16v4" /> <path d="M7 20h10" />
<path d="M15 16v4" /></svg>
<path d="M9 16v4" /> <path d="M9 16v4" />
<path d="M15 16v4" /> <path d="M15 16v4" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 447 B

@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="md:flex-grow"> <div class="md:flex-grow">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12"> <div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold"> <h1 class="text-3xl md:text-4xl font-bold">
<%= t('mcp_server') %> <%= t('mcp_server') %>
</h1> </h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center"> <div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
@ -67,7 +67,7 @@
</thead> </thead>
<tbody> <tbody>
<% @mcp_tokens.each do |mcp_token| %> <% @mcp_tokens.each do |mcp_token| %>
<tr scope="row"> <tr>
<td> <td>
<%= mcp_token.name %> <%= mcp_token.name %>
</td> </td>
@ -98,8 +98,8 @@
<div class="flex items-center gap-4 py-2.5"> <div class="flex items-center gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('enable_mcp_server') %></span> <span class="text-left"><%= t('enable_mcp_server') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled') %>"> <span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">

@ -3,10 +3,10 @@
<%= t('use_an_authenticator_mobile_app_like_google_authenticator_or_1password_to_scan_the_qr_code_below') %> <%= t('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: 'md:w-80 md: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' }).sub(/\A<\?xml[^>]*\?>/, '') %>
</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, inputmode: 'numeric', placeholder: 'XXXXXX', class: 'base-input text-center' %>
<span> <span>
<%= @error_message %> <%= @error_message %>
</span> </span>

@ -1,7 +1,7 @@
<%= render 'shared/turbo_modal', title: t('remove_2fa') do %> <%= render 'shared/turbo_modal', title: t('remove_2fa') do %>
<%= form_for '', url: mfa_setup_path, method: :delete, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: mfa_setup_path, method: :delete, data: { turbo_frame: :_top } do |f| %>
<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, inputmode: 'numeric', placeholder: 'XXXXXX', class: 'base-input text-center' %>
<span> <span>
<%= @error_message %> <%= @error_message %>
</span> </span>

@ -6,7 +6,7 @@
<span> <span>
<%= t('completed_documents_notification_bcc_address') %> <%= t('completed_documents_notification_bcc_address') %>
</span> </span>
<span class="tooltip" data-tip="<%= t('send_email_copy_with_completed_documents_to_a_specified_bcc_address') %>"> <span class="tooltip hidden md:inline-flex" data-tip="<%= t('send_email_copy_with_completed_documents_to_a_specified_bcc_address') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</span> </span>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4"> <h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('email_notifications') %> <%= t('email_notifications') %>
</h1> </h1>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
@ -22,7 +22,7 @@
</div> </div>
<%= render 'bcc_form', config: @bcc_config %> <%= render 'bcc_form', config: @bcc_config %>
<div class="flex justify-between items-end mb-4 mt-8"> <div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold"> <h2 class="text-2xl md:text-3xl font-bold">
<%= t('sign_request_email_reminders') %> <%= t('sign_request_email_reminders') %>
</h2> </h2>
</div> </div>

@ -1,21 +1,21 @@
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;"> <div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)"> <div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:<%= editor_tag %>#bold" data-target="<%= editor_tag %>.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:<%= editor_tag %>#bold" data-target="<%= editor_tag %>.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %> <%= svg_icon('bold', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)"> <div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:<%= editor_tag %>#italic" data-target="<%= editor_tag %>.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:<%= editor_tag %>#italic" data-target="<%= editor_tag %>.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %> <%= svg_icon('italic', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)"> <div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:<%= editor_tag %>#underline" data-target="<%= editor_tag %>.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:<%= editor_tag %>#underline" data-target="<%= editor_tag %>.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %> <%= svg_icon('underline', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)"> <div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:<%= editor_tag %>#linkSelection" data-target="<%= editor_tag %>.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:<%= editor_tag %>#linkSelection" data-target="<%= editor_tag %>.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %> <%= svg_icon('link', class: 'w-4 h-4') %>
</button> </button>
@ -23,12 +23,12 @@
</div> </div>
<div class="mx-2 h-5 border-l border-base-content/20"></div> <div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)"> <div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-action="click:<%= editor_tag %>#undo" data-target="<%= editor_tag %>.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:<%= editor_tag %>#undo" data-target="<%= editor_tag %>.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %> <%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)"> <div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-action="click:<%= editor_tag %>#redo" data-target="<%= editor_tag %>.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:<%= editor_tag %>#redo" data-target="<%= editor_tag %>.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %> <%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button> </button>
@ -37,8 +37,9 @@
<% if local_assigns[:variables]&.any? %> <% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %> <% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto"> <div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer"> <label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer whitespace-nowrap">
<%= t('add_variable') %> <span class="hidden sm:inline"><%= t('add_variable') %></span>
<span class="sm:hidden"><%= t('variable') %></span>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %> <%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label> </label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50"> <div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<p class="text-4xl font-bold mb-4"> <p class="text-3xl md:text-4xl font-bold mb-4">
<%= t('email_templates') %> <%= t('email_templates') %>
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
@ -9,11 +9,11 @@
<%= render 'documents_copy_email_form' %> <%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %> <%= render 'submitter_completed_email_form' %>
</div> </div>
<p class="text-4xl font-bold mb-4 mt-8"> <p class="text-3xl md:text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %> <%= t('company_logo') %>
</p> </p>
<%= render 'logo_form' %> <%= render 'logo_form' %>
<p class="text-4xl font-bold mb-4 mt-8"> <p class="text-3xl md:text-4xl font-bold mb-4 mt-8">
<%= t('submission_form') %> <%= t('submission_form') %>
</p> </p>
<div class="space-y-4"> <div class="space-y-4">

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4"> <h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('profile') %> <%= t('profile') %>
</h1> </h1>
<%= form_for current_user, url: update_contact_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for current_user, url: update_contact_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>

@ -0,0 +1,7 @@
<div class="mb-2 w-full">
<a href="<%= root_path %>" class="flex items-center space-x-0.5 font-medium">
<%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %>
<span><%= t('back') %></span>
</a>
</div>
<%= render 'shared/settings_nav', expanded: true %>

@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30' %> <% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30 order-1 order-2 order-3 order-4 order-5 lg:inline lg:hidden' %>

@ -1,4 +1,4 @@
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub"> <a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom tooltip-no-touch" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium"> <span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %> <%= svg_icon('start', class: 'h-3 w-3') %>
<span>18k</span> <span>18k</span>

@ -28,7 +28,13 @@
<span class="text-sm align-text-top"><%= current_user.initials %></span> <span class="text-sm align-text-top"><%= current_user.initials %></span>
</label> </label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[160px] text-right"> <ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[160px] text-right">
<li> <li class="md:hidden">
<%= link_to settings_path, class: 'flex items-center' do %>
<%= svg_icon('adjustments', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1"><%= t('settings') %></span>
<% end %>
</li>
<li class="hidden md:list-item">
<%= link_to settings_profile_index_path, class: 'flex items-center' do %> <%= link_to settings_profile_index_path, class: 'flex items-center' do %>
<%= svg_icon('adjustments', class: 'w-5 h-5 flex-shrink-0 stroke-2') %> <%= svg_icon('adjustments', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1"><%= t('profile') %></span> <span class="mr-1"><%= t('profile') %></span>

@ -1,14 +1,17 @@
<% if pagy.pages > 1 %> <% if pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between"> <% left_additional_html = local_assigns[:left_additional_html] %>
<div class="hidden md:block text-sm"> <div class="flex flex-col md:flex-row md:items-center <%= left_additional_html.present? ? 'mt-3' : 'mt-6' %> mb-6 md:my-6 justify-center md:justify-between">
<% if pagy.count.nil? %> <div class="text-sm <%= 'mb-2 md:mb-0' if left_additional_html.present? %>">
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %> <span class="hidden md:inline">
<% else %> <% if pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %> <%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
<% end %> <% else %>
<%= local_assigns[:left_additional_html] %> <%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %>
<% end %>
</span>
<%= left_additional_html %>
</div> </div>
<div class="flex items-center space-x-1.5"> <div class="flex items-center space-x-1.5 justify-center md:justify-end">
<%= local_assigns[:right_additional_html] %> <%= local_assigns[:right_additional_html] %>
<div class="join"> <div class="join">
<% if pagy.previous %> <% if pagy.previous %>

@ -15,7 +15,7 @@
</div> </div>
<% end %> <% end %>
<search-input class="group flex items-center max-md:focus-within:flex-1 <%= 'max-md:flex-1' if params[:q].present? %>"> <search-input class="group flex items-center max-md:focus-within:flex-1 <%= 'max-md:flex-1' if params[:q].present? %>">
<input id="search" name="q" value="<%= params[:q] %>" enterkeyhint="search" class="input text-lg pr-10 -mr-12 w-0 md:w-60 max-md:group-focus-within:w-full <%= 'pl-8 input-outlined max-md:w-full' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>"> <input id="search" name="q" value="<%= params[:q] %>" enterkeyhint="search" class="input text-lg pr-10 -mr-12 w-0 md:w-60 max-md:group-focus-within:w-full <%= 'pl-8 input-outlined w-full' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle"> <button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle">
<span class="enabled"> <span class="enabled">
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>

@ -1,150 +1,168 @@
<div class="block w-full md:w-52 flex-none"> <% expanded = local_assigns[:expanded] %>
<menu-active> <div class="<%= expanded ? 'block -mx-3 md:mx-0' : 'w-full md:w-52 flex-none' %>">
<ul id="account_settings_menu" class="menu px-0"> <% unless expanded %>
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5"><%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %><span><%= t('back') %></span></a></li> <div class="md:hidden w-full">
<li class="menu-title py-0 !bg-transparent"> <a href="<%= settings_path %>" class="flex items-center space-x-0.5 font-medium">
<span class="!bg-transparent"><%= t('settings') %></span> <%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %>
</li> <span><%= t('settings') %></span>
<li></li> </a>
<li> </div>
<%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %> <% end %>
</li> <div class="<%= 'hidden md:block' unless expanded %>">
<li> <menu-active>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %> <ul id="account_settings_menu" class="menu px-0">
</li> <% unless expanded %>
<% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %> <li class="menu-title py-0 !bg-transparent mb-3 -mt-5">
<li> <a href="<%= root_path %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5">
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %> <%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %>
</li> <span><%= t('back') %></span>
<% end %> </a>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
<li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% end %> <li class="menu-title py-0 !bg-transparent">
<% if can?(:read, AccountConfig) %> <span class="!bg-transparent"><%= t('settings') %></span>
<li>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <li></li>
<% if can?(:read, AccountConfig) %>
<li> <li>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %>
<% if can?(:read, User) %>
<li> <li>
<%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<%= render 'shared/settings_nav_extra' %>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %>
<li> <li>
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% end %> <% unless Docuseal.multitenant? %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
<% if can?(:read, WebhookUrl) %> <li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if can?(:read, AccountConfig) %>
<li> <li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% end %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<li> <li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %> <%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %>
<%= t('embedding') %>
<% end %>
</li> </li>
<% end %> <% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %> <% if can?(:read, AccountConfig) %>
<li> <li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if !Docuseal.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %> <% if can?(:read, User) %>
<li> <li>
<%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<%= render 'shared/settings_nav_extra2' %> <%= render 'shared/settings_nav_extra' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <% if Docuseal.demo? || !Docuseal.multitenant? %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %> <% if can?(:read, AccessToken) %>
<li> <li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle"> <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
<span class="mr-2 w-full">
<%= t('test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing? %>
</submit-form>
</label>
</li> </li>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
</ul> <% if can?(:read, WebhookUrl) %>
</menu-active> <li>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %> <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
<div id="support_channels" class="mx-4 border-t border-base-300 hidden md:block"> </li>
<div class="text-sm mt-3"> <% end %>
<%= t('need_help_ask_a_question_') %> <% end %>
</div> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<div class="flex mt-3 space-x-3"> <li>
<div class="tooltip" data-tip="GitHub"> <%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <%= t('plans') %>
<%= svg_icon('brand_github', class: 'w-8 h-8') %> <span class="badge badge-warning"><%= t('pro') %></span>
</a> <% end %>
</div> </li>
<div class="tooltip" data-tip="<%= t('discord_community') %>"> <% end %>
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<%= svg_icon('brand_discord', class: 'w-8 h-8') %> <li>
</a> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<%= t('embedding') %>
<% end %>
</li>
<% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if !Docuseal.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %>
<li>
<%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra2' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %>
<li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
<%= t('test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing? %>
</submit-form>
</label>
</li>
<% end %>
<% end %>
<% end %>
</ul>
</menu-active>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div id="support_channels" class="mx-4 border-t border-base-300 <%= 'hidden md:block' unless expanded %>">
<div class="text-sm mt-3">
<%= t('need_help_ask_a_question_') %>
</div> </div>
<%= capture do %> <div class="flex mt-3 space-x-3">
<div class="tooltip" data-tip="<%= t('ai_assistant') %>"> <div class="tooltip tooltip-no-touch" data-tip="GitHub">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('message', class: 'w-8 h-8') %> <%= svg_icon('brand_github', class: 'w-8 h-8') %>
</a>
</div>
<div class="tooltip tooltip-no-touch" data-tip="<%= t('discord_community') %>">
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
</a> </a>
</div> </div>
<%= capture do %>
<div class="tooltip tooltip-no-touch" data-tip="<%= t('ai_assistant') %>">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('message', class: 'w-8 h-8') %>
</a>
</div>
<% end %>
</div>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline">
<%= Docuseal::SUPPORT_EMAIL %>
</a>
<% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
<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 %> <% end %>
</div> </div>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center"> <% end %>
<%= Docuseal::SUPPORT_EMAIL %> </div>
</a>
<% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
<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>
<% end %>
</div> </div>

@ -1,7 +1,8 @@
<% offset = local_assigns[:title] ? '45px' : '0px' %>
<turbo-frame id="drawer"> <turbo-frame id="drawer">
<turbo-modal class="modal modal-open items-start !animate-none justify-end overflow-y-auto !justify-normal md:!justify-end" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>"> <turbo-modal class="modal modal-open items-start !animate-none justify-end overflow-y-auto !justify-normal md:!justify-end" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div> <div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen relative w-full relative"> <div class="bg-base-100 min-h-screen max-h-screen relative w-full min-w-0">
<% if local_assigns[:title] %> <% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-4 items-center border-b pb-2 font-medium"> <div class="flex justify-between bg-base-100 py-2 px-4 items-center border-b pb-2 font-medium">
<span> <span>
@ -14,7 +15,7 @@
&times; &times;
</span> </span>
<% end %> <% end %>
<div class="w-screen md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)"> <div class="md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= offset %>); max-height: calc(100dvh - <%= offset %>)">
<%= yield %> <%= yield %>
</div> </div>
</div> </div>

@ -1,18 +1,34 @@
<turbo-frame id="modal"> <% if turbo_frame_request? %>
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>"> <turbo-frame id="modal">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div> <turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto"> <div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<% if local_assigns[:title] %> <div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto">
<div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5"> <% if local_assigns[:title] %>
<span> <div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5">
<%= local_assigns[:title] %> <span>
</span> <%= local_assigns[:title] %>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a> </span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
</div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px); max-height: calc(100dvh - 14px - 45px)">
<%= yield %>
</div> </div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px)">
<%= yield %>
</div> </div>
</turbo-modal>
</turbo-frame>
<% else %>
<% if local_assigns[:title] %>
<div class="sticky top-0 z-10 flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium">
<span>
<%= local_assigns[:title] %>
</span>
<history-back>
<a href="#" class="text-xl block py-2 px-5 -my-2 -mr-5">&times;</a>
</history-back>
</div> </div>
</turbo-modal> <% end %>
</turbo-frame> <div class="w-full">
<%= yield %>
</div>
<% end %>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SMS</h1> <h1 class="text-3xl md:text-4xl font-bold mb-4">SMS</h1>
<%= render 'placeholder' %> <%= render 'placeholder' %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SAML SSO</h1> <h1 class="text-3xl md:text-4xl font-bold mb-4">SAML SSO</h1>
<%= render 'placeholder' %> <%= render 'placeholder' %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>

@ -29,7 +29,7 @@
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %> <%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %> <%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %> <%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %> <%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
<div class="flex justify-between items-center mt-1"> <div class="flex justify-between items-center mt-1">
<span> <span>
<% if flash[:alert] %> <% if flash[:alert] %>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4"> <h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('storage') %> <%= t('storage') %>
</h1> </h1>
<% value = @encrypted_config.value || { 'service' => 'disk' } %> <% value = @encrypted_config.value || { 'service' => 'disk' } %>

@ -45,8 +45,8 @@
<% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %> <% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %>
<% default_body = template&.preferences&.dig('request_email_body').presence || config_body %> <% default_body = template&.preferences&.dig('request_email_body').presence || config_body %>
<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %> <% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden"> <div id="<%= message_field_id %>" class="relative flex flex-col rounded-2xl md:bg-base-300/40 hidden">
<div class="card-body"> <div class="flex flex-auto flex-col gap-2 md:text-sm md:p-4">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 11 && template&.preferences&.dig('submitters').to_a.size > 1 %> <%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 11 && template&.preferences&.dig('submitters').to_a.size > 1 %>
<% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %> <% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %>
<% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %> <% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %>

@ -1,5 +1,5 @@
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<div class="tooltip w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>"> <div class="tooltip tooltip-no-touch w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %> <%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
</div> </div>
</div> </div>

@ -25,10 +25,12 @@
<span class="hidden md:inline"><%= t('audit_log') %></span> <span class="hidden md:inline"><%= t('audit_log') %></span>
</a> </a>
<% elsif signed_in? %> <% elsif signed_in? %>
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %> <open-modal-mobile class="flex">
<%= svg_icon('logs', class: 'w-6 h-6') %> <%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
<span class="hidden md:block"><%= t('event_log') %></span> <%= svg_icon('logs', class: 'w-6 h-6') %>
<% end %> <span class="hidden md:block"><%= t('event_log') %></span>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
<% if signed_in? && !@submission.completed_at? && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %> <% if signed_in? && !@submission.completed_at? && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %>
<% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %> <% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %>
@ -180,10 +182,12 @@
</span> </span>
</div> </div>
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !@submission.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %> <% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !@submission.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>"> <span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %> <open-modal-mobile>
<%= svg_icon('pencil', class: 'w-5 h-5') %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
<% end %> <%= svg_icon('pencil', class: 'w-5 h-5') %>
<% end %>
</open-modal-mobile>
</span> </span>
<% end %> <% end %>
</div> </div>

@ -16,8 +16,8 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="flex flex-col items-end md:flex-row gap-2"> <div class="flex items-center md:items-end gap-2">
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
</div> </div>
</div> </div>

@ -18,10 +18,12 @@
<span class="hidden sm:block"> <span class="hidden sm:block">
<%= render 'templates/upload_button' %> <%= render 'templates/upload_button' %>
</span> </span>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %> <open-modal-mobile data-disabled="<%= !Docuseal.multitenant? %>" class="flex">
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<span class="hidden md:block"><%= t('create') %></span> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<% end %> <span class="hidden md:block"><%= t('create') %></span>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -34,7 +36,7 @@
<% end %> <% end %>
<% if is_show_tabs %> <% if is_show_tabs %>
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4"> <div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="hidden md:flex md:items-end gap-2 md:w-fit">
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> <a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %> <%= svg_icon('list', class: 'w-5 h-5') %>
@ -54,8 +56,8 @@
</div> </div>
</a> </a>
</div> </div>
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="flex items-center md:items-end gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
<%= render 'submissions_filters/actions', filter_params: %> <%= render 'submissions_filters/actions', filter_params: %>
</div> </div>

@ -1,72 +1,136 @@
<% query_params = params.permit(:q).merge(filter_params) %> <% query_params = params.permit(:q).merge(filter_params) %>
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %> <% filter_names = %w[status folder author completed_at created_at] %>
<div class="flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700"> <% ordered_filters = request.query_parameters.keys.filter_map { |key| filter_names.find { |name| key.start_with?(name) } }.uniq %>
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <% chip_order = ordered_filters.reverse.each_with_index.to_h { |name, index| [name, index + 1] } %>
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %> <% chip_order.default = ordered_filters.size + 1 %>
<span class="font-normal truncate"><%= t(params[:status]) %></span> <% status_icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
<% end %> <% status_icons = { 'all' => 'list', 'pending' => 'clock', 'completed' => 'circle_check' } %>
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %> <% current_status = status_icons.key?(params[:status].to_s) ? params[:status].to_s : 'all' %>
<%= svg_icon('x', class: 'w-5 h-5') %> <% with_status_button = local_assigns[:with_status] && status_icon.blank? %>
<% end %> <% chips_html = capture do %>
</div> <% if status_icon %>
<% end %> <div class="order-none flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700">
<% if params[:folder].present? %> <%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('folder') %>"> <%= svg_icon(status_icon, class: 'w-5 h-5 shrink-0') %>
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <span class="font-normal truncate"><%= t(params[:status]) %></span>
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %> <% end %>
<span class="font-normal truncate"><%= params[:folder] %></span> <%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
<% end %> <%= svg_icon('x', class: 'w-5 h-5') %>
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %> <% end %>
<%= svg_icon('x', class: 'w-5 h-5') %> </div>
<% end %> <% end %>
</div> <% if params[:folder].present? %>
<% end %> <div class="order-<%= chip_order['folder'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('folder') %>">
<% if params[:author].present? %> <%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('author') %>"> <%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <span class="font-normal truncate"><%= params[:folder] %></span>
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> <% end %>
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span> <%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
<% end %> <%= svg_icon('x', class: 'w-5 h-5') %>
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %> <% end %>
<%= svg_icon('x', class: 'w-5 h-5') %> </div>
<% end %> <% end %>
</div> <% if params[:author].present? %>
<div class="order-<%= chip_order['author'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('author') %>">
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
<% end %>
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
<% end %>
<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
<div class="order-<%= chip_order['completed_at'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('completed_at') %>">
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
<%= svg_icon('calendar_check', class: 'w-5 h-5 shrink-0') %>
<span class="flex flex-col font-normal text-left md:text-center text-xs">
<% if query_params[:completed_at_from] == query_params[:completed_at_to] %>
<span><%= l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) %></span>
<% else %>
<span><%= query_params[:completed_at_from].present? ? l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) : '∞' %></span>
<span><%= query_params[:completed_at_to].present? ? l(Date.parse(query_params[:completed_at_to]), locale: current_account.locale) : t('today') %></span>
<% end %>
</span>
<% end %>
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
<% end %>
<% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %>
<div class="order-<%= chip_order['created_at'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('created_at') %>">
<%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
<%= svg_icon('calendar', class: 'w-5 h-5 shrink-0') %>
<span class="flex flex-col font-normal text-left md:text-center text-xs">
<% if query_params[:created_at_from] == query_params[:created_at_to] %>
<span><%= l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) %></span>
<% else %>
<span><%= query_params[:created_at_from].present? ? l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) : '∞' %></span>
<span><%= query_params[:created_at_to].present? ? l(Date.parse(query_params[:created_at_to]), locale: current_account.locale) : t('today') %></span>
<% end %>
</span>
<% end %>
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
<% end %>
<% end %> <% end %>
<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %> <% if with_status_button && chips_html.blank? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('completed_at') %>"> <div class="dropdown md:hidden shrink-0">
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <label tabindex="0" class="cursor-pointer flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>">
<%= svg_icon('calendar_check', class: 'w-5 h-5 shrink-0') %> <%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
<span class="flex flex-row md:flex-col font-normal text-left md:text-center md:text-xs"> <span><%= t(current_status) %></span>
<% if query_params[:completed_at_from] == query_params[:completed_at_to] %> <%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %>
<span><%= l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) %></span> </label>
<% else %> <ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box" style="min-width: 180px;">
<span><%= query_params[:completed_at_from].present? ? l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) : '∞' %></span> <li>
<span class="px-1 md:px-0 md:hidden">-</span> <%= link_to url_for(params: request.query_parameters.except('status')), class: ('bg-base-200' if current_status == 'all') do %>
<span><%= query_params[:completed_at_to].present? ? l(Date.parse(query_params[:completed_at_to]), locale: current_account.locale) : t('today') %></span> <%= svg_icon('list', class: 'w-5 h-5 shrink-0') %>
<span><%= t('all') %></span>
<% if local_assigns[:all_count] %>
<span class="badge badge-sm badge-ghost font-medium px-2 ml-auto">
<%= local_assigns[:all_count] %>
</span>
<% end %>
<% end %> <% end %>
</span> </li>
<% end %> <li>
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %> <%= link_to url_for(params: request.query_parameters.merge('status' => 'pending')), class: ('bg-base-200' if current_status == 'pending') do %>
<%= svg_icon('x', class: 'w-5 h-5') %> <%= svg_icon('clock', class: 'w-5 h-5 shrink-0') %>
<% end %> <span><%= t('pending') %></span>
<% if local_assigns[:pending_count] %>
<span class="badge badge-sm badge-ghost font-medium px-2 ml-auto">
<%= local_assigns[:pending_count] %>
</span>
<% end %>
<% end %>
</li>
<li>
<%= link_to url_for(params: request.query_parameters.merge('status' => 'completed')), class: ('bg-base-200' if current_status == 'completed') do %>
<%= svg_icon('circle_check', class: 'w-5 h-5 shrink-0') %>
<span><%= t('completed') %></span>
<% if local_assigns[:completed_count] %>
<span class="badge badge-sm badge-ghost font-medium px-2 ml-auto">
<%= local_assigns[:completed_count] %>
</span>
<% end %>
<% end %>
</li>
</ul>
</div> </div>
<% end %> <% end %>
<% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %> <% if chips_html.present? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('created_at') %>"> <scroll-fade class="flex items-center gap-2 overflow-x-auto flex-1 min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar]:w-0 md:contents">
<%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <% if with_status_button %>
<%= svg_icon('calendar', class: 'w-5 h-5 shrink-0') %> <a href="<%= submissions_filter_path('status', query_params.merge(path: url_for)) %>" data-turbo-frame="modal" class="order-none md:hidden flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl shrink-0 <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>">
<span class="flex flex-row md:flex-col font-normal text-left md:text-center md:text-xs"> <%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
<% if query_params[:created_at_from] == query_params[:created_at_to] %> <span><%= t(current_status) %></span>
<span><%= l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) %></span> <%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %>
<% else %> </a>
<span><%= query_params[:created_at_from].present? ? l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) : '∞' %></span>
<span class="px-1 md:px-0 md:hidden">-</span>
<span><%= query_params[:created_at_to].present? ? l(Date.parse(query_params[:created_at_to]), locale: current_account.locale) : t('today') %></span>
<% end %>
</span>
<% end %> <% end %>
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %> <%= chips_html %>
<%= svg_icon('x', class: 'w-5 h-5') %> </scroll-fade>
<% end %>
</div>
<% end %> <% end %>

@ -1,10 +1,13 @@
<% query_params = params.permit(:q).merge(filter_params) %> <% query_params = params.permit(:q).merge(filter_params) %>
<div class="dropdown dropdown-end"> <% advanced_filters = filter_params.then { |f| f[:status].in?(%w[pending completed]) ? f.except(:status) : f } %>
<label tabindex="0" class="cursor-pointer flex h-10 px-3 py-1 space-x-1 text-lg items-center justify-between border text-center text-neutral rounded-xl border-neutral-300 hover:border-neutral-700"> <div class="dropdown dropdown-end shrink-0">
<label tabindex="0" class="cursor-pointer flex h-10 py-1 text-lg items-center border text-center text-neutral rounded-xl hover:border-neutral-700 <%= advanced_filters.present? ? 'w-10 md:w-auto px-0 md:px-3 space-x-0 md:space-x-1 justify-center md:justify-between border-neutral-700 md:border-neutral-300' : 'px-3 space-x-1 justify-between border-neutral-300' %>">
<%= svg_icon('filter', class: 'w-5 h-5 flex-shrink-0 stroke-2') %> <%= svg_icon('filter', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="<%= filter_params.then { |f| f[:status].in?(%w[pending completed]) ? f.except(:status) : f }.present? ? 'md:hidden' : '' %>">Filter</span> <% if advanced_filters.blank? %>
<span>Filter</span>
<% end %>
</label> </label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[180px] text-right"> <ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right" style="min-width: 180px;">
<li class="flex"> <li class="flex">
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %> <%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
<%= svg_icon('calendar_check', class: 'w-5 h-5 flex-shrink-0 stroke-2') %> <%= svg_icon('calendar_check', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>

@ -3,9 +3,9 @@
<div class="form-control w-full"> <div class="form-control w-full">
<div id="status" class="radio-select grid grid-cols-2 gap-2 px-1"> <div id="status" class="radio-select grid grid-cols-2 gap-2 px-1">
<% ['', 'pending', 'completed', 'partially_completed', 'sent', 'opened', 'declined', 'expired'].each do |status| %> <% ['', 'pending', 'completed', 'partially_completed', 'sent', 'opened', 'declined', 'expired'].each do |status| %>
<label class="radio-label cursor-pointer inline-flex items-center space-x-2"> <label class="radio-label cursor-pointer inline-flex items-center space-x-2 min-w-0">
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %> <%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio shrink-0' %>
<span><%= t(status.presence || 'all') %></span> <span class="truncate"><%= t(status.presence || 'all') %></span>
</label> </label>
<% end %> <% end %>
</div> </div>

@ -33,7 +33,7 @@
<%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %> <%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.hidden_field :submitter_slug, value: @submitter.slug %> <%= f.hidden_field :submitter_slug, value: @submitter.slug %>
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %> <%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
<div class="flex justify-between items-center mt-1"> <div class="flex justify-between items-center mt-1">
<span> <span>
<% if flash[:alert] %> <% if flash[:alert] %>

@ -37,10 +37,12 @@
<span class="hidden sm:block"> <span class="hidden sm:block">
<%= render 'templates/upload_button', folder_name: @template_folder.full_name %> <%= render 'templates/upload_button', folder_name: @template_folder.full_name %>
</span> </span>
<%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %> <open-modal-mobile data-disabled="<%= !Docuseal.multitenant? %>" class="flex">
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<span class="hidden md:block"><%= t('create') %></span> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<% end %> <span class="hidden md:block"><%= t('create') %></span>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning', 'declined' => 'badge-error' } %> <% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning', 'declined' => 'badge-error' } %>
<div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row items-strech"> <div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row">
<% if local_assigns[:with_template] %> <% if local_assigns[:with_template] %>
<% template = submission.template %> <% template = submission.template %>
<a href="<%= template ? template_path(template) : submission_path(submission) %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0"> <a href="<%= template ? template_path(template) : submission_path(submission) %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0">
@ -52,10 +52,12 @@
<%= submitter.name || submitter.email || submitter.phone %> <%= submitter.name || submitter.email || submitter.phone %>
</a> </a>
<% if !submitter.completed_at? && !submission.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if !submitter.completed_at? && !submission.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>"> <span class="pl-0.5 tooltip tooltip-top tooltip-no-touch md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %> <open-modal-mobile>
<%= svg_icon('pencil', class: 'w-5 h-5') %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<% end %> <%= svg_icon('pencil', class: 'w-5 h-5') %>
<% end %>
</open-modal-mobile>
</span> </span>
<% end %> <% end %>
</span> </span>
@ -102,12 +104,12 @@
</a> </a>
</div> </div>
<% if !submission.archived_at? && !template&.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && !template&.archived_at? && can?(:destroy, submission) %>
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('archive') %>" class="tooltip tooltip-top tooltip-no-touch">
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('archive', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %> <%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('archive', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
</span> </span>
<% end %> <% end %>
<% if local_assigns[:archived] && can?(:destroy, submission) %> <% if local_assigns[:archived] && can?(:destroy, submission) %>
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('remove') %>" class="tooltip tooltip-top tooltip-no-touch">
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('trash', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %> <%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('trash', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
</span> </span>
<% end %> <% end %>
@ -117,7 +119,7 @@
<div class="flex flex-col md:flex-row md:items-center gap-3"> <div class="flex flex-col md:flex-row md:items-center gap-3">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<a href="<%= submission_path(submission) %>" class="z-[1] tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <a href="<%= submission_path(submission) %>" class="z-[1] tooltip tooltip-no-touch flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t(latest_submitter.status) %> <%= t(latest_submitter.status) %>
</span> </span>
@ -134,7 +136,7 @@
<div class="relative flex justify-between items-start md:items-center space-x-3"> <div class="relative flex justify-between items-start md:items-center space-x-3">
<span class="flex flex-col md:flex-row md:items-center gap-2"> <span class="flex flex-col md:flex-row md:items-center gap-2">
<% if !is_submission_completed && !submission.expired? %> <% if !is_submission_completed && !submission.expired? %>
<a href="<%= submission_path(submission) %>" class="z-[1] tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <a href="<%= submission_path(submission) %>" class="z-[1] tooltip tooltip-no-touch flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold"> <span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
<%= t(submitter.status) %> <%= t(submitter.status) %>
</span> </span>
@ -145,10 +147,12 @@
<%= submitter.name || submitter.email || submitter.phone %> <%= submitter.name || submitter.email || submitter.phone %>
</a> </a>
<% if !submitter.completed_at? && !submission.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if !submitter.completed_at? && !submission.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>"> <span class="pl-0.5 tooltip tooltip-top tooltip-no-touch md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %> <open-modal-mobile>
<%= svg_icon('pencil', class: 'w-5 h-5') %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<% end %> <%= svg_icon('pencil', class: 'w-5 h-5') %>
<% end %>
</open-modal-mobile>
</span> </span>
<% end %> <% end %>
</span> </span>
@ -209,12 +213,12 @@
</a> </a>
</div> </div>
<% if !submission.archived_at? && !template&.archived_at? %> <% if !submission.archived_at? && !template&.archived_at? %>
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('archive') %>" class="tooltip tooltip-top tooltip-no-touch">
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('archive', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %> <%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('archive', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
</span> </span>
<% end %> <% end %>
<% if local_assigns[:archived] && can?(:destroy, submission) %> <% if local_assigns[:archived] && can?(:destroy, submission) %>
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('remove') %>" class="tooltip tooltip-top tooltip-no-touch">
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('trash', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %> <%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('trash', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
</span> </span>
<% end %> <% end %>

@ -15,12 +15,12 @@
<% end %> <% end %>
</p> </p>
<p class="flex text-xs text-base-content/60"> <p class="flex text-xs text-base-content/60">
<span class="flex items-center space-x-1 w-1/2"> <span class="flex items-center space-x-1 w-1/2 md:w-2/3 lg:w-1/2 whitespace-nowrap">
<%= svg_icon('calendar', class: 'w-4 h-4') %> <%= svg_icon('calendar', class: 'w-4 h-4 flex-shrink-0') %>
<span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span> <span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
</span> </span>
<% if local_assigns[:with_folder] %> <% if local_assigns[:with_folder] %>
<span class="flex items-center space-x-1 w-1/2"> <span class="flex items-center space-x-1 w-1/2 md:w-1/3 lg:w-1/2">
<%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %> <%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %>
<span class="truncate"><%= template.folder.full_name %></span> <span class="truncate"><%= template.folder.full_name %></span>
</span> </span>
@ -31,36 +31,36 @@
<div class="absolute top-0 bottom-0 w-0 items-center hidden md:group-hover:flex" style="right: 37px" data-targets="dashboard-dropzone.hiddenOnDrag"> <div class="absolute top-0 bottom-0 w-0 items-center hidden md:group-hover:flex" style="right: 37px" data-targets="dashboard-dropzone.hiddenOnDrag">
<div class="space-y-1"> <div class="space-y-1">
<% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %> <% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %>
<span class="tooltip tooltip-left" data-tip="<%= t('move') %>"> <span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= t('move') %>">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle"> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('folder_share', class: 'w-4 h-4') %> <%= svg_icon('folder_share', class: 'w-4 h-4') %>
</a> </a>
</span> </span>
<% end %> <% end %>
<% if template.archived_at? && can?(:destroy, template) %> <% if template.archived_at? && can?(:destroy, template) %>
<span class="tooltip tooltip-left" data-tip="<%= t('restore') %>"> <span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= t('restore') %>">
<%= button_to template_restore_index_path(template), class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %> <%= button_to template_restore_index_path(template), class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %>
<%= svg_icon('rotate', class: 'w-4 h-4 enabled') %> <%= svg_icon('rotate', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %> <% end %>
</span> </span>
<% elsif !template.archived_at? && can?(:update, template) %> <% elsif !template.archived_at? && can?(:update, template) %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>"> <span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= t('edit') %>">
<a href="<%= edit_template_path(template) %>" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle"> <a href="<%= edit_template_path(template) %>" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('pencil', class: 'w-4 h-4') %> <%= svg_icon('pencil', class: 'w-4 h-4') %>
</a> </a>
</span> </span>
<% end %> <% end %>
<% if can?(:create, template) %> <% if can?(:create, template) %>
<span class="tooltip tooltip-left" data-tip="<%= t('clone') %>"> <span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= t('clone') %>">
<a href="<%= new_template_clone_path(template) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle"> <a href="<%= new_template_clone_path(template) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('copy', class: 'w-4 h-4') %> <%= svg_icon('copy', class: 'w-4 h-4') %>
</a> </a>
</span> </span>
<% end %> <% end %>
<% if can?(:destroy, template) %> <% if can?(:destroy, template) %>
<span class="tooltip tooltip-left" data-tip="<%= template.archived_at? ? t('delete') : t('archive') %>"> <span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= template.archived_at? ? t('delete') : t('archive') %>">
<%= button_to template_path(template), data: template.archived_at? ? { turbo_confirm: t('template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } : {}, params: { permanently: template.archived_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: t('restore') do %> <%= button_to template_path(template), data: template.archived_at? ? { turbo_confirm: t('template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } : {}, params: { permanently: template.archived_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria: { label: template.archived_at? ? t('delete') : t('archive') } do %>
<%= svg_icon(template.archived_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %> <%= svg_icon(template.archived_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %> <% end %>

@ -20,7 +20,7 @@
</span> </span>
</a> </a>
<% if can?(:update, template) %> <% if can?(:update, template) %>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('move') %>"> <span class="pl-1 tooltip tooltip-right tooltip-no-touch md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('move') %>">
<a href="<%= edit_template_folder_path(template.id, subfolder: @template.folder.parent_folder.present?) %>" data-turbo-frame="modal"> <a href="<%= edit_template_folder_path(template.id, subfolder: @template.folder.parent_folder.present?) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %> <%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a> </a>
@ -44,7 +44,7 @@
<% if !template.archived_at? %> <% if !template.archived_at? %>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<% if can?(:update, template) %> <% if can?(:update, template) %>
<div class="tooltip" data-tip="<%= t('preferences') %>"> <div class="tooltip tooltip-no-touch hidden md:block" data-tip="<%= t('preferences') %>">
<%= link_to template_preferences_path(template), class: 'btn border border-base-200 bg-base-200 hover:bg-base-300 hover:border-base-300 btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %> <%= link_to template_preferences_path(template), class: 'btn border border-base-200 bg-base-200 hover:bg-base-300 hover:border-base-300 btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= svg_icon('adjustments_horizontal', class: 'w-6 h-6') %> <%= svg_icon('adjustments_horizontal', class: 'w-6 h-6') %>
@ -52,12 +52,14 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%= link_to template_share_link_path(template), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', data: { turbo_frame: :modal } do %> <open-modal-mobile class="absolute md:relative bottom-0 right-0 mt-1">
<span class="flex items-center justify-center space-x-2"> <%= link_to template_share_link_path(template), class: 'btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white px-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6') %> <span class="flex items-center justify-center space-x-2">
<span><%= t('link') %></span> <%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6') %>
</span> <span><%= t('link') %></span>
<% end %> </span>
<% end %>
</open-modal-mobile>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -66,40 +68,48 @@
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-4], title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete %> <%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-4], title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete %>
<% end %> <% end %>
<% if can?(:create, current_account.templates.new(author: current_user)) %> <% if can?(:create, current_account.templates.new(author: current_user)) %>
<%= link_to new_template_clone_path(template), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %> <div class="flex-1">
<span class="flex items-center justify-center space-x-2"> <%= link_to new_template_clone_path(template), class: 'btn btn-outline btn-sm w-full', data: { turbo_frame: :modal } do %>
<%= svg_icon('copy', class: 'w-6 h-6') %>
<span class="inline"><%= t('clone') %></span>
</span>
<% end %>
<% end %>
<% if !template.archived_at? %>
<% if can?(:update, template) %>
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= svg_icon('pencil', class: 'w-6 h-6') %> <%= svg_icon('copy', class: 'w-6 h-6') %>
<span><%= t('edit') %></span> <span class="inline"><%= t('clone') %></span>
</span> </span>
<% end %> <% end %>
</div>
<% end %>
<% if !template.archived_at? %>
<% if can?(:update, template) %>
<div class="flex-1">
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm w-full' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('pencil', class: 'w-6 h-6') %>
<span><%= t('edit') %></span>
</span>
<% end %>
</div>
<% elsif can?(:read, template) && !template.archived_at? %> <% elsif can?(:read, template) && !template.archived_at? %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %> <div class="flex-1">
<span class="flex items-center justify-center space-x-2"> <%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm w-full' do %>
<%= svg_icon('file_text', class: 'w-6 h-6') %> <span class="flex items-center justify-center space-x-2">
<span><%= t('preview') %></span> <%= svg_icon('file_text', class: 'w-6 h-6') %>
</span> <span><%= t('preview') %></span>
<% end %> </span>
<% end %>
</div>
<% end %> <% end %>
<% end %> <% end %>
<% if template.archived_at? %> <% if template.archived_at? %>
<% if can?(:destroy, template) %> <% if can?(:destroy, template) %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %> <%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1' %>
<% end %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %>
<span><%= t('preview') %></span>
</span>
<% end %> <% end %>
<div class="flex-1">
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm w-full' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %>
<span><%= t('preview') %></span>
</span>
<% end %>
</div>
<% end %> <% end %>
</div> </div>
</div> </div>

@ -18,10 +18,12 @@
<span><%= t('export') %></span> <span><%= t('export') %></span>
<% end %> <% end %>
<% if !@template.archived_at? && can?(:create, Submission) %> <% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), id: 'add_recipients_button', class: 'white-button !border', data: { turbo_frame: 'modal' } do %> <open-modal-mobile class="flex">
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_submission_path(@template), id: 'add_recipients_button', class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= t('add_recipients_html') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<% end %> <%= t('add_recipients_html') %>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -29,7 +31,7 @@
<% end %> <% end %>
<% if is_show_tabs %> <% if is_show_tabs %>
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4"> <div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="hidden md:flex md:items-end gap-2 md:w-fit">
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700"> <a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %> <%= svg_icon('list', class: 'w-5 h-5') %>
@ -37,7 +39,7 @@
</div> </div>
<% unless can?(:manage, :countless) %> <% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium"> <div class="badge badge-neutral badge-outline font-medium">
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.count %> <%= all_count = params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.count %>
</div> </div>
<% end %> <% end %>
</a> </a>
@ -48,7 +50,7 @@
</div> </div>
<% unless can?(:manage, :countless) %> <% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium"> <div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.count %> <%= pending_count = params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.count %>
</div> </div>
<% end %> <% end %>
</a> </a>
@ -59,13 +61,13 @@
</div> </div>
<% unless can?(:manage, :countless) %> <% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium"> <div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.count %> <%= completed_count = params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.count %>
</div> </div>
<% end %> <% end %>
</a> </a>
</div> </div>
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="flex items-center md:items-end gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/applied_filters', filter_params:, with_status: true, all_count:, pending_count:, completed_count: %>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
<% unless @template.archived_at? %> <% unless @template.archived_at? %>
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %> <%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
@ -89,10 +91,12 @@
<p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p> <p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p>
<div class="space-y-2 flex flex-col"> <div class="space-y-2 flex flex-col">
<% if can?(:create, Submission) %> <% if can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template, with_link: true), id: 'send_to_recipients_button', class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> <open-modal-mobile class="flex flex-col mt-6">
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_submission_path(@template, with_link: true), id: 'send_to_recipients_button', class: 'base-button', data: { turbo_frame: 'modal' } do %>
<span class="mr-1"><%= t('send_to_recipients') %></span> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<% end %> <span class="mr-1"><%= t('send_to_recipients') %></span>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && @template.variables_schema.blank? %> <% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && @template.variables_schema.blank? %>
<%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %> <%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
@ -100,10 +104,12 @@
<span class="mr-1"><%= t('sign_it_yourself') %></span> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %> <% end %>
<% else %> <% else %>
<%= link_to new_template_submission_path(@template, selfsign: true), id: 'sign_yourself_button', class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %> <open-modal-mobile class="flex flex-col mt-6">
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= link_to new_template_submission_path(@template, selfsign: true), id: 'sign_yourself_button', class: 'white-button', data: { turbo_frame: 'modal' } do %>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <%= svg_icon('writing', class: 'w-6 h-6') %>
<% end %> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
</div> </div>
</div> </div>

@ -18,12 +18,12 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-end items-center gap-2"> <div class="flex flex-nowrap md:flex-wrap md:justify-end items-center gap-2">
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %>
<div class="shrink-0 md:order-first"> <div class="hidden md:block shrink-0 md:order-first">
<%= link_to new_template_submissions_export_path(@template, archived: true), class: 'btn btn-ghost text-base h-10 min-h-10', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template, archived: true), class: 'btn btn-ghost text-base h-10 min-h-10', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span><%= t('export') %></span> <span class="hidden md:inline"><%= t('export') %></span>
<% end %> <% end %>
</div> </div>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>

@ -30,10 +30,12 @@
<span class="hidden sm:block"> <span class="hidden sm:block">
<%= render 'templates/upload_button' %> <%= render 'templates/upload_button' %>
</span> </span>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %> <open-modal-mobile data-disabled="<%= !Docuseal.multitenant? %>" class="flex">
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<span class="hidden md:block"><%= t('create') %></span> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<% end %> <span class="hidden md:block"><%= t('create') %></span>
<% end %>
</open-modal-mobile>
<% end %> <% end %>
</div> </div>
</div> </div>

@ -10,9 +10,7 @@
<span class="flex items-center mt-1"> <span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %> <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>
<%= svg_icon('folder_share', class: 'w-5 h-5 flex-shrink-0 hidden group-hover:inline mr-1') %> <%= svg_icon('folder_share', class: 'w-5 h-5 flex-shrink-0 hidden group-hover:inline mr-1') %>
<span class="text-md"> <span><%= @template.folder.parent_folder&.name || @template.folder.name %></span>
<%= @template.folder.parent_folder&.name || @template.folder.name %>
</span>
</span> </span>
</label> </label>
</div> </div>

@ -3,14 +3,14 @@
<% show_recipients = @template.submitters.to_a.length > 1 %> <% show_recipients = @template.submitters.to_a.length > 1 %>
<% options = [[t('general'), 'general']] %> <% options = [[t('general'), 'general']] %>
<% options << [t('recipients'), 'recipients'] if show_recipients %> <% options << [t('recipients'), 'recipients'] if show_recipients %>
<% options << [t('api_and_embedding'), 'api'] if show_api %> <% options << [safe_join([tag.span('API', class: 'md:hidden'), tag.span(t('api_and_embedding'), class: 'hidden md:inline')]), 'api'] if show_api %>
<% if options.size > 1 %> <% if options.size > 1 %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 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 == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == 'general', 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 peer-checked:btn-active normal-case <%= 'px-8 md:px-0' if value.in?(%w[general recipients]) %> <%= options.size > 2 ? 'md:w-44' : 'md:w-48' %>"> <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-sm join-item peer-checked:btn-active normal-case px-8 md:px-0 <%= 'min-w-28 md:min-w-0' if value == 'api' %> <%= options.size > 2 ? 'md:w-44' : 'md:w-48' %>">
<%= label %> <%= label %>
</label> </label>
</span> </span>

@ -26,7 +26,7 @@
<div class="relative flex-grow"> <div class="relative flex-grow">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly> <input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly>
<% if @template.variables_schema.blank? && !@template.preferences&.dig('require_email_2fa') && !@template.preferences&.dig('require_phone_2fa') %> <% if @template.variables_schema.blank? && !@template.preferences&.dig('require_email_2fa') && !@template.preferences&.dig('require_phone_2fa') %>
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>"> <a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip tooltip-no-touch text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
<%= svg_icon('qrcode', class: 'w-6 h-6') %> <%= svg_icon('qrcode', class: 'w-6 h-6') %>
</a> </a>
<% end %> <% end %>

@ -5,7 +5,7 @@
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<span> <span>
<%= radio_button_tag 'option', value, value == 'draw', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == 'draw', 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 md:w-28 peer-checked:btn-active normal-case"> <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-sm join-item md:w-28 peer-checked:btn-active normal-case">
<%= label %> <%= label %>
</label> </label>
</span> </span>

@ -5,7 +5,7 @@
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<span> <span>
<%= radio_button_tag 'option', value, value == 'draw', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == 'draw', 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 md:w-28 peer-checked:btn-active normal-case"> <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-sm join-item md:w-28 peer-checked:btn-active normal-case">
<%= label %> <%= label %>
</label> </label>
</span> </span>

@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="md:flex-grow"> <div class="md:flex-grow">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12"> <div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold"> <h1 class="text-3xl md:text-4xl font-bold">
<% if params[:status] == 'archived' %> <% if params[:status] == 'archived' %>
<%= t('archived_users') %> <%= t('archived_users') %>
<% elsif params[:status] == 'integration' %> <% elsif params[:status] == 'integration' %>
@ -56,7 +56,7 @@
</thead> </thead>
<tbody> <tbody>
<% @users.each do |user| %> <% @users.each do |user| %>
<tr scope="row"> <tr>
<td> <td>
<%= user.full_name %> <%= user.full_name %>
</td> </td>

@ -6,7 +6,7 @@
<% next_attempt_at = last_attempt.created_at + (2**last_attempt.attempt).minutes %> <% next_attempt_at = last_attempt.created_at + (2**last_attempt.attempt).minutes %>
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 && next_attempt_at > 30.seconds.ago %> <% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 && next_attempt_at > 30.seconds.ago %>
<li class="ml-7"> <li class="ml-7">
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;"> <span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center text-base-content/60 bg-base-100" style="left: -12px;">
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %> <%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
</span> </span>
<p class="leading-none text-base-content/90 pt-1"> <p class="leading-none text-base-content/90 pt-1">

@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow min-w-0"> <div class="flex-grow min-w-0">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1> <h1 class="text-3xl md:text-4xl font-bold">Webhooks</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<%= render 'shared/test_mode_toggle' %> <%= render 'shared/test_mode_toggle' %>
<% if @webhook_url.persisted? %> <% if @webhook_url.persisted? %>

@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">Webhook</h1> <h1 class="text-3xl md:text-4xl font-bold">Webhook</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %> <% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
<%= render 'shared/test_mode_toggle' %> <%= render 'shared/test_mode_toggle' %>
@ -25,7 +25,7 @@
<%= svg_icon('lock', class: 'w-4 h-4') %> <%= svg_icon('lock', class: 'w-4 h-4') %>
<span><%= t('security') %></span> <span><%= t('security') %></span>
<% end %> <% end %>
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>"> <div class="tooltip tooltip-left md:tooltip-top tooltip-no-touch" data-tip="<%= t('delete_webhook') %>">
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %> <%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
<span><%= t('delete') %></span> <span><%= t('delete') %></span>
<% end %> <% end %>
@ -83,7 +83,7 @@
</div> </div>
<% if @webhook_events.present? || params[:status].present? %> <% if @webhook_events.present? || params[:status].present? %>
<div class="mt-6"> <div class="mt-6">
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2> <h2 id="log" class="text-2xl md:text-3xl font-bold"><%= t('events_log') %></h2>
<div class="tabs border-b mt-4"> <div class="tabs border-b mt-4">
<%= link_to t('all'), url_for(params: request.query_parameters.except('status', 'page')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> <%= link_to t('all'), url_for(params: request.query_parameters.except('status', 'page')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('succeeded'), url_for(params: request.query_parameters.except('page').merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> <%= link_to t('succeeded'), url_for(params: request.query_parameters.except('page').merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>

@ -53,6 +53,25 @@ en: &en
require_all_recipients: Require all recipients require_all_recipients: Require all recipients
stripe_account_has_been_connected: Stripe account has been connected. stripe_account_has_been_connected: Stripe account has been connected.
re_connect_stripe: Re-connect Stripe re_connect_stripe: Re-connect Stripe
paypal_integration: PayPal Integration
paypal_account_has_been_connected: PayPal account has been connected.
re_connect_paypal: Re-connect PayPal
disconnect: Disconnect
failed_to_connect_paypal: Failed to connect PayPal. Please try again.
paypal_connect_not_completed: PayPal connection was not completed.
payment_receipt: Payment Receipt
amount_paid: Amount paid
transaction: Transaction
transaction_id: Transaction ID
order_id: Order ID
payment_date: Date
paid_by: Paid by
paid_to: Paid to
business: Business
country: Country
payer_id: Payer ID
merchant_id: Merchant ID
breakdown: Breakdown
bcc_recipients: BCC recipients bcc_recipients: BCC recipients
resend_pending: Re-send pending resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order always_enforce_signing_order: Always enforce the signing order
@ -454,7 +473,7 @@ en: &en
back_to_active: Back to Active back_to_active: Back to Active
submissions: Submissions submissions: Submissions
templates: Templates templates: Templates
document_templates_html: <span class="hidden md:inline">Document</span> Templates document_templates_html: <span class="hidden lg:inline">Document</span> Templates
archived: Archived archived: Archived
search: Search search: Search
submissions_not_found: Submissions not Found submissions_not_found: Submissions not Found
@ -946,6 +965,7 @@ en: &en
undo: Undo undo: Undo
redo: Redo redo: Redo
add_variable: Add variable add_variable: Add variable
variable: Variable
enter_a_url_or_variable_name: Enter a URL or variable name enter_a_url_or_variable_name: Enter a URL or variable name
new_token: New token new_token: New token
token: Token token: Token
@ -1124,6 +1144,24 @@ es: &es
stripe_integration: Integración con Stripe stripe_integration: Integración con Stripe
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada. stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
re_connect_stripe: Volver a conectar Stripe re_connect_stripe: Volver a conectar Stripe
paypal_integration: Integración con PayPal
paypal_account_has_been_connected: La cuenta de PayPal ha sido conectada.
re_connect_paypal: Volver a conectar PayPal
failed_to_connect_paypal: No se pudo conectar PayPal. Por favor, inténtalo de nuevo.
paypal_connect_not_completed: La conexión con PayPal no se completó.
payment_receipt: Recibo de pago
amount_paid: Importe pagado
transaction: Transacción
transaction_id: ID de transacción
order_id: ID de pedido
payment_date: Fecha
paid_by: Pagado por
paid_to: Pagado a
business: Empresa
country: País
payer_id: ID de pagador
merchant_id: ID de comerciante
breakdown: Desglose
private: Privado private: Privado
_variables: Variables _variables: Variables
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Su dirección de correo electrónico ha sido liberada inmediatamente. Si se une a un equipo, ahora puede ser invitado al equipo de su empresa sin esperar a que finalice el período de eliminación. your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Su dirección de correo electrónico ha sido liberada inmediatamente. Si se une a un equipo, ahora puede ser invitado al equipo de su empresa sin esperar a que finalice el período de eliminación.
@ -1535,7 +1573,7 @@ es: &es
back_to_active: Volver a activo back_to_active: Volver a activo
submissions: Envíos submissions: Envíos
templates: Plantillas templates: Plantillas
document_templates_html: Plantillas <span class="hidden md:inline">de documento</span> document_templates_html: Plantillas <span class="hidden lg:inline">de documento</span>
archived: Archivado archived: Archivado
search: Buscar search: Buscar
submissions_not_found: Envíos no encontrados submissions_not_found: Envíos no encontrados
@ -2024,6 +2062,7 @@ es: &es
undo: Deshacer undo: Deshacer
redo: Rehacer redo: Rehacer
add_variable: Agregar variable add_variable: Agregar variable
variable: Variable
enter_a_url_or_variable_name: Ingrese una URL o nombre de variable enter_a_url_or_variable_name: Ingrese una URL o nombre de variable
new_token: Nuevo token new_token: Nuevo token
token: Token token: Token
@ -2202,6 +2241,24 @@ it: &it
stripe_integration: Integrazione Stripe stripe_integration: Integrazione Stripe
stripe_account_has_been_connected: L'account Stripe è stato collegato. stripe_account_has_been_connected: L'account Stripe è stato collegato.
re_connect_stripe: Ricollega Stripe re_connect_stripe: Ricollega Stripe
paypal_integration: Integrazione PayPal
paypal_account_has_been_connected: L'account PayPal è stato collegato.
re_connect_paypal: Ricollega PayPal
failed_to_connect_paypal: Impossibile collegare PayPal. Riprova.
paypal_connect_not_completed: La connessione con PayPal non è stata completata.
payment_receipt: Ricevuta di pagamento
amount_paid: Importo pagato
transaction: Transazione
transaction_id: ID transazione
order_id: ID ordine
payment_date: Data
paid_by: Pagato da
paid_to: Pagato a
business: Azienda
country: Paese
payer_id: ID pagatore
merchant_id: ID esercente
breakdown: Dettaglio
private: Privato private: Privato
_variables: Variabili _variables: Variabili
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Il tuo indirizzo email è stato rilasciato immediatamente. Se stai per unirti a un team, ora puoi essere invitato nel team della tua azienda senza aspettare la fine del periodo di eliminazione. your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Il tuo indirizzo email è stato rilasciato immediatamente. Se stai per unirti a un team, ora puoi essere invitato nel team della tua azienda senza aspettare la fine del periodo di eliminazione.
@ -2613,7 +2670,7 @@ it: &it
back_to_active: Torna attivo back_to_active: Torna attivo
submissions: Invii submissions: Invii
templates: Modelli templates: Modelli
document_templates_html: Modelli <span class="hidden md:inline">di documento</span> document_templates_html: Modelli <span class="hidden lg:inline">di documento</span>
archived: Archiviato archived: Archiviato
search: Cerca search: Cerca
submissions_not_found: Invii non trovati submissions_not_found: Invii non trovati
@ -3102,6 +3159,7 @@ it: &it
undo: Annulla undo: Annulla
redo: Ripeti redo: Ripeti
add_variable: Aggiungi variabile add_variable: Aggiungi variabile
variable: Variabile
enter_a_url_or_variable_name: Inserisci un URL o nome variabile enter_a_url_or_variable_name: Inserisci un URL o nome variabile
new_token: Nuovo token new_token: Nuovo token
token: Token token: Token
@ -3280,6 +3338,24 @@ fr: &fr
stripe_integration: Intégration Stripe stripe_integration: Intégration Stripe
stripe_account_has_been_connected: Le compte Stripe a été connecté. stripe_account_has_been_connected: Le compte Stripe a été connecté.
re_connect_stripe: Reconnecter Stripe re_connect_stripe: Reconnecter Stripe
paypal_integration: Intégration PayPal
paypal_account_has_been_connected: Le compte PayPal a été connecté.
re_connect_paypal: Reconnecter PayPal
failed_to_connect_paypal: Échec de la connexion à PayPal. Veuillez réessayer.
paypal_connect_not_completed: La connexion à PayPal n'a pas été complétée.
payment_receipt: Reçu de paiement
amount_paid: Montant payé
transaction: Transaction
transaction_id: ID de transaction
order_id: ID de commande
payment_date: Date
paid_by: Payé par
paid_to: Payé à
business: Entreprise
country: Pays
payer_id: ID payeur
merchant_id: ID marchand
breakdown: Détail
private: Privé private: Privé
_variables: Variables _variables: Variables
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Votre adresse e-mail a été libérée immédiatement. Si vous rejoignez une équipe, vous pouvez maintenant être invité dans l'équipe de votre entreprise sans attendre la fin de la période de suppression. your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Votre adresse e-mail a été libérée immédiatement. Si vous rejoignez une équipe, vous pouvez maintenant être invité dans l'équipe de votre entreprise sans attendre la fin de la période de suppression.
@ -3691,7 +3767,7 @@ fr: &fr
back_to_active: Retour aux actifs back_to_active: Retour aux actifs
submissions: Soumissions submissions: Soumissions
templates: Modèles templates: Modèles
document_templates_html: Modèles <span class="hidden md:inline">de documents</span> document_templates_html: Modèles <span class="hidden lg:inline">de documents</span>
archived: Archivé archived: Archivé
search: Rechercher search: Rechercher
submissions_not_found: Soumissions introuvables submissions_not_found: Soumissions introuvables
@ -4177,6 +4253,7 @@ fr: &fr
undo: Annuler undo: Annuler
redo: Rétablir redo: Rétablir
add_variable: Ajouter une variable add_variable: Ajouter une variable
variable: Variable
enter_a_url_or_variable_name: Entrez une URL ou un nom de variable enter_a_url_or_variable_name: Entrez une URL ou un nom de variable
new_token: Nouveau jeton new_token: Nouveau jeton
token: Jeton token: Jeton
@ -4355,6 +4432,24 @@ pt: &pt
stripe_integration: Integração com Stripe stripe_integration: Integração com Stripe
stripe_account_has_been_connected: Conta Stripe foi conectada. stripe_account_has_been_connected: Conta Stripe foi conectada.
re_connect_stripe: Reconectar Stripe re_connect_stripe: Reconectar Stripe
paypal_integration: Integração com PayPal
paypal_account_has_been_connected: Conta PayPal foi conectada.
re_connect_paypal: Reconectar PayPal
failed_to_connect_paypal: Falha ao conectar PayPal. Tente novamente.
paypal_connect_not_completed: A conexão com PayPal não foi concluída.
payment_receipt: Recibo de pagamento
amount_paid: Valor pago
transaction: Transação
transaction_id: ID da transação
order_id: ID do pedido
payment_date: Data
paid_by: Pago por
paid_to: Pago a
business: Empresa
country: País
payer_id: ID do pagador
merchant_id: ID do comerciante
breakdown: Detalhamento
private: Privado private: Privado
_variables: Variáveis _variables: Variáveis
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Seu endereço de e-mail foi liberado imediatamente. Se você estiver entrando em uma equipe, agora pode ser convidado para a equipe da sua empresa sem esperar o fim do período de exclusão. your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Seu endereço de e-mail foi liberado imediatamente. Se você estiver entrando em uma equipe, agora pode ser convidado para a equipe da sua empresa sem esperar o fim do período de exclusão.
@ -4766,7 +4861,7 @@ pt: &pt
back_to_active: Voltar para ativo back_to_active: Voltar para ativo
submissions: Submissões submissions: Submissões
templates: Modelos templates: Modelos
document_templates_html: Modelos <span class="hidden md:inline">de documento</span> document_templates_html: Modelos <span class="hidden lg:inline">de documento</span>
archived: Arquivado archived: Arquivado
search: Buscar search: Buscar
submissions_not_found: Submissões não encontradas submissions_not_found: Submissões não encontradas
@ -5255,6 +5350,7 @@ pt: &pt
undo: Desfazer undo: Desfazer
redo: Refazer redo: Refazer
add_variable: Adicionar variável add_variable: Adicionar variável
variable: Variável
enter_a_url_or_variable_name: Digite uma URL ou nome de variável enter_a_url_or_variable_name: Digite uma URL ou nome de variável
new_token: Novo token new_token: Novo token
token: Token token: Token
@ -5443,6 +5539,24 @@ de: &de
require_all_recipients: Alle Empfänger verpflichten require_all_recipients: Alle Empfänger verpflichten
stripe_account_has_been_connected: Stripe-Konto wurde verbunden. stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
re_connect_stripe: Stripe erneut verbinden re_connect_stripe: Stripe erneut verbinden
paypal_integration: PayPal-Integration
paypal_account_has_been_connected: PayPal-Konto wurde verbunden.
re_connect_paypal: PayPal erneut verbinden
failed_to_connect_paypal: PayPal konnte nicht verbunden werden. Bitte erneut versuchen.
paypal_connect_not_completed: Die PayPal-Verbindung wurde nicht abgeschlossen.
payment_receipt: Zahlungsbeleg
amount_paid: Gezahlter Betrag
transaction: Transaktion
transaction_id: Transaktions-ID
order_id: Bestell-ID
payment_date: Datum
paid_by: Gezahlt von
paid_to: Gezahlt an
business: Unternehmen
country: Land
payer_id: Zahler-ID
merchant_id: Händler-ID
breakdown: Aufschlüsselung
bcc_recipients: BCC-Empfänger bcc_recipients: BCC-Empfänger
resend_pending: Ausstehende erneut versenden resend_pending: Ausstehende erneut versenden
always_enforce_signing_order: Reihenfolge der Unterschriften immer erzwingen always_enforce_signing_order: Reihenfolge der Unterschriften immer erzwingen
@ -5844,7 +5958,7 @@ de: &de
back_to_active: Zurück zu Aktiv back_to_active: Zurück zu Aktiv
submissions: Einreichungen submissions: Einreichungen
templates: Vorlagen templates: Vorlagen
document_templates_html: <span class="md:hidden">Vorlagen</span><span class="hidden md:inline">Dokumentvorlagen</span> document_templates_html: <span class="lg:hidden">Vorlagen</span><span class="hidden lg:inline">Dokumentvorlagen</span>
archived: Archiviert archived: Archiviert
search: Suchen search: Suchen
submissions_not_found: Einreichungen nicht gefunden submissions_not_found: Einreichungen nicht gefunden
@ -6333,6 +6447,7 @@ de: &de
undo: Rückgängig undo: Rückgängig
redo: Wiederholen redo: Wiederholen
add_variable: Variable hinzufügen add_variable: Variable hinzufügen
variable: Variable
enter_a_url_or_variable_name: Geben Sie eine URL oder einen Variablennamen ein enter_a_url_or_variable_name: Geben Sie eine URL oder einen Variablennamen ein
new_token: Neues Token new_token: Neues Token
token: Token token: Token
@ -6925,6 +7040,24 @@ nl: &nl
require_all_recipients: Alle ontvangers verplichten require_all_recipients: Alle ontvangers verplichten
stripe_account_has_been_connected: Stripe-account is verbonden. stripe_account_has_been_connected: Stripe-account is verbonden.
re_connect_stripe: Stripe opnieuw verbinden re_connect_stripe: Stripe opnieuw verbinden
paypal_integration: PayPal-integratie
paypal_account_has_been_connected: PayPal-account is verbonden.
re_connect_paypal: PayPal opnieuw verbinden
failed_to_connect_paypal: PayPal kon niet worden verbonden. Probeer het opnieuw.
paypal_connect_not_completed: De verbinding met PayPal is niet voltooid.
payment_receipt: Betalingsbewijs
amount_paid: Betaald bedrag
transaction: Transactie
transaction_id: Transactie-ID
order_id: Order-ID
payment_date: Datum
paid_by: Betaald door
paid_to: Betaald aan
business: Bedrijf
country: Land
payer_id: Betaler-ID
merchant_id: Verkoper-ID
breakdown: Specificatie
bcc_recipients: BCC-ontvangers bcc_recipients: BCC-ontvangers
resend_pending: Opnieuw verzenden in behandeling resend_pending: Opnieuw verzenden in behandeling
always_enforce_signing_order: Ondertekenvolgorde altijd afdwingen always_enforce_signing_order: Ondertekenvolgorde altijd afdwingen
@ -7327,7 +7460,7 @@ nl: &nl
back_to_active: Terug naar Actief back_to_active: Terug naar Actief
submissions: Inzendingen submissions: Inzendingen
templates: Sjablonen templates: Sjablonen
document_templates_html: <span class="md:hidden">Sjablonen</span><span class="hidden md:inline">Documentsjablonen</span> document_templates_html: <span class="lg:hidden">Sjablonen</span><span class="hidden lg:inline">Documentsjablonen</span>
archived: Gearchiveerd archived: Gearchiveerd
search: Zoeken search: Zoeken
submissions_not_found: Geen inzendingen gevonden submissions_not_found: Geen inzendingen gevonden
@ -7812,6 +7945,7 @@ nl: &nl
undo: Ongedaan maken undo: Ongedaan maken
redo: Opnieuw redo: Opnieuw
add_variable: Variabele toevoegen add_variable: Variabele toevoegen
variable: Variabele
enter_a_url_or_variable_name: Voer een URL of variabelenaam in enter_a_url_or_variable_name: Voer een URL of variabelenaam in
new_token: Nieuw token new_token: Nieuw token
token: Token token: Token

@ -178,6 +178,8 @@ Rails.application.routes.draw do
resources :send_email, only: %i[create], controller: 'submitters_send_email' resources :send_email, only: %i[create], controller: 'submitters_send_email'
end end
resources :settings, only: %i[index]
scope '/settings', as: :settings do scope '/settings', as: :settings do
unless Docuseal.multitenant? unless Docuseal.multitenant?
resources :storage, only: %i[index create], controller: 'storage_settings' resources :storage, only: %i[index create], controller: 'storage_settings'

@ -130,13 +130,13 @@ module Submissions
submission submission
end end
def maybe_set_dynamic_documents(submission) def maybe_set_dynamic_documents(submission, preview: false)
return submission unless submission.template_id? return submission unless submission.template_id?
template = submission.template template = submission.template
return submission if template.variables_schema.present? || return submission if !preview && (template.variables_schema.present? ||
submission.variables_schema.present? submission.variables_schema.present?)
return submission if template.schema.none? { |e| e['dynamic'] } return submission if template.schema.none? { |e| e['dynamic'] }

@ -49,7 +49,7 @@ module Submitters
end end
def fulltext_search_field(current_user, submitters, keyword, field_name) def fulltext_search_field(current_user, submitters, keyword, field_name)
keyword = keyword.delete("\0") keyword = keyword.delete("\0\\")
return submitters.none if keyword.blank? return submitters.none if keyword.blank?

@ -220,9 +220,16 @@ module Submitters
submitter.values submitter.values
end end
formula = normalize_formula(formula, submitter.submission, submission_values:) values = submission_values.merge(acc.compact_blank)
acc[field['uuid']] = calculate_formula_value(formula, submission_values.merge(acc.compact_blank)) acc[field['uuid']] =
if field['type'] == 'text'
eval_text_formula_value(formula, values, submitter.submission)
else
normalized_formula = normalize_formula(formula, submitter.submission, submission_values:)
calculate_formula_value(normalized_formula, values)
end
end end
computed_values.compact_blank computed_values.compact_blank
@ -250,6 +257,10 @@ module Submitters
0 0
end end
def eval_text_formula_value(_formula, _values, _submission)
''
end
def replace_current_date_placeholders(submitter) def replace_current_date_placeholders(submitter)
submitter.values.each_with_object({}) do |(uuid, v), acc| submitter.values.each_with_object({}) do |(uuid, v), acc|
acc[uuid] = acc[uuid] =

Loading…
Cancel
Save