Compare commits

...
48 Commits
Author SHA1 Message Date
Alex Turchyn 13a1835c00 Merge from docusealco/wip 2026-10-26 12:27:46 +02:00
Pete Matsyburka c35407d7a9 cleanup routes 2026-10-25 21:43:38 +02:00
Pete Matsyburka e55ccc0b76 update dep 2026-10-25 16:09:58 +02:00
Pete Matsyburka b8d00870b4 optimize query 2026-10-25 15:02:22 +02:00
Pete Matsyburka 5413fbc248 add template_id to submitters 2026-10-25 09:39:47 +02:00
Pete Matsyburka 2d2eb7ce4d ruby 4.0.9 2026-10-23 23:42:14 +03:00
Pete Matsyburka ed2ddbf74e remove legacy route 2026-10-23 11:10:14 +03:00
Pete Matsyburka b99e5d1262 adjust autosave 2026-10-23 10:02:32 +03:00
Pete Matsyburka 87f6878eda fix brakeman 2026-10-23 08:29:39 +03:00
Pete Matsyburka 99b815eb4b fix generation 2026-10-23 08:25:07 +03:00
Pete Matsyburka e40a29b9ab update certs 2026-10-22 18:47:09 +03:00
Pete Matsyburka 151890bb72 add autosave config 2026-10-22 17:17:47 +03:00
Pete Matsyburka 78edd87cc1 add main tag 2026-10-22 15:01:54 +03:00
Pete Matsyburka 88c99a2b35 fix turbo morph 2026-10-22 14:48:03 +03:00
Pete Matsyburka 282b8cfa73 fix search 2026-10-22 11:02:24 +03:00
Pete Matsyburka e9888c7466 add validation 2026-10-21 15:41:53 +03:00
Pete Matsyburka 0647befb3a escape email variables 2026-10-21 09:54:29 +03:00
Pete Matsyburka 85aa3d0451 mask email field value 2026-10-21 09:35:38 +03:00
Pete Matsyburka 6d2b3bb654 adjust test webhook 2026-10-20 20:15:37 +03:00
Pete Matsyburka b394854e9d adjust send form 2026-10-20 09:50:21 +03:00
Pete Matsyburka 2e586e930e adjust signature validation 2026-10-19 19:03:26 +03:00
Alex Turchyn 8f8f69f0bc Merge from docusealco/wip 2026-10-19 14:57:12 +03:00
Pete Matsyburka 04663d55eb update deps 2026-10-19 11:10:48 +03:00
Pete Matsyburka 93d0b935e2 fix dynamic document 2026-10-17 13:17:52 +03:00
Pete Matsyburka 829b2b2855 disable jemalloc 2026-10-17 10:42:20 +03:00
Alex Turchyn cf6de444f4 disable the review form for native apps 2026-10-16 23:22:32 +03:00
Pete Matsyburka 126f5d58a2 convert docx 2026-10-16 20:18:10 +03:00
Pete Matsyburka 09b93067f4 fix close modal 2026-10-16 19:56:36 +03:00
Pete Matsyburka f14458f238 fix generate 2026-10-16 19:56:36 +03:00
Pete Matsyburka 445139f05c adjust mobile nav 2026-10-16 19:56:36 +03:00
Pete Matsyburka e5e4965f05 adjust kba 2026-10-16 19:56:36 +03:00
Pete Matsyburka 1c8f7b0316 use jemalloc 2026-10-15 15:28:54 +03:00
Alex Turchyn 9fe3656652 fix icons shrinking 2026-10-14 08:11:45 +03:00
Pete Matsyburka 17ae46bd61 make remember signature permanent 2026-10-13 16:45:42 +03:00
Pete Matsyburka a036483938 tenants test flag 2026-10-13 09:23:38 +03:00
Alex Turchyn 3c65b0e4ba Merge from docusealco/wip 2026-10-12 15:29:17 +03:00
Pete Matsyburka ced5ab67ab adjust generate previews 2026-10-12 09:33:42 +03:00
Pete Matsyburka 28c4177128 adjsut detailed send button 2026-10-11 17:52:54 +03:00
Pete Matsyburka 5d86a08b5f update send modal 2026-10-10 15:57:08 +03:00
Pete Matsyburka a69fb81429 fix multiselect 2026-10-09 12:57:12 +03:00
Pete Matsyburka e6ac3380d4 dynamic editor image drag 2026-10-09 12:25:21 +03:00
Pete Matsyburka 3a535e7112 adjust date timezone 2026-10-08 16:06:41 +03:00
Pete Matsyburka 7464f9cfad download event log 2026-10-08 10:04:12 +03:00
Pete Matsyburka 58e24ef758 update submitter slug 2026-10-08 10:04:01 +03:00
Pete Matsyburka c1d809f989 update demo 2026-10-07 09:00:58 +03:00
Pete Matsyburka 80b24f0e2a show signature when completed 2026-10-06 12:12:17 +03:00
Pete Matsyburka 2e253904a3 invalid email error 2026-10-06 11:01:04 +03:00
Pete Matsyburka a7f381caf0 update gem 2026-10-05 12:31:08 +03:00
89 changed files with 776 additions and 318 deletions

No files matched your search

+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.9
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.9
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.9
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
container: container:
image: ruby:4.0.5-alpine image: ruby:4.0.9-alpine
services: services:
postgres: postgres:
+8 -5
View File
@@ -1,4 +1,4 @@
FROM ruby:4.0.5-alpine AS download FROM ruby:4.0.9-alpine AS download
WORKDIR /fonts WORKDIR /fonts
@@ -17,7 +17,7 @@ RUN apk --no-cache add wget unzip && \
mkdir -p /pdfium-linux && \ mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux unzip -q pdfium-linux.zip -d /pdfium-linux
FROM ruby:4.0.5-alpine AS webpack FROM ruby:4.0.9-alpine AS webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -44,7 +44,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:4.0.5-alpine AS app FROM ruby:4.0.9-alpine AS app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
@@ -52,7 +52,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \ RUN apk upgrade --no-cache ca-certificates-bundle && \
apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -91,7 +92,7 @@ COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \ RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \ mkdir -p /usr/share/fonts/noto/bold && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/bold/ && fc-cache -f && \
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \ bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
chown -R docuseal:docuseal /app/tmp/cache chown -R docuseal:docuseal /app/tmp/cache
@@ -100,6 +101,8 @@ ENV HOME=/home/docuseal
ENV WORKDIR=/data/docuseal ENV WORKDIR=/data/docuseal
ENV VIPS_MAX_COORD=17000 ENV VIPS_MAX_COORD=17000
ENV VIPS_BLOCK_UNTRUSTED=1 ENV VIPS_BLOCK_UNTRUSTED=1
# ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
EXPOSE 3000 EXPOSE 3000
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"] CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+2 -1
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '4.0.5' ruby '4.0.9'
gem 'addressable' gem 'addressable'
gem 'arabic-letter-connector', require: false gem 'arabic-letter-connector', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'wordcraft', require: false, group: :ce
gem 'xlsxtream', require: false gem 'xlsxtream', require: false
group :development, :test do group :development, :test do
+17 -6
View File
@@ -524,11 +524,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.5-aarch64-linux-gnu) sqlite3 (2.9.6-aarch64-linux-gnu)
sqlite3 (2.9.5-aarch64-linux-musl) sqlite3 (2.9.6-aarch64-linux-musl)
sqlite3 (2.9.5-arm64-darwin) sqlite3 (2.9.6-arm64-darwin)
sqlite3 (2.9.5-x86_64-linux-gnu) sqlite3 (2.9.6-x86_64-linux-gnu)
sqlite3 (2.9.5-x86_64-linux-musl) sqlite3 (2.9.6-x86_64-linux-musl)
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)
@@ -572,6 +572,16 @@ GEM
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wordcraft (0.11.0-aarch64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.11.0-aarch64-linux-musl)
ffi (>= 1.15)
wordcraft (0.11.0-arm64-darwin)
ffi (>= 1.15)
wordcraft (0.11.0-x86_64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.11.0-x86_64-linux-musl)
ffi (>= 1.15)
xlsxtream (3.1.0) xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7) zip_kit (>= 6.2, < 7)
xpath (3.2.0) xpath (3.2.0)
@@ -650,10 +660,11 @@ DEPENDENCIES
tzinfo-data tzinfo-data
web-console web-console
webmock webmock
wordcraft
xlsxtream xlsxtream
RUBY VERSION RUBY VERSION
ruby 4.0.5 ruby 4.0.9
BUNDLED WITH BUNDLED WITH
4.0.3 4.0.3
@@ -1,74 +0,0 @@
# frozen_string_literal: true
module Api
class AttachmentsController < ActionController::API
include ActionController::Cookies
include ActiveStorage::SetCurrent
COOKIE_STORE_LIMIT = 10
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
file = params[:file]
if params[:type].in?(%w[initials signature])
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end
if ImageUtils.error?(image)
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
rescue Submitters::MaliciousFileExtension => e
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
values[submitter.email] = attachment.uuid
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
values.to_json
end
end
end
@@ -1,15 +0,0 @@
# frozen_string_literal: true
module Api
class SubmitterEmailClicksController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
end
render json: {}
end
end
end
+1 -4
View File
@@ -179,10 +179,7 @@ module Api
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present? submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present? submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = submitters.where(template_id: params[:template_id]) if params[:template_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filter_by_completed_at(submitters, params) maybe_filter_by_completed_at(submitters, params)
end end
+6 -1
View File
@@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base
end end
def sign_in_for_demo def sign_in_for_demo
sign_in(User.active.order('random()').take) unless signed_in? return if signed_in?
account = Account.create!(name: "Demo #{SecureRandom.hex(3)}")
sign_in(account.users.create!(first_name: 'Demo', last_name: 'User',
email: "#{SecureRandom.hex}@docuseal.com", password: SecureRandom.hex))
end end
def current_account def current_account
@@ -66,6 +66,8 @@ class SubmissionsController < ApplicationController
create_submissions(@template, submissions_params, params) create_submissions(@template, submissions_params, params)
end end
raise Submissions::CreateFromSubmitters::BaseError, 'Invalid email address' if submissions.blank?
WebhookUrls.enqueue_events(submissions, 'submission.created') WebhookUrls.enqueue_events(submissions, 'submission.created')
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
@@ -34,7 +34,7 @@ class SubmitFormUploadController < ActionController::API
attachment = Submitters.create_attachment!(@submitter, file, metadata:) attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present? if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment) cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
+17
View File
@@ -81,9 +81,26 @@ class SubmittersController < ApplicationController
submitter.name = attrs[:name] if attrs.key?(:name) submitter.name = attrs[:name] if attrs.key?(:name)
if submitter_changed?(submitter)
submitter.submitter_versions.new(slug: submitter.slug, email: submitter.email_was,
name: submitter.name_was, phone: submitter.phone_was)
submitter.slug = SecureRandom.base58(14)
submitter.values = Submitters.fetch_values_for_delegate(submitter)
end
submitter submitter
end end
def submitter_changed?(submitter)
if submitter.email_was.blank? && submitter.phone_was.blank?
submitter.name_was.present? && submitter.name_changed?
else
(submitter.email_was.present? && submitter.email_changed?) ||
(submitter.phone_was.present? && submitter.phone_changed?)
end
end
def submitter_params def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip) params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end end
@@ -8,7 +8,9 @@ class TemplateSharingsTestingController < ApplicationController
end end
def create def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account account = can?(:manage, :tenants_test) ? current_account : true_user.account
testing_account = Accounts.find_or_create_testing_user(account).account
if params[:value] == '1' if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template) TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
+7
View File
@@ -35,6 +35,9 @@ import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleSubmit from './elements/toggle_submit' import ToggleSubmit from './elements/toggle_submit'
import ToggleOnSubmit from './elements/toggle_on_submit' import ToggleOnSubmit from './elements/toggle_on_submit'
import ToggleOnChecked from './elements/toggle_on_checked'
import ToggleSendTitle from './elements/toggle_send_title'
import AppendTemplate from './elements/append_template'
import CheckOnClick from './elements/check_on_click' import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input' import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input' import SearchInput from './elements/search_input'
@@ -159,6 +162,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-submit', ToggleSubmit) safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('toggle-on-checked', ToggleOnChecked)
safeRegisterElement('toggle-send-title', ToggleSendTitle)
safeRegisterElement('append-template', AppendTemplate)
safeRegisterElement('password-input', PasswordInput) safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput) safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute) safeRegisterElement('toggle-attribute', ToggleAttribute)
@@ -222,6 +228,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
template, template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'), dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
autosave: this.dataset.autosave !== 'false',
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')), dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
@@ -0,0 +1,29 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', this.onClick)
}
disconnectedCallback () {
this.removeEventListener('click', this.onClick)
}
onClick = (e) => {
e.preventDefault()
const target = document.getElementById(this.dataset.targetId)
const item = this.querySelector('template').content.firstElementChild.cloneNode(true)
item.querySelector('a').addEventListener('click', (e) => {
e.preventDefault()
item.remove()
target.classList.toggle('hidden', !target.childElementCount)
})
target.append(item)
target.classList.remove('hidden')
item.querySelector('input').focus()
}
}
+18 -1
View File
@@ -2,7 +2,7 @@ import { actionable } from '@github/catalyst/lib/actionable'
import { targets, targetable } from '@github/catalyst/lib/targetable' import { targets, targetable } from '@github/catalyst/lib/targetable'
export default actionable(targetable(class extends HTMLElement { export default actionable(targetable(class extends HTMLElement {
static [targets.static] = ['items'] static [targets.static] = ['items', 'groups']
addItem (e) { addItem (e) {
e.preventDefault() e.preventDefault()
@@ -24,11 +24,28 @@ export default actionable(targetable(class extends HTMLElement {
duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden')) duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden'))
originalItem.parentNode.append(duplicateItem) originalItem.parentNode.append(duplicateItem)
this.toggleMultipleClasses()
duplicateItem.querySelector("input:not([type='hidden'])")?.focus()
} }
removeItem (e) { removeItem (e) {
e.preventDefault() e.preventDefault()
this.items.find((item) => item.contains(e.target))?.remove() this.items.find((item) => item.contains(e.target))?.remove()
this.toggleMultipleClasses()
}
toggleMultipleClasses () {
const isMultiple = this.items.length > 1
const elements = [...this.items, ...this.groups]
elements.forEach((element) => {
const classes = element.dataset.multipleClasses || this.dataset.multipleClasses
classes?.split(' ').forEach((cls) => element.classList.toggle(cls, isMultiple))
})
} }
})) }))
+1 -1
View File
@@ -22,7 +22,7 @@ export default class extends HTMLElement {
} }
async flashFrom (fetchResponse) { async flashFrom (fetchResponse) {
const html = await fetchResponse?.responseHTML const html = await fetchResponse?.responseHTML.catch(() => null)
const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash') const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash')
if (flash?.dataset.alert) return { alert: flash.dataset.alert } if (flash?.dataset.alert) return { alert: flash.dataset.alert }
@@ -0,0 +1,23 @@
export default class extends HTMLElement {
connectedCallback () {
this.form.addEventListener('change', this.toggle)
this.toggle()
}
disconnectedCallback () {
this.form.removeEventListener('change', this.toggle)
}
toggle = () => {
const selector = this.dataset.names.split(' ').map((name) => `[name="${name}"]:checked:enabled`).join(', ')
const isChecked = !!this.form.querySelector(selector)
this.firstElementChild.classList.toggle('hidden', !isChecked)
this.lastElementChild.classList.toggle('hidden', isChecked)
}
get form () {
return this.closest('form')
}
}
@@ -0,0 +1,55 @@
export default class extends HTMLElement {
connectedCallback () {
this.events.forEach((event) => this.form.addEventListener(event, this.toggle))
this.toggle()
}
disconnectedCallback () {
this.events.forEach((event) => this.form.removeEventListener(event, this.toggle))
}
toggle = (event) => {
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
if (event?.type === 'focusin' && !this.isKeepSendUsed && this.isFocusFromName(event)) {
this.isKeepSendUsed = true
this.keepSendInput = event.target
} else if (event?.type === 'focusout' && event.target === this.keepSendInput) {
this.keepSendInput = null
}
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) && !this.keepSendInput?.matches(this.fieldSelector(field))
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
(this.isChecked('send_sms') && !isNameOnly('phone'))
this.firstElementChild.classList.toggle('hidden', !isSend)
this.lastElementChild.classList.toggle('hidden', isSend)
}
isChecked (name) {
return !!this.form.querySelector(`[name="${name}"]:checked:enabled`)
}
isFilled (field) {
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
}
isFocusFromName ({ target, relatedTarget }) {
return !!relatedTarget?.matches(this.fieldSelector('name')) && !!relatedTarget.value.trim() &&
['email', 'phone'].some((field) => target.matches(this.fieldSelector(field)))
}
fieldSelector (field) {
return `[name$="[][${field}]"]`
}
get events () {
return ['change', 'input', 'linked-input.update', 'focusin', 'focusout']
}
get form () {
return this.closest('form')
}
}
+3
View File
@@ -24,6 +24,9 @@ const ICONS = {
user: 'person', user: 'person',
users: 'person.2', users: 'person.2',
users_plus: 'person.badge.plus', users_plus: 'person.badge.plus',
user_plus: 'person.badge.plus',
eye_plus: 'eye',
mail_plus: 'envelope',
photo: 'photo', photo: 'photo',
settings: 'gearshape', settings: 'gearshape',
logout: 'rectangle.portrait.and.arrow.right', logout: 'rectangle.portrait.and.arrow.right',
+11 -2
View File
@@ -3,11 +3,19 @@
v-if="!error" v-if="!error"
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"
> >
<span class="flex items-end">
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template> <template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
<span
class="tooltip tooltip-bottom ml-2 mb-1"
data-tip="To verify your identity, the details you enter are checked against third-party records, which may include credit bureau data, and used to generate questions based on those records. Only these identity details are shared with the identity verification provider; this document and your tax return information are not shared with it or with any credit reporting company. If the check is recorded on your credit report, it appears as a soft inquiry, which does not affect your credit score and is not shown to lenders. This check does not give the sender of this document or the IRS access to your credit report."
>
<IconInfoCircle class="w-4 h-4 text-neutral-500" />
</span>
</span>
<span <span
v-if="questions" v-if="questions"
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm" class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
@@ -282,13 +290,14 @@
<script> <script>
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue' import { IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue'
export default { export default {
name: 'KbaStep', name: 'KbaStep',
components: { components: {
MarkdownContent, MarkdownContent,
IconInnerShadowTop IconInnerShadowTop,
IconInfoCircle
}, },
inject: ['baseUrl', 't'], inject: ['baseUrl', 't'],
props: { props: {
+1 -3
View File
@@ -319,7 +319,7 @@ export default {
default: false default: false
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title', 'multi-select'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
data () { data () {
return { return {
isContenteditable: false, isContenteditable: false,
@@ -798,8 +798,6 @@ export default {
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1) this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
} }
this.$emit('multi-select', e)
return return
} }
+64 -4
View File
@@ -3,8 +3,10 @@
ref="dragContainer" ref="dragContainer"
style="max-width: 1600px" style="max-width: 1600px"
:class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'" :class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'"
@dragstart="isInternalDrag = true"
@dragend="isInternalDrag = false"
@dragover="onDragover" @dragover="onDragover"
@drop="isDragFile = false" @drop="[isDragFile = false, isInternalDrag = false]"
> >
<HoverDropzone <HoverDropzone
v-if="sortedDocuments.length && withUploadButton && editable && !isSheetView" v-if="sortedDocuments.length && withUploadButton && editable && !isSheetView"
@@ -413,9 +415,9 @@
<a <a
:href="`/templates/${template.id}/form`" :href="`/templates/${template.id}/form`"
:data-native-actions="template.submitters.length > 1 ? 'users' : null" :data-native-actions="template.submitters.length > 1 ? 'users' : null"
data-turbo="false" data-turbo-prefetch="false"
class="flex items-center justify-center space-x-2" class="flex items-center justify-center space-x-2"
@click.exact="isPreviewLoading = true" @click.exact="onSaveAndPreviewClick"
> >
<IconInnerShadowTop <IconInnerShadowTop
v-if="isPreviewLoading" v-if="isPreviewLoading"
@@ -638,6 +640,7 @@
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField" :draw-custom-field="drawCustomField"
:draw-option="drawOption" :draw-option="drawOption"
:autosave="autosave"
@update="onDynamicDocumentUpdate" @update="onDynamicDocumentUpdate"
@draw="clearDrawField" @draw="clearDrawField"
@add-custom-field="addCustomField" @add-custom-field="addCustomField"
@@ -1377,6 +1380,7 @@ export default {
isPreviewLoading: false, isPreviewLoading: false,
isLoadingBlankPage: false, isLoadingBlankPage: false,
isSaving: false, isSaving: false,
hasUnsavedChanges: false,
isDetectingPageFields: false, isDetectingPageFields: false,
detectFieldsQueue: [], detectFieldsQueue: [],
detectingAnalyzingProgress: null, detectingAnalyzingProgress: null,
@@ -1390,6 +1394,7 @@ export default {
drawOption: null, drawOption: null,
dragField: null, dragField: null,
isDragFile: false, isDragFile: false,
isInternalDrag: false,
isMathLoaded: false, isMathLoaded: false,
isRevisionsModalOpen: false, isRevisionsModalOpen: false,
isDocumentsDrawerOpen: false, isDocumentsDrawerOpen: false,
@@ -1659,6 +1664,11 @@ export default {
window.addEventListener('dragleave', this.onWindowDragLeave) window.addEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.addEventListener('beforeunload', this.onBeforeUnload)
document.addEventListener('turbo:before-visit', this.onBeforeVisit)
}
this.$nextTick(() => { this.$nextTick(() => {
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()
@@ -1682,6 +1692,11 @@ export default {
this.resizeObserver.disconnect() this.resizeObserver.disconnect()
window.removeEventListener('dragleave', this.onWindowDragLeave) window.removeEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.removeEventListener('beforeunload', this.onBeforeUnload)
document.removeEventListener('turbo:before-visit', this.onBeforeVisit)
}
}, },
beforeUpdate () { beforeUpdate () {
this.documentRefs = [] this.documentRefs = []
@@ -2039,9 +2054,19 @@ export default {
ref.x = e.clientX - ref.offsetX ref.x = e.clientX - ref.offsetX
ref.y = e.clientY - ref.offsetY ref.y = e.clientY - ref.offsetY
} else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid) { } else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid && !this.isInternalDrag) {
const items = Array.from(e.dataTransfer.items)
if (this.dynamicDocuments.length && items.length && items.every((item) => item.type.startsWith('image/'))) {
if (!e.defaultPrevented) {
e.preventDefault()
e.dataTransfer.dropEffect = 'none'
}
} else {
this.isDragFile = true this.isDragFile = true
} }
}
}, },
onWindowDragLeave (event) { onWindowDragLeave (event) {
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) { if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
@@ -2228,6 +2253,37 @@ export default {
this.insertField(field) this.insertField(field)
} }
}, },
onSaveAndPreviewClick (event) {
this.isPreviewLoading = true
if (!this.autosave) {
event.preventDefault()
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
dynamicDocumentRefs.forEach((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false })
Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())]).then(() => {
window.Turbo.visit(`/templates/${this.template.id}/form`)
})
}
},
onBeforeUnload (event) {
if (this.hasUnsavedChanges) {
event.preventDefault()
event.returnValue = ''
return ''
}
},
onBeforeVisit (event) {
if (this.hasUnsavedChanges && !window.confirm(this.t('close_without_saving_changes'))) {
event.preventDefault()
}
},
closeDropdown () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
}, },
@@ -4023,6 +4079,8 @@ export default {
} }
if (!this.autosave && !force) { if (!this.autosave && !force) {
this.hasUnsavedChanges = true
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -4045,6 +4103,8 @@ export default {
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(() => { }).then(() => {
this.hasUnsavedChanges = false
if (this.onSave) { if (this.onSave) {
this.onSave(this.template) this.onSave(this.template)
} }
@@ -84,6 +84,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
autosave: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: false, required: false,
@@ -279,7 +284,7 @@ export default {
this.updatedSectionEditors.set(section.id, editor) this.updatedSectionEditors.set(section.id, editor)
this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000) this.saveTimer = setTimeout(() => this.autosave ? this.updateSectionsAndSave() : this.updateSections(), 1000)
}, },
loadListsStyle () { loadListsStyle () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
@@ -299,7 +304,9 @@ export default {
updateVariablesSchema () { updateVariablesSchema () {
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body) this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
}, },
updateSectionsAndSave () { updateSections () {
delete this.saveTimer
this.updatedSectionEditors.forEach((editor, sectionId) => { this.updatedSectionEditors.forEach((editor, sectionId) => {
const target = this.bodyDom.getElementById(sectionId) const target = this.bodyDom.getElementById(sectionId)
@@ -315,6 +322,9 @@ export default {
this.updateVariablesSchema() this.updateVariablesSchema()
this.$emit('update', this.document) this.$emit('update', this.document)
},
updateSectionsAndSave () {
this.updateSections()
return this.saveBody() return this.saveBody()
}, },
@@ -27,13 +27,17 @@ tiptapStylesheet.replaceSync(
font-feature-settings: "liga" 0; font-feature-settings: "liga" 0;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
min-height: inherit; flex-grow: 1;
} }
.ProseMirror > article { .ProseMirror > article:last-of-type {
margin-bottom: auto; margin-bottom: auto;
} }
.ProseMirror p.doc-trailing-break::after {
content: none;
}
.ProseMirror [contenteditable="false"] { .ProseMirror [contenteditable="false"] {
white-space: normal; white-space: normal;
} }
@@ -639,6 +639,20 @@ export default {
if (!file) return if (!file) return
const image = await this.uploadImage(file)
if (image) {
this.editor.chain().focus().insertContent(image).run()
}
},
async insertImageFiles (files, pos) {
const images = (await Promise.all(files.map(this.uploadImage))).filter(Boolean)
if (images.length) {
this.editor.chain().focus().insertContentAt(pos, images).run()
}
},
async uploadImage (file) {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
@@ -656,9 +670,7 @@ export default {
await this.$nextTick() await this.$nextTick()
this.editor.chain().focus() return { type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } }
.insertContent({ type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } })
.run()
}, },
setDynamicMenuCoords (editor) { setDynamicMenuCoords (editor) {
const { from, to } = editor.state.selection const { from, to } = editor.state.selection
@@ -975,7 +987,7 @@ export default {
onSelectedAreaChange () { onSelectedAreaChange () {
this.save() this.save()
}, },
onFieldDrop (view, event, _slice, moved) { onFieldDrop (view, event, slice, moved) {
this.isAreaDrag = false this.isAreaDrag = false
if (moved) { if (moved) {
@@ -983,8 +995,9 @@ export default {
} }
const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField
const imageFiles = slice.size ? [] : Array.from(event.dataTransfer.files).filter((file) => file.type.startsWith('image/'))
if (!draggedField) return false if (!draggedField && !imageFiles.length) return false
event.preventDefault() event.preventDefault()
@@ -992,6 +1005,7 @@ export default {
if (!pos) return false if (!pos) return false
if (draggedField) {
this.insertFieldAtRange({ this.insertFieldAtRange({
sourceField: draggedField, sourceField: draggedField,
existingField: this.fieldsDragFieldRef?.value, existingField: this.fieldsDragFieldRef?.value,
@@ -1000,6 +1014,9 @@ export default {
this.fieldsDragFieldRef.value = null this.fieldsDragFieldRef.value = null
this.customDragFieldRef.value = null this.customDragFieldRef.value = null
} else {
this.insertImageFiles(imageFiles, pos.pos)
}
return true return true
}, },
+7
View File
@@ -70,6 +70,7 @@ const en = {
remove_condition: 'Remove condition', remove_condition: 'Remove condition',
add_condition: 'Add condition', add_condition: 'Add condition',
are_you_sure_: 'Are you sure?', are_you_sure_: 'Are you sure?',
close_without_saving_changes: 'Close without saving changes?',
sign_yourself: 'Sign Yourself', sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date', set_signing_date: 'Set signing date',
signing_date: 'Signing Date', signing_date: 'Signing Date',
@@ -371,6 +372,7 @@ const es = {
down: 'Abajo', down: 'Abajo',
set_signing_date: 'Establecer fecha de firma', set_signing_date: 'Establecer fecha de firma',
are_you_sure_: '¿Estás seguro?', are_you_sure_: '¿Estás seguro?',
close_without_saving_changes: '¿Cerrar sin guardar los cambios?',
sign_yourself: 'Firma tú mismo', sign_yourself: 'Firma tú mismo',
checked: 'Marcado', checked: 'Marcado',
current_date: 'Fecha actual', current_date: 'Fecha actual',
@@ -652,6 +654,7 @@ const it = {
remove_condition: 'Rimuovi condizione', remove_condition: 'Rimuovi condizione',
add_condition: 'Aggiungi condizione', add_condition: 'Aggiungi condizione',
are_you_sure_: 'Sei sicuro?', are_you_sure_: 'Sei sicuro?',
close_without_saving_changes: 'Chiudere senza salvare le modifiche?',
sign_yourself: 'Firma te stesso', sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma', set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma', signing_date: 'Data di firma',
@@ -953,6 +956,7 @@ const pt = {
down: 'Para baixo', down: 'Para baixo',
set_signing_date: 'Definir data de assinatura', set_signing_date: 'Definir data de assinatura',
are_you_sure_: 'Tem certeza?', are_you_sure_: 'Tem certeza?',
close_without_saving_changes: 'Fechar sem salvar as alterações?',
sign_yourself: 'Assine você mesmo', sign_yourself: 'Assine você mesmo',
checked: 'Marcado', checked: 'Marcado',
current_date: 'Data atual', current_date: 'Data atual',
@@ -1234,6 +1238,7 @@ const fr = {
remove_condition: 'Supprimer la condition', remove_condition: 'Supprimer la condition',
add_condition: 'Ajouter une condition', add_condition: 'Ajouter une condition',
are_you_sure_: 'Êtes-vous sûr ?', are_you_sure_: 'Êtes-vous sûr ?',
close_without_saving_changes: 'Fermer sans enregistrer les modifications ?',
sign_yourself: 'Signer vous-même', sign_yourself: 'Signer vous-même',
set_signing_date: 'Définir la date de signature', set_signing_date: 'Définir la date de signature',
signing_date: 'Date de signature', signing_date: 'Date de signature',
@@ -1525,6 +1530,7 @@ const de = {
remove_condition: 'Bedingung entfernen', remove_condition: 'Bedingung entfernen',
add_condition: 'Bedingung hinzufügen', add_condition: 'Bedingung hinzufügen',
are_you_sure_: 'Sind Sie sicher?', are_you_sure_: 'Sind Sie sicher?',
close_without_saving_changes: 'Ohne Speichern der Änderungen schließen?',
sign_yourself: 'Selbst unterschreiben', sign_yourself: 'Selbst unterschreiben',
set_signing_date: 'Unterzeichnungsdatum festlegen', set_signing_date: 'Unterzeichnungsdatum festlegen',
signing_date: 'Unterzeichnungsdatum', signing_date: 'Unterzeichnungsdatum',
@@ -1816,6 +1822,7 @@ const nl = {
remove_condition: 'Voorwaarde verwijderen', remove_condition: 'Voorwaarde verwijderen',
add_condition: 'Voorwaarde toevoegen', add_condition: 'Voorwaarde toevoegen',
are_you_sure_: 'Weet u het zeker?', are_you_sure_: 'Weet u het zeker?',
close_without_saving_changes: 'Sluiten zonder wijzigingen op te slaan?',
sign_yourself: 'Zelf ondertekenen', sign_yourself: 'Zelf ondertekenen',
set_signing_date: 'Ondertekeningsdatum instellen', set_signing_date: 'Ondertekeningsdatum instellen',
signing_date: 'Ondertekeningsdatum', signing_date: 'Ondertekeningsdatum',
-8
View File
@@ -57,7 +57,6 @@
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field', $event)" @add-custom-field="$emit('add-custom-field', $event)"
@contextmenu="openAreaContextMenu($event, item.area, item.field)" @contextmenu="openAreaContextMenu($event, item.area, item.field)"
@multi-select="openMultiSelectContextMenu"
@click-title="closeContextMenu" @click-title="closeContextMenu"
/> />
<FieldArea <FieldArea
@@ -450,13 +449,6 @@ export default {
areas: this.selectedAreasRef.value areas: this.selectedAreasRef.value
} }
}, },
openMultiSelectContextMenu (event) {
if (this.selectedAreasRef.value.length >= 2) {
this.openSelectionContextMenu(event)
} else {
this.closeContextMenu()
}
},
handleSelectionCopy () { handleSelectionCopy () {
this.$emit('copy-selected-areas') this.$emit('copy-selected-areas')
+5 -3
View File
@@ -30,14 +30,16 @@ class SendTestWebhookRequestJob
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS) raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
end end
Faraday.post(webhook_url.url, body_json = {
{
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current.iso8601, timestamp: Time.current.iso8601,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json
Faraday.post(webhook_url.url, body_json,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT, 'User-Agent' => USER_AGENT,
'X-Docuseal-Signature' => WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: body_json),
**webhook_url.secret.to_h) **webhook_url.secret.to_h)
end end
end end
+1
View File
@@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format' DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields' TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats' TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
TEMPLATE_BUILDER_AUTOSAVE_KEY = 'template_builder_autosave'
POLICY_LINKS_KEY = 'policy_links' POLICY_LINKS_KEY = 'policy_links'
ENABLE_MCP_KEY = 'enable_mcp' ENABLE_MCP_KEY = 'enable_mcp'
+9 -1
View File
@@ -25,15 +25,17 @@
# account_id :bigint not null # account_id :bigint not null
# external_id :string # external_id :string
# submission_id :bigint not null # submission_id :bigint not null
# template_id :bigint
# #
# Indexes # Indexes
# #
# index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL) # index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL)
# index_submitters_on_account_id_and_id (account_id,id) # index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_email (email) # index_submitters_on_email_and_template_id (email,template_id) WHERE (email IS NOT NULL)
# index_submitters_on_external_id (external_id) # index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE # index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id) # index_submitters_on_submission_id (submission_id)
# index_submitters_on_template_id_and_id (template_id,id)
# #
# Foreign Keys # Foreign Keys
# #
@@ -68,6 +70,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
before_validation :set_template_id, on: :create
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? } after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status def status
@@ -124,6 +128,10 @@ class Submitter < ApplicationRecord
private private
def set_template_id
self.template_id = submission&.template_id
end
def anonymize_email_events def anonymize_email_events
email_events.each do |event| email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email)) event.update!(email: Digest::MD5.base64digest(event.email))
+1 -1
View File
@@ -196,7 +196,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
<% if can?(:manage, account_config) %> <% if !Docuseal.demo? && can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M12 18c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
<path d="M16 19h6" />
<path d="M19 16v6" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 19h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" />
<path d="M16 19h6" />
<path d="M19 16v6" />
<path d="M3 7l9 6l9 -6" />
</svg>

After

Width:  |  Height:  |  Size: 441 B

+2 -3
View File
@@ -1,8 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /> <path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4c.96 0 1.84 .338 2.53 .901" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M16 19h6" /> <path d="M16 19h6" />
<path d="M19 16v6" /> <path d="M19 16v6" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h4" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 421 B

+3 -3
View File
@@ -33,10 +33,10 @@
<% end %> <% end %>
<turbo-frame id="drawer"></turbo-frame> <turbo-frame id="drawer"></turbo-frame>
<%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %> <%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <main class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<%= yield %> <%= yield %>
</div> </main>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<%= render 'shared/body_scripts' %> <%= render 'shared/body_scripts' %>
</body> </body>
</html> </html>
+2
View File
@@ -13,6 +13,8 @@
<%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %> <%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %>
</head> </head>
<body> <body>
<main>
<%= yield %> <%= yield %>
</main>
</body> </body>
</html> </html>
+3 -1
View File
@@ -13,8 +13,10 @@
<%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %> <%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %>
</head> </head>
<body> <body>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <main>
<%= yield %> <%= yield %>
</main>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
</body> </body>
</html> </html>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="w-full text-center"> <div class="w-full text-center">
<span class="font-bold"><%= t('demo_environment') %></span> <span class="font-bold"><%= t('demo_environment') %></span>
<br> <br>
<%= t('create_a_new_template_document_form_or_submit_the_existing_one_html', new_template_link: new_template_path, start_form_link: start_form_url(slug: ::Template.first&.slug)) %> 😊 <%= t('create_a_new_template_document_form_html', new_template_link: new_template_path) %>
</div> </div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -22,13 +22,13 @@
<% end %> <% end %>
<% if can?(:create, ::Template) && controller_name.in?(%w[templates_dashboard submissions_dashboard template_folders templates_archived submissions_archived]) %> <% if can?(:create, ::Template) && controller_name.in?(%w[templates_dashboard submissions_dashboard template_folders templates_archived submissions_archived]) %>
<% folder_name = @template_folder&.full_name %> <% folder_name = @template_folder&.full_name %>
<native-action data-placement="create" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>"></native-action> <native-action data-placement="create" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>"></native-action>
<native-action data-placement="create" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action> <native-action data-placement="create" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action>
<native-action data-placement="create" data-icon="pencil_plus"> <native-action data-placement="create" data-icon="pencil_plus">
<%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %> <%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %>
</native-action> </native-action>
<% unless controller_name.in?(%w[templates_archived submissions_archived]) %> <% unless controller_name.in?(%w[templates_archived submissions_archived]) %>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>"></native-action> <native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action> <native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="pencil_plus"> <native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="pencil_plus">
<%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %> <%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %>
+2 -2
View File
@@ -1,5 +1,5 @@
<%= render 'shared/navbar_warning' %> <%= render 'shared/navbar_warning' %>
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between"> <nav class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2"> <a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/title' %> <%= render 'shared/title' %>
@@ -106,4 +106,4 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </nav>
+2 -2
View File
@@ -93,14 +93,14 @@
</li> </li>
<% end %> <% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<% if Docuseal.multitenant? || !turbo_native_ios? %> <% unless turbo_native_app? %>
<li> <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 %> <%= 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 %> <% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if Docuseal.multitenant? %> <% if Docuseal.multitenant? && !turbo_native_app? %>
<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 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') %> <%= t('embedding') %>
+3 -3
View File
@@ -1,4 +1,4 @@
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -12,7 +12,7 @@
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %> <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
</p> </p>
</div> </div>
</div> </div>
@@ -32,5 +32,5 @@
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>
</main> </div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %> <% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -55,4 +55,4 @@
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %> <%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
<% end %> <% end %>
</div> </div>
</main> </div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %> <% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -28,4 +28,4 @@
<%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %> <%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
</div> </div>
</div> </div>
</main> </div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %> <% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<% end %> <% end %>
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4"> <div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'start_form/banner' %> <%= render 'start_form/banner' %>
<p class="text-xl font-semibold text-center"> <p class="text-xl font-semibold text-center">
@@ -27,5 +27,5 @@
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %> <%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</main> </div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'shared/attribution', link_path: '/start', account: @template.account %>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %> <% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
@@ -66,4 +66,4 @@
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'shared/attribution', link_path: '/start', account: @template.account %>
<%= render 'start_form/policy', account: @template.account %> <%= render 'start_form/policy', account: @template.account %>
</div> </div>
</main> </div>
+70
View File
@@ -0,0 +1,70 @@
<% fields_id = "add_new_fields_#{SecureRandom.hex(3)}" %>
<div id="<%= fields_id %>" class="space-y-3 mt-4 hidden" data-targets="dynamic-list.groups" data-multiple-classes="px-3 pt-3 pb-4 rounded-xl border border-dashed border-base-300"></div>
<native-menu class="<%= turbo_native_app? ? 'block' : 'dropdown block max-sm:hidden' %> mt-4 -mb-2">
<label tabindex="0" class="btn btn-ghost btn-sm normal-case font-normal w-full flex items-center justify-center border-dashed border-base-300 hover:border-base-content/20 !bg-transparent">
<%= svg_icon('users_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</label>
<ul tabindex="0" class="<%= turbo_native_app? ? 'hidden' : 'z-10 dropdown-content p-2 mt-1 shadow menu bg-base-100 rounded-box' %>" style="left: 50%; --tw-translate-x: -50%">
<li>
<native-menu-action data-icon="user_plus" data-label="<%= t('recipients') %>" class="grid-cols-1 p-0">
<a href="#" class="flex items-center gap-2 px-4 py-2" data-action="click:dynamic-list#addItem">
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span class="whitespace-nowrap"><%= t('recipients') %></span>
</a>
</native-menu-action>
</li>
<append-template class="contents" data-target-id="<%= fields_id %>">
<li>
<native-menu-action data-icon="eye_plus" data-label="<%= t('cc_view_only') %>" class="grid-cols-1 p-0">
<a href="#" class="flex items-center gap-2 px-4 py-2">
<%= svg_icon('eye_plus', class: 'w-4 h-4 stroke-2') %>
<span class="whitespace-nowrap"><%= t('cc_view_only') %></span>
</a>
</native-menu-action>
</li>
<template>
<submitter-item class="form-control">
<label class="label pt-0 pb-1">
<span class="label-text"><%= t('cc_view_only') %></span>
</label>
<div class="relative">
<submitters-autocomplete data-field="email">
<input type="email" name="cc_recipients[]" autocomplete="off" class="base-input !h-10 w-full pr-10" placeholder="<%= t('email') %>">
</submitters-autocomplete>
<a href="#" class="absolute right-2 top-2 block w-6 h-6 text-neutral-700 p-1 hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %>
</a>
</div>
</submitter-item>
</template>
</append-template>
<append-template class="contents" data-target-id="<%= fields_id %>">
<li>
<native-menu-action data-icon="mail_plus" data-label="<%= t('bcc_completed') %>" class="grid-cols-1 p-0">
<a href="#" class="flex items-center gap-2 px-4 py-2">
<%= svg_icon('mail_plus', class: 'w-4 h-4 stroke-2') %>
<span class="whitespace-nowrap"><%= t('bcc_completed') %></span>
</a>
</native-menu-action>
</li>
<template>
<div class="form-control">
<label class="label pt-0 pb-1">
<span class="label-text"><%= t('bcc_completed') %></span>
</label>
<div class="relative">
<input type="email" name="bcc_completed[]" autocomplete="off" class="base-input !h-10 w-full pr-10" placeholder="<%= t('email') %>">
<a href="#" class="absolute right-2 top-2 block w-6 h-6 text-neutral-700 p-1 hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %>
</a>
</div>
<% default_bcc_completed = template.preferences['bcc_completed'].presence || current_account.account_configs.find_by(key: AccountConfig::BCC_EMAILS)&.value %>
<% if default_bcc_completed.present? %>
<input type="hidden" name="bcc_completed[]" value="<%= default_bcc_completed %>">
<% end %>
</div>
</template>
</append-template>
</ul>
</native-menu>
+14 -12
View File
@@ -1,12 +1,12 @@
<% has_phone_field = false %> <% has_phone_field = false %>
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="block" data-multiple-classes="px-3 <%= submitters.size > 1 ? 'pt-3 pb-4' : 'py-4' %> rounded-xl border border-dashed border-base-300">
<div class="space-y-4"> <div class="space-y-4">
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items"> <div class="relative" data-targets="dynamic-list.items">
<div> <div>
<div class="absolute right-4 top-5"> <div class="absolute top-5" style="right: 12px">
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white"> <a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %> <%= svg_icon('trash', class: 'w-4 h-4') %>
</a> </a>
</div> </div>
@@ -86,14 +86,11 @@
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %> <% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem"> <%= render 'add_new', template: %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</a>
<% end %> <% end %>
</dynamic-list> </dynamic-list>
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div class="mt-4">
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<% if has_phone_field %> <% if has_phone_field %>
@@ -101,7 +98,12 @@
<% end %> <% end %>
<%= render 'extra_fields', f: %> <%= render 'extra_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control mt-4">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button class: 'base-button' do %>
<toggle-send-title>
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
</toggle-send-title>
<% end %>
</div> </div>
<% end %> <% end %>
+15 -13
View File
@@ -1,4 +1,4 @@
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<% if submitters.size == 1 && local_assigns[:variables_form] %> <% if submitters.size == 1 && local_assigns[:variables_form] %>
<% item = submitters.first %> <% item = submitters.first %>
@@ -26,18 +26,18 @@
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>"> <emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
<submitters-autocomplete data-field="email" class="block relative"> <submitters-autocomplete data-field="email" class="block relative">
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :emails, required: true, class: 'base-textarea w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %> <%= f.text_area :emails, required: true, class: 'base-textarea block w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %>
</autoresize-textarea> </autoresize-textarea>
</submitters-autocomplete> </submitters-autocomplete>
</emails-textarea> </emails-textarea>
</submitter-item> </submitter-item>
<% else %> <% else %>
<dynamic-list class="space-y-4"> <dynamic-list class="block" data-multiple-classes="px-3 pt-3 pb-4 rounded-xl border border-dashed border-base-300">
<div class="space-y-4"> <div class="space-y-4">
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items"> <div class="relative" data-targets="dynamic-list.items">
<div> <div>
<div class="absolute right-4 top-5"> <div class="absolute top-5" style="right: 12px">
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white"> <a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %> <%= svg_icon('trash', class: 'w-4 h-4') %>
</a> </a>
</div> </div>
@@ -60,22 +60,24 @@
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> <% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem"> <%= render 'add_new', template: %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</a>
<% end %> <% end %>
</dynamic-list> </dynamic-list>
<% end %> <% end %>
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div class="mt-4">
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<%= render 'extra_fields', f: %> <%= render 'extra_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control mt-4">
<%= content_for(:submit_button) || capture do %> <%= content_for(:submit_button) || capture do %>
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button class: 'base-button' do %>
<toggle-on-checked data-names="send_email send_sms">
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
</toggle-on-checked>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+14 -12
View File
@@ -1,11 +1,11 @@
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="block" data-multiple-classes="px-3 <%= submitters.size > 1 ? 'pt-3 pb-4' : 'py-4' %> rounded-xl border border-dashed border-base-300">
<div class="space-y-4"> <div class="space-y-4">
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items"> <div class="relative" data-targets="dynamic-list.items">
<div> <div>
<div class="absolute right-4 top-5"> <div class="absolute top-5" style="right: 12px">
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white"> <a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %> <%= svg_icon('trash', class: 'w-4 h-4') %>
</a> </a>
</div> </div>
@@ -48,19 +48,21 @@
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> <% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem"> <%= render 'add_new', template: %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</a>
<% end %> <% end %>
</dynamic-list> </dynamic-list>
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div class="mt-4">
<%= render 'submitters_order', f:, template: %> <%= render 'submitters_order', f:, template: %>
<%= render 'send_sms', f:, checked: true %> <%= render 'send_sms', f:, checked: true %>
<%= render 'extra_phone_fields', f: %> <%= render 'extra_phone_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control mt-4">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button class: 'base-button' do %>
<toggle-on-checked data-names="send_email send_sms">
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
</toggle-on-checked>
<% end %>
</div> </div>
<% end %> <% end %>
@@ -14,7 +14,7 @@
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex"> <toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label> <label>
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %> <%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
<span class="link peer-checked:hidden"><%= t('edit_message') %></span> <span class="link link-hover peer-checked:hidden"><%= t('edit_message') %></span>
</label> </label>
</toggle-visible> </toggle-visible>
<% end %> <% end %>
+2
View File
@@ -1,5 +1,7 @@
<% unless turbo_native_ios? %> <% unless turbo_native_ios? %>
<toggle-on-checked data-names="is_custom_message" class="block">
<div class="form-control"> <div class="form-control">
<%= render 'sms_settings/placeholder' %> <%= render 'sms_settings/placeholder' %>
</div> </div>
</toggle-on-checked>
<% end %> <% end %>
+1 -1
View File
@@ -26,7 +26,7 @@
</div> </div>
</toggle-visible> </toggle-visible>
<% end %> <% end %>
<div class="px-5 mb-5 mt-4"> <div class="px-5 mb-5 mt-5">
<% unless only_detailed %> <% unless only_detailed %>
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>"> <div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %> <%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
+20 -14
View File
@@ -11,7 +11,7 @@
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %> <% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %> <% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
<% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %> <% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %>
<main style="max-width: 1600px" class="mx-auto pl-4"> <div style="max-width: 1600px" class="mx-auto pl-4">
<% unless native_tab == 'recipients' %> <% unless native_tab == 'recipients' %>
<div class="flex justify-between items-center pr-4 sticky top-0 md:relative z-10 bg-base-100 <%= 'py-1.5' unless turbo_native_app? %>"> <div class="flex justify-between items-center pr-4 sticky top-0 md:relative z-10 bg-base-100 <%= 'py-1.5' unless turbo_native_app? %>">
<% unless turbo_native_app? %> <% unless turbo_native_app? %>
@@ -54,7 +54,8 @@
<% if @submission.submitters.to_a.any?(&:completed_at?) %> <% if @submission.submitters.to_a.any?(&:completed_at?) %>
<% show_combined_download = @submission.completed_at? && !is_combined_enabled %> <% show_combined_download = @submission.completed_at? && !is_combined_enabled %>
<% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %> <% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %>
<% show_download_dropdown = show_combined_download || show_unarchive %> <% show_event_log_download = signed_in? && can?(:manage, :event_log_download) && can?(:read, @submission) %>
<% show_download_dropdown = show_combined_download || show_event_log_download || show_unarchive %>
<% if @submission.completed_at? || !is_combined_enabled %> <% if @submission.completed_at? || !is_combined_enabled %>
<div class="join relative"> <div class="join relative">
<native-action data-label="<%= t('download') %>" data-icon="download"> <native-action data-label="<%= t('download') %>" data-icon="download">
@@ -95,6 +96,9 @@
</li> </li>
</native-action> </native-action>
<% end %> <% end %>
<% if show_event_log_download %>
<%= render 'submissions/download_event_log' %>
<% end %>
<% if show_unarchive %> <% if show_unarchive %>
<native-action data-label="<%= t('unarchive') %>" data-icon="rotate" data-section="archive"> <native-action data-label="<%= t('unarchive') %>" data-icon="rotate" data-section="archive">
<li> <li>
@@ -163,6 +167,7 @@
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% submitter = submitters_index[field['submitter_uuid']] %> <% submitter = submitters_index[field['submitter_uuid']] %>
<% value = nil if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
<% if value.present? %> <% if value.present? %>
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
<span class="group"> <span class="group">
@@ -222,39 +227,39 @@
</div> </div>
<% if submitter&.name.present? %> <% if submitter&.name.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('user', class: 'w-5 h-5') %> <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
<span> <span class="truncate">
<%= submitter&.name %> <%= submitter&.name %>
</span> </span>
</div> </div>
<% end %> <% end %>
<% if submitter&.email.present? %> <% if submitter&.email.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('mail', class: 'w-5 h-5') %> <%= svg_icon('mail', class: 'w-5 h-5 shrink-0') %>
<span> <span class="truncate">
<%= submitter.email || 'N/A' %> <%= submitter.email || 'N/A' %>
</span> </span>
</div> </div>
<% end %> <% end %>
<% if submitter&.phone.present? %> <% if submitter&.phone.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('phone', class: 'w-5 h-5') %> <%= svg_icon('phone', class: 'w-5 h-5 shrink-0') %>
<span> <span class="truncate">
<%= submitter.phone %> <%= submitter.phone %>
</span> </span>
</div> </div>
<% end %> <% end %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<% if submitter&.viewer? %> <% if submitter&.viewer? %>
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5') %> <%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5 shrink-0') %>
<% elsif submitter && !submitter.completed_at? && @submission.completed_at? %> <% elsif submitter && !submitter.completed_at? && @submission.completed_at? %>
<%= svg_icon('writing_off', class: 'w-5 h-5') %> <%= svg_icon('writing_off', class: 'w-5 h-5 shrink-0') %>
<% elsif @submission.expire_at? && submitter && !submitter.completed_at? %> <% elsif @submission.expire_at? && submitter && !submitter.completed_at? %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %> <%= svg_icon('clock_exclamation', class: 'w-5 h-5 shrink-0') %>
<% else %> <% else %>
<%= svg_icon('writing', class: 'w-5 h-5') %> <%= svg_icon('writing', class: 'w-5 h-5 shrink-0') %>
<% end %> <% end %>
<span> <span class="truncate">
<% if submitter&.declined_at? %> <% if submitter&.declined_at? %>
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %> <%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif submitter&.viewer? %> <% elsif submitter&.viewer? %>
@@ -320,6 +325,7 @@
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %> <% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %>
<% next if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
<div class="text-xs font-medium uppercase mb-0.5" dir="auto"> <div class="text-xs font-medium uppercase mb-0.5" dir="auto">
<%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %>
@@ -388,7 +394,7 @@
</label> </label>
</toggle-visible> </toggle-visible>
<% end %> <% end %>
</main> </div>
<% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %> <% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %>
<%= render 'scripts/autosize_field' %> <%= render 'scripts/autosize_field' %>
<% end %> <% end %>
@@ -15,7 +15,7 @@
<% last_submitter = @submission.submitters.completed.order(:completed_at).last %> <% last_submitter = @submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %> <% if last_submitter %>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %> <%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.in_time_zone(@submission.account.timezone).to_date, format: :long)) %>
</p> </p>
<% end %> <% end %>
</div> </div>
+3 -3
View File
@@ -1,5 +1,5 @@
<% completed_at = @submitter.completed_at || @submitter.submission.completed_at %> <% completed_at = @submitter.completed_at || @submitter.submission.completed_at %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -13,7 +13,7 @@
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(completed_at.to_date, format: :long)) %> <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
</p> </p>
</div> </div>
</div> </div>
@@ -51,5 +51,5 @@
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>
</main> </div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
+1 -1
View File
@@ -11,7 +11,7 @@
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %></p> <p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -11,7 +11,7 @@
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-sm"><%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp, format: :long)) %></p> <p class="text-sm"><%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -3,7 +3,7 @@
<% I18n.with_locale(@submitter.account.locale) do %> <% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %> <% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -19,7 +19,7 @@
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %> <% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %> <% if last_submitter %>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %> <%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
</p> </p>
<% end %> <% end %>
</div> </div>
@@ -70,4 +70,4 @@
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div> <div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
<% end %> <% end %>
</div> </div>
</main> </div>
+1 -1
View File
@@ -11,7 +11,7 @@
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %></p> <p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %></p>
</div> </div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -14,7 +14,7 @@
<% delegate_modal_id = nil %> <% delegate_modal_id = nil %>
<% banner_html = turbo_native_app? ? '' : local_assigns[:banner_html] %> <% banner_html = turbo_native_app? ? '' : local_assigns[:banner_html] %>
<div style="max-height: -webkit-fill-available;"> <div style="max-height: -webkit-fill-available;">
<main id="scrollbox"> <div id="scrollbox">
<div id="sr_only_content"></div> <div id="sr_only_content"></div>
<div class="mx-auto block pb-72" style="max-width: 1000px"> <div class="mx-auto block pb-72" style="max-width: 1000px">
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %> <%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
@@ -172,7 +172,7 @@
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
</main> </div>
</div> </div>
<div class="fixed bottom-0 w-full h-0 z-50"> <div class="fixed bottom-0 w-full h-0 z-50">
<div class="mx-auto" style="max-width: 1000px"> <div class="mx-auto" style="max-width: 1000px">
+1 -1
View File
@@ -31,7 +31,7 @@
</div> </div>
</span> </span>
</div> </div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple> <input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" multiple>
</div> </div>
</label> </label>
</file-dropzone> </file-dropzone>
+1 -1
View File
@@ -16,7 +16,7 @@
</label> </label>
<input type="hidden" name="form_id" value="<%= form_id %>"> <input type="hidden" name="form_id" value="<%= form_id %>">
<convert-upload> <convert-upload>
<input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple> <input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" multiple>
</convert-upload> </convert-upload>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>"> <input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %> <% end %>
+3 -2
View File
@@ -7,7 +7,8 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %> <%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %> <% end %>
<% end %> <% end %>
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %> <% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY, AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY]).to_a %>
<% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %> <% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %>
<% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %> <% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder> <% autosave = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY }&.value != false %>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" data-accept-file-types="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-autosave="<%= autosave %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
+1 -1
View File
@@ -26,7 +26,7 @@
<%= t('share_template_with_test_mode') %> <%= t('share_template_with_test_mode') %>
</span> </span>
<submit-form data-on="change"> <submit-form data-on="change">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: true_user.account.testing_accounts) %> <%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: (can?(:manage, :tenants_test) ? current_account : true_user.account).testing_accounts) %>
</submit-form> </submit-form>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -127,4 +127,4 @@
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q]) %> <%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q]) %>
<% end %> <% end %>
<% end %> <% end %>
<%= render 'shared/review_form' %> <%= render 'shared/review_form' unless turbo_native_app? %>
@@ -28,7 +28,7 @@
</submit-form> </submit-form>
</form> </form>
<% end %> <% end %>
<a href="<%= edit_template_path(@template) %>" class="base-button" data-turbo="false" style="flex-shrink: 0; padding: 0px 24px;"> <a href="<%= edit_template_path(@template) %>" class="base-button" data-turbo-prefetch="false" style="flex-shrink: 0; padding: 0px 24px;">
<%= t('exit_preview') %> <%= t('exit_preview') %>
</a> </a>
</div> </div>
@@ -194,7 +194,7 @@
<%= t('share_template_with_test_mode') %> <%= t('share_template_with_test_mode') %>
</span> </span>
<submit-form data-on="change" class="flex"> <submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: true_user.account.testing_accounts) %> <%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: (can?(:manage, :tenants_test) ? current_account : true_user.account).testing_accounts) %>
</submit-form> </submit-form>
</div> </div>
<% end %> <% end %>
+2 -2
View File
@@ -5,11 +5,11 @@
"note": "Permitted parameters are necessary for creating submitters via API" "note": "Permitted parameters are necessary for creating submitters via API"
}, },
{ {
"fingerprint": "645f2f352a813105b8b2eeadc68101d2e7ea04b8e839aa1336616be56f1108ca", "fingerprint": "58619647d807d5bedb9499d6b1432f7f9cee385167aa97b0cb5e1b31a3db3f40",
"note": "Safe SQL" "note": "Safe SQL"
}, },
{ {
"fingerprint": "5bcf4e6426d17b0efd98e47fde7c012f09500b9595af9cff226cf09341c023be", "fingerprint": "8538c7cca701df19fdc3c1c6c5b7b0a1a3acacc19c840d63147553014d60ffa0",
"note": "Safe SQL" "note": "Safe SQL"
}, },
{ {
+1
View File
@@ -9,3 +9,4 @@ autoload :OnnxRuntime, 'onnxruntime'
autoload :RQRCode, 'rqrcode' autoload :RQRCode, 'rqrcode'
autoload :ArabicLetterConnector, 'arabic-letter-connector/logic' autoload :ArabicLetterConnector, 'arabic-letter-connector/logic'
autoload :JWT, 'jwt' autoload :JWT, 'jwt'
autoload :WordCraft, 'wordcraft'
+33 -12
View File
@@ -704,6 +704,7 @@ en: &en
developer_sandbox: Developer Sandbox. developer_sandbox: Developer Sandbox.
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> to start using in Production.' upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> to start using in Production.'
download_combined_pdf: Download combined PDF download_combined_pdf: Download combined PDF
download_event_log: Download Event Log
audit_log: Audit Log audit_log: Audit Log
signers: Signers signers: Signers
not_invited_yet: Not invited yet not_invited_yet: Not invited yet
@@ -715,7 +716,7 @@ en: &en
declined_on_time: 'Declined on %{time}' declined_on_time: 'Declined on %{time}'
expire_on_time: 'Expire on %{time}' expire_on_time: 'Expire on %{time}'
sign_in_person: Sign In-person sign_in_person: Sign In-person
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submit the existing one</a> create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address.
re_send_email: Re-send Email re_send_email: Re-send Email
re_send_emails: Re-send Emails re_send_emails: Re-send Emails
@@ -1027,6 +1028,8 @@ en: &en
failed_to_scan_the_document: Failed to scan the document failed_to_scan_the_document: Failed to scan the document
document_scanning_is_not_available_on_this_device: Document scanning is not available on this device document_scanning_is_not_available_on_this_device: Document scanning is not available on this device
click_to_upload: Click to upload click_to_upload: Click to upload
cc_view_only: CC View-only
bcc_completed: BCC Completed
devise: devise:
confirmations: confirmations:
confirmed: Your email address has been successfully confirmed. confirmed: Your email address has been successfully confirmed.
@@ -1862,6 +1865,7 @@ es: &es
developer_sandbox: Sandbox para desarrolladores. developer_sandbox: Sandbox para desarrolladores.
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Actualiza</a> para empezar a usar en Producción.' upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Actualiza</a> para empezar a usar en Producción.'
download_combined_pdf: Descargar PDF combinado download_combined_pdf: Descargar PDF combinado
download_event_log: Descargar registro de eventos
audit_log: Registro de Auditoría audit_log: Registro de Auditoría
signers: Firmantes signers: Firmantes
not_invited_yet: Aún no invitado not_invited_yet: Aún no invitado
@@ -1873,7 +1877,7 @@ es: &es
declined_on_time: 'Rechazado el %{time}' declined_on_time: 'Rechazado el %{time}'
expire_on_time: 'Expira el %{time}' expire_on_time: 'Expira el %{time}'
sign_in_person: Firma en persona sign_in_person: Firma en persona
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">enviar el existente</a> create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada.
re_send_email: Reenviar email re_send_email: Reenviar email
re_send_emails: Reenviar emails re_send_emails: Reenviar emails
@@ -2174,6 +2178,8 @@ es: &es
failed_to_save_the_scan: No se pudo guardar el escaneo failed_to_save_the_scan: No se pudo guardar el escaneo
failed_to_scan_the_document: No se pudo escanear el documento failed_to_scan_the_document: No se pudo escanear el documento
document_scanning_is_not_available_on_this_device: El escaneo de documentos no está disponible en este dispositivo document_scanning_is_not_available_on_this_device: El escaneo de documentos no está disponible en este dispositivo
cc_view_only: CC Solo lectura
bcc_completed: CCO Completado
devise: devise:
confirmations: confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente. confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
@@ -3023,6 +3029,7 @@ it: &it
developer_sandbox: Sandbox per sviluppatori developer_sandbox: Sandbox per sviluppatori
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Aggiorna</a> per iniziare a usare in produzione.' upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Aggiorna</a> per iniziare a usare in produzione.'
download_combined_pdf: Scarica PDF combinato download_combined_pdf: Scarica PDF combinato
download_event_log: Scarica registro degli eventi
audit_log: Registro di audit audit_log: Registro di audit
signers: Firmatari signers: Firmatari
not_invited_yet: Non ancora invitato not_invited_yet: Non ancora invitato
@@ -3034,7 +3041,7 @@ it: &it
declined_on_time: 'Rifiutato il %{time}' declined_on_time: 'Rifiutato il %{time}'
expire_on_time: 'Scade il %{time}' expire_on_time: 'Scade il %{time}'
sign_in_person: Firma di persona sign_in_person: Firma di persona
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">invia quello esistente</a> create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato.
re_send_email: Reinvia email re_send_email: Reinvia email
re_send_emails: Reinvia le email re_send_emails: Reinvia le email
@@ -3335,6 +3342,8 @@ it: &it
failed_to_save_the_scan: Impossibile salvare la scansione failed_to_save_the_scan: Impossibile salvare la scansione
failed_to_scan_the_document: Impossibile scansionare il documento failed_to_scan_the_document: Impossibile scansionare il documento
document_scanning_is_not_available_on_this_device: La scansione dei documenti non è disponibile su questo dispositivo document_scanning_is_not_available_on_this_device: La scansione dei documenti non è disponibile su questo dispositivo
cc_view_only: CC Sola lettura
bcc_completed: BCC Completato
devise: devise:
confirmations: confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo. confirmed: Il tuo indirizzo email è stato confermato con successo.
@@ -4170,6 +4179,7 @@ fr: &fr
developer_sandbox: Bac à sable développeur. developer_sandbox: Bac à sable développeur.
upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Mettez à niveau</a> pour commencer à utiliser en production. upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Mettez à niveau</a> pour commencer à utiliser en production.
download_combined_pdf: Télécharger le PDF combiné download_combined_pdf: Télécharger le PDF combiné
download_event_log: Télécharger le journal des événements
audit_log: Journal d’audit audit_log: Journal d’audit
signers: Signataires signers: Signataires
not_invited_yet: Pas encore invité not_invited_yet: Pas encore invité
@@ -4181,7 +4191,7 @@ fr: &fr
declined_on_time: Refusé le %{time} declined_on_time: Refusé le %{time}
expire_on_time: Expire le %{time} expire_on_time: Expire le %{time}
sign_in_person: Signer en personne sign_in_person: Signer en personne
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Créez un nouveau modèle</a> de document ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">soumettez l’existant</a> create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Créez un nouveau modèle</a> de document
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée.
re_send_email: Renvoyer l’e‑mail re_send_email: Renvoyer l’e‑mail
re_send_emails: Renvoyer e‑mails re_send_emails: Renvoyer e‑mails
@@ -4479,6 +4489,8 @@ fr: &fr
failed_to_save_the_scan: Impossible d’enregistrer la numérisation failed_to_save_the_scan: Impossible d’enregistrer la numérisation
failed_to_scan_the_document: Impossible de numériser le document failed_to_scan_the_document: Impossible de numériser le document
document_scanning_is_not_available_on_this_device: La numérisation de documents n’est pas disponible sur cet appareil document_scanning_is_not_available_on_this_device: La numérisation de documents n’est pas disponible sur cet appareil
cc_view_only: Cc Lecture seule
bcc_completed: Cci Terminé
devise: devise:
confirmations: confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès. confirmed: Votre adresse e-mail a été confirmée avec succès.
@@ -5327,6 +5339,7 @@ pt: &pt
developer_sandbox: Sandbox para desenvolvedores developer_sandbox: Sandbox para desenvolvedores
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Atualizar</a> para começar a usar em produção.' upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Atualizar</a> para começar a usar em produção.'
download_combined_pdf: Baixar PDF combinado download_combined_pdf: Baixar PDF combinado
download_event_log: Baixar log de eventos
audit_log: Log de auditoria audit_log: Log de auditoria
signers: Signatários signers: Signatários
not_invited_yet: Ainda não convidado not_invited_yet: Ainda não convidado
@@ -5338,7 +5351,7 @@ pt: &pt
declined_on_time: 'Recusado em %{time}' declined_on_time: 'Recusado em %{time}'
expire_on_time: 'Expira em %{time}' expire_on_time: 'Expira em %{time}'
sign_in_person: Assinar pessoalmente sign_in_person: Assinar pessoalmente
create_a_new_template_document_form_or_submit_the_existing_one_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submeter o existente</a>' create_a_new_template_document_form_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento'
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado.
re_send_email: Reenviar e-mail re_send_email: Reenviar e-mail
re_send_emails: Reenviar e-mails re_send_emails: Reenviar e-mails
@@ -5639,6 +5652,8 @@ pt: &pt
failed_to_save_the_scan: Não foi possível salvar a digitalização failed_to_save_the_scan: Não foi possível salvar a digitalização
failed_to_scan_the_document: Não foi possível digitalizar o documento failed_to_scan_the_document: Não foi possível digitalizar o documento
document_scanning_is_not_available_on_this_device: A digitalização de documentos não está disponível neste dispositivo document_scanning_is_not_available_on_this_device: A digitalização de documentos não está disponível neste dispositivo
cc_view_only: CC Somente leitura
bcc_completed: BCC Concluído
devise: devise:
confirmations: confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso. confirmed: Seu endereço de e-mail foi confirmado com sucesso.
@@ -6487,6 +6502,7 @@ de: &de
developer_sandbox: Entwickler-Sandbox. developer_sandbox: Entwickler-Sandbox.
upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> durchführen, um die Produktionsnutzung zu starten.' upgrade_to_start_using_in_production_html: '<a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> durchführen, um die Produktionsnutzung zu starten.'
download_combined_pdf: Kombiniertes PDF herunterladen download_combined_pdf: Kombiniertes PDF herunterladen
download_event_log: Ereignisprotokoll herunterladen
audit_log: Prüfprotokoll audit_log: Prüfprotokoll
signers: Unterzeichner signers: Unterzeichner
not_invited_yet: Noch nicht eingeladen not_invited_yet: Noch nicht eingeladen
@@ -6498,7 +6514,7 @@ de: &de
declined_on_time: 'Abgelehnt am %{time}' declined_on_time: 'Abgelehnt am %{time}'
expire_on_time: 'Läuft ab am %{time}' expire_on_time: 'Läuft ab am %{time}'
sign_in_person: Vor Ort unterschreiben sign_in_person: Vor Ort unterschreiben
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a> oder <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">bestehende einreichen</a> create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a>
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse.
re_send_email: E-Mail erneut senden re_send_email: E-Mail erneut senden
re_send_emails: E-Mails senden re_send_emails: E-Mails senden
@@ -6799,6 +6815,8 @@ de: &de
failed_to_save_the_scan: Scan konnte nicht gespeichert werden failed_to_save_the_scan: Scan konnte nicht gespeichert werden
failed_to_scan_the_document: Dokument konnte nicht gescannt werden failed_to_scan_the_document: Dokument konnte nicht gescannt werden
document_scanning_is_not_available_on_this_device: Das Scannen von Dokumenten ist auf diesem Gerät nicht verfügbar document_scanning_is_not_available_on_this_device: Das Scannen von Dokumenten ist auf diesem Gerät nicht verfügbar
cc_view_only: CC Nur Ansicht
bcc_completed: BCC Abgeschlossen
devise: devise:
confirmations: confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt. confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
@@ -6981,14 +6999,14 @@ pl:
decline_reason: Powód odrzucenia decline_reason: Powód odrzucenia
provide_a_reason: Podaj powód provide_a_reason: Podaj powód
notify_the_sender_with_the_reason_you_declined: Powiadom nadawcę o powodzie, dla którego odrzuciłeś notify_the_sender_with_the_reason_you_declined: Powiadom nadawcę o powodzie, dla którego odrzuciłeś
form_has_been_declined_on_html: 'Formularz został odrzucony o <span class="font-semibold">%{time}</span>' form_has_been_declined_on_html: 'Formularz został odrzucony <span class="font-semibold">%{time}</span>'
document_has_been_delegated_on_html: 'Dokument został oddelegowany o <span class="font-semibold">%{time}</span>' document_has_been_delegated_on_html: 'Dokument został oddelegowany <span class="font-semibold">%{time}</span>'
name_declined_by_submitter: '"%{name}" odrzucono przez %{submitter}' name_declined_by_submitter: '"%{name}" odrzucono przez %{submitter}'
name_declined_by_submitter_with_the_following_reason: '"%{name}" został odrzucony przez %{submitter} z następującym powodem:' name_declined_by_submitter_with_the_following_reason: '"%{name}" został odrzucony przez %{submitter} z następującym powodem:'
email: E-mail email: E-mail
verification_code_code: 'Kod weryfikacyjny: %{code}' verification_code_code: 'Kod weryfikacyjny: %{code}'
digitally_signed_by: Podpis cyfrowy przez digitally_signed_by: Podpis cyfrowy przez
form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formularz wygasł <span class="font-semibold">%{time}</span>'
role: Rola role: Rola
provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć
provide_your_email: Podaj swój e-mail provide_your_email: Podaj swój e-mail
@@ -7123,8 +7141,8 @@ uk:
decline_reason: Причина відхилення decline_reason: Причина відхилення
provide_a_reason: Надайте причину provide_a_reason: Надайте причину
notify_the_sender_with_the_reason_you_declined: Повідомте відправника про причину свого відхилення notify_the_sender_with_the_reason_you_declined: Повідомте відправника про причину свого відхилення
form_has_been_declined_on_html: 'Форму було відхилено о <span class="font-semibold">%{time}</span>' form_has_been_declined_on_html: 'Форму було відхилено <span class="font-semibold">%{time}</span>'
document_has_been_delegated_on_html: 'Документ було делеговано о <span class="font-semibold">%{time}</span>' document_has_been_delegated_on_html: 'Документ було делеговано <span class="font-semibold">%{time}</span>'
name_declined_by_submitter: '"%{name}" відхилено %{submitter}' name_declined_by_submitter: '"%{name}" відхилено %{submitter}'
name_declined_by_submitter_with_the_following_reason: '"%{name}" було відхилено %{submitter} з такою причиною:' name_declined_by_submitter_with_the_following_reason: '"%{name}" було відхилено %{submitter} з такою причиною:'
email: E-mail email: E-mail
@@ -8175,6 +8193,7 @@ nl: &nl
developer_sandbox: Ontwikkelaars-sandbox. developer_sandbox: Ontwikkelaars-sandbox.
upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> om in productie te gaan gebruiken. upgrade_to_start_using_in_production_html: <a href="%{url}" target="_blank" class="link font-semibold">Upgrade</a> om in productie te gaan gebruiken.
download_combined_pdf: Gecombineerde PDF downloaden download_combined_pdf: Gecombineerde PDF downloaden
download_event_log: Gebeurtenislogboek downloaden
audit_log: Auditlogboek audit_log: Auditlogboek
signers: Ondertekenaars signers: Ondertekenaars
not_invited_yet: Nog niet uitgenodigd not_invited_yet: Nog niet uitgenodigd
@@ -8186,7 +8205,7 @@ nl: &nl
declined_on_time: Geweigerd op %{time} declined_on_time: Geweigerd op %{time}
expire_on_time: Verloopt op %{time} expire_on_time: Verloopt op %{time}
sign_in_person: In persoon ondertekenen sign_in_person: In persoon ondertekenen
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Maak een nieuw sjabloon</a> voor een documentformulier of <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">dien het bestaande in</a> create_a_new_template_document_form_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Maak een nieuw sjabloon</a> voor een documentformulier
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres. send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres.
re_send_email: E-mail opnieuw verzenden re_send_email: E-mail opnieuw verzenden
re_send_emails: E-mails verzenden re_send_emails: E-mails verzenden
@@ -8483,6 +8502,8 @@ nl: &nl
failed_to_save_the_scan: Scan kon niet worden opgeslagen failed_to_save_the_scan: Scan kon niet worden opgeslagen
failed_to_scan_the_document: Document kon niet worden gescand failed_to_scan_the_document: Document kon niet worden gescand
document_scanning_is_not_available_on_this_device: Documenten scannen is niet beschikbaar op dit apparaat document_scanning_is_not_available_on_this_device: Documenten scannen is niet beschikbaar op dit apparaat
cc_view_only: CC Alleen bekijken
bcc_completed: BCC Voltooid
devise: devise:
confirmations: confirmations:
confirmed: Uw e-mailadres is succesvol bevestigd. confirmed: Uw e-mailadres is succesvol bevestigd.
+1 -9
View File
@@ -25,8 +25,6 @@ Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do namespace :api, defaults: { format: :json } do
resource :user, only: %i[show] resource :user, only: %i[show]
resources :attachments, only: %i[create]
resources :submitter_email_clicks, only: %i[create]
resources :submitter_form_views, only: %i[create] resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update] resources :submitters, only: %i[index show update]
resources :submissions, only: %i[index show create update destroy] do resources :submissions, only: %i[index show create update destroy] do
@@ -128,13 +126,7 @@ Rails.application.routes.draw do
controller: 'api/active_storage_blobs_proxy' controller: 'api/active_storage_blobs_proxy'
if Docuseal.multitenant? if Docuseal.multitenant?
resource :blobs_proxy_legacy, only: %i[show], get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service if Rails.env.local?
path: '/blobs/proxy/:signed_id/*filename',
controller: 'api/active_storage_blobs_proxy_legacy',
as: :rails_blob
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
ActiveSupport.run_load_hooks(:multitenant_routes, self) ActiveSupport.run_load_hooks(:multitenant_routes, self)
end end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddTemplateIdToSubmitters < ActiveRecord::Migration[8.1]
def change
add_column :submitters, :template_id, :bigint, if_not_exists: true
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class PopulateSubmitterTemplateId < ActiveRecord::Migration[8.1]
disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
end
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def up
return if ENV['MULTITENANT'] == 'true'
max_id = MigrationSubmitter.maximum(:id)
return unless max_id
submission_template_id =
Arel::Nodes::Grouping.new(
MigrationSubmission.arel_table.project(MigrationSubmission.arel_table[:template_id])
.where(MigrationSubmission.arel_table[:id].eq(MigrationSubmitter.arel_table[:submission_id]))
.ast
)
(1..max_id).step(10_000) do |start_id|
MigrationSubmitter.where(template_id: nil, id: start_id...(start_id + 10_000))
.update_all(template_id: submission_template_id)
end
end
def down
nil
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddSubmittersTemplateIdIndexes < ActiveRecord::Migration[8.1]
def up
add_index :submitters, %i[email template_id], where: 'email IS NOT NULL', if_not_exists: true
add_index :submitters, %i[template_id id], if_not_exists: true
remove_index :submitters, :email, if_exists: true
end
def down
add_index :submitters, :email, if_not_exists: true
remove_index :submitters, %i[template_id id], if_exists: true
remove_index :submitters, %i[email template_id], if_exists: true
end
end
+4 -2
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do ActiveRecord::Schema[8.1].define(version: 2026_10_25_120002) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "btree_gin" enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@@ -424,6 +424,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
t.datetime "sent_at" t.datetime "sent_at"
t.string "slug", null: false t.string "slug", null: false
t.bigint "submission_id", null: false t.bigint "submission_id", null: false
t.bigint "template_id"
t.string "timezone" t.string "timezone"
t.string "ua" t.string "ua"
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@@ -431,10 +432,11 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
t.text "values", null: false t.text "values", null: false
t.index ["account_id", "completed_at"], name: "index_submitters_on_account_id_and_completed_at", where: "(completed_at IS NOT NULL)" t.index ["account_id", "completed_at"], name: "index_submitters_on_account_id_and_completed_at", where: "(completed_at IS NOT NULL)"
t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id" t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id"
t.index ["email"], name: "index_submitters_on_email" t.index ["email", "template_id"], name: "index_submitters_on_email_and_template_id", where: "(email IS NOT NULL)"
t.index ["external_id"], name: "index_submitters_on_external_id" t.index ["external_id"], name: "index_submitters_on_external_id"
t.index ["slug"], name: "index_submitters_on_slug", unique: true t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["submission_id"], name: "index_submitters_on_submission_id" t.index ["submission_id"], name: "index_submitters_on_submission_id"
t.index ["template_id", "id"], name: "index_submitters_on_template_id_and_id"
end end
create_table "template_accesses", force: :cascade do |t| create_table "template_accesses", force: :cascade do |t|
-28
View File
@@ -5,34 +5,6 @@ module Accounts
module_function module_function
def create_duplicate(account)
new_account = account.dup
new_user = account.users.first.dup
new_user.uuid = SecureRandom.uuid
new_user.account = new_account
new_user.encrypted_password = SecureRandom.hex
new_user.email = "#{SecureRandom.hex}@docuseal.com"
account.templates.each do |template|
new_template = template.dup
new_template.account = new_account
new_template.slug = SecureRandom.base58(14)
new_template.archived_at = nil
new_template.save!
Templates::CloneAttachments.call(template: new_template, original_template: template)
end
new_user.save!(validate: false)
new_account.templates.update_all(folder_id: new_account.default_template_folder.id)
new_account
end
def users_count(account) def users_count(account)
rel = User.where(account_id: account.id).or( rel = User.where(account_id: account.id).or(
User.where(account_id: account.account_linked_accounts User.where(account_id: account.account_linked_accounts
+1 -4
View File
@@ -7,10 +7,7 @@ class ApiPathConsiderJsonMiddleware
def call(env) def call(env)
if env['PATH_INFO'].starts_with?('/api') && if env['PATH_INFO'].starts_with?('/api') &&
(!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') && !env['PATH_INFO'].ends_with?('/submitter_sms_clicks')
!env['PATH_INFO'].ends_with?('/attachments') &&
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks') &&
!env['PATH_INFO'].ends_with?('/submitter_email_clicks')
env['CONTENT_TYPE'] = 'application/json' env['CONTENT_TYPE'] = 'application/json'
end end
+13 -3
View File
@@ -26,6 +26,8 @@ module ReplaceEmailVariables
DOCUMENTS_LINKS = /\{+documents\.links\}+/i DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i
ESCAPED_BRACES = "\u{E000}\u{E001}"
EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil) EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil)
module_function module_function
@@ -86,7 +88,11 @@ module ReplaceEmailVariables
build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip) build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip)
end end
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') } text = replace(text, SENDER_EMAIL, html_escape:) do
submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@')
end
text.tr(ESCAPED_BRACES, '{}')
end end
# rubocop:enable Metrics # rubocop:enable Metrics
@@ -124,6 +130,8 @@ module ReplaceEmailVariables
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
end end
elsif (mask = field.dig('preferences', 'mask').presence)
TextUtils.mask_value(value[field['uuid']].to_s, mask)
else else
value[field&.dig('uuid')] value[field&.dig('uuid')]
end end
@@ -134,10 +142,12 @@ module ReplaceEmailVariables
def replace(text, var, html_escape: false) def replace(text, var, html_escape: false)
text.gsub(var) do text.gsub(var) do
value = yield(Regexp.last_match).to_s.tr('{}', ESCAPED_BRACES)
if html_escape if html_escape
ERB::Util.html_escape(yield(Regexp.last_match)) ERB::Util.html_escape(value)
else else
yield(Regexp.last_match) value
end end
end end
end end
+20 -7
View File
@@ -3,6 +3,8 @@
module SearchEntries module SearchEntries
MAX_VALUE_LENGTH = 100 MAX_VALUE_LENGTH = 100
MUTEX = Mutex.new
UUID_REGEXP = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i UUID_REGEXP = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
module_function module_function
@@ -70,10 +72,10 @@ module SearchEntries
arel arel
else else
"tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*')::tsquery" "tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*')::tsquery"
end end
[sql, { keyword: }] [sql, { keyword:, dictionary: default_dictionary }]
end end
end end
@@ -83,7 +85,7 @@ module SearchEntries
Arel.sql("ngram @@ (quote_literal(:term#{terms.size - 1}) || ':' || :weight)::tsquery") Arel.sql("ngram @@ (quote_literal(:term#{terms.size - 1}) || ':' || :weight)::tsquery")
else else
Arel.sql(<<~SQL.squish) Arel.sql(<<~SQL.squish)
(quote_literal(coalesce((ts_lexize('english_stem', :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery (quote_literal(coalesce((ts_lexize(:dictionary, :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery
SQL SQL
end end
@@ -91,7 +93,7 @@ module SearchEntries
index = terms.index(term) index = terms.index(term)
arel = Arel.sql(<<~SQL.squish) arel = Arel.sql(<<~SQL.squish)
(quote_literal(coalesce((ts_lexize('english_stem', :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery (quote_literal(coalesce((ts_lexize(:dictionary, :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery
SQL SQL
acc ? Arel::Nodes::InfixOperation.new('&&', arel, acc) : arel acc ? Arel::Nodes::InfixOperation.new('&&', arel, acc) : arel
@@ -109,7 +111,8 @@ module SearchEntries
) )
end end
[query.to_sql, terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:)] [query.to_sql,
terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:, dictionary: default_dictionary)]
end end
def build_weights_wildcard_tsquery(keyword, weight) def build_weights_wildcard_tsquery(keyword, weight)
@@ -122,11 +125,11 @@ module SearchEntries
SQL SQL
else else
<<~SQL.squish <<~SQL.squish
tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*' || :weight)::tsquery tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*' || :weight)::tsquery
SQL SQL
end end
[sql, { keyword:, weight: }] [sql, { keyword:, weight:, dictionary: default_dictionary }]
end end
def index_submitter(submitter) def index_submitter(submitter)
@@ -256,4 +259,14 @@ module SearchEntries
ngrams.uniq { |e| e.sub(/':[\d,]/, "':1") }.join(' ') ngrams.uniq { |e| e.sub(/':[\d,]/, "':1") }.join(' ')
end end
def default_dictionary
@default_dictionary || MUTEX.synchronize do
@default_dictionary ||= SearchEntry.connection.select_value(<<~SQL.squish)
SELECT mapdict::regdictionary::text FROM pg_ts_config_map
WHERE mapcfg = get_current_ts_config() AND maptokentype = 1
ORDER BY mapseqno LIMIT 1
SQL
end
end
end end
+23
View File
@@ -90,6 +90,10 @@ module Submissions
maybe_add_invite_submitters(submission, template, attrs[:submitters]) maybe_add_invite_submitters(submission, template, attrs[:submitters])
maybe_add_cc_submitters(submission, params[:cc_recipients],
submitters_order:, user:, params:,
preferences: preferences.merge(submission_preferences))
assign_submitters_is_viewer(submission) assign_submitters_is_viewer(submission)
submission.template = nil unless with_template submission.template = nil unless with_template
@@ -211,6 +215,25 @@ module Submissions
end end
end end
def maybe_add_cc_submitters(submission, recipients, submitters_order:, user:, params:, preferences:)
order = submission.template_submitters.filter_map { |s| s['order'] }.max
Array.wrap(recipients).each do |recipient|
attrs = recipient.is_a?(String) ? { email: recipient } : recipient.slice(:name, :email, :phone)
next if attrs.compact_blank.blank?
uuid = SecureRandom.uuid
submission.template_submitters <<
{ 'name' => 'CC', 'uuid' => uuid, 'is_viewer' => true, 'order' => order }.compact
build_submitter(submission:, attrs:, uuid:, user:, params:,
is_order_sent: submitters_order == 'random',
preferences: preferences.except('email_message_uuid'))
end
end
def submitter_message_preferences(uuid, params) def submitter_message_preferences(uuid, params)
return {} if params[:request_email_per_submitter] != '1' return {} if params[:request_email_per_submitter] != '1'
return {} if params[:is_custom_message] != '1' return {} if params[:is_custom_message] != '1'
+6 -3
View File
@@ -267,7 +267,10 @@ module Submissions
is_id_verified = submission_events.any?(&:complete_verification?) is_id_verified = submission_events.any?(&:complete_verification?)
is_kba_passed = submission_events.any?(&:complete_kba?) complete_kba_event = submission_events.find(&:complete_kba?) || SubmissionEvent.new
kba_answers = complete_kba_event.data['answers']
kba_score = " (#{kba_answers['correct']}/#{kba_answers['asked']})" if kba_answers
info_rows = [ info_rows = [
[ [
@@ -297,8 +300,8 @@ module Submissions
is_id_verified && { is_id_verified && {
text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n" text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n"
}, },
is_kba_passed && { complete_kba_event.persisted? && {
text: "#{I18n.t('knowledge_based_authentication')}: #{I18n.t('passed')}\n" text: "#{I18n.t('knowledge_based_authentication')}: #{I18n.t('passed')}#{kba_score}\n"
}, },
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" }, completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" }, completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
@@ -602,6 +602,8 @@ module Submissions
cell_valign = field.dig('preferences', 'valign').to_s.presence_in(TEXT_VALIGNS) || 'center' cell_valign = field.dig('preferences', 'valign').to_s.presence_in(TEXT_VALIGNS) || 'center'
cell_layouter = cell_layouters[cell_valign] cell_layouter = cell_layouters[cell_valign]
value = value.to_s
if (mask = field.dig('preferences', 'mask').presence) if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask) value = TextUtils.mask_value(value, mask)
end end
@@ -882,6 +884,8 @@ module Submissions
end end
def maybe_flatten_pdf(pdf) def maybe_flatten_pdf(pdf)
pdf.catalog.delete(:AcroForm) if pdf.catalog[:AcroForm].is_a?(Integer)
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances] pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
pdf.acro_form&.flatten pdf.acro_form&.flatten
rescue HexaPDF::MissingGlyphError rescue HexaPDF::MissingGlyphError
+9 -1
View File
@@ -152,7 +152,7 @@ module Submitters
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa') preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa')
preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa') preferences['require_email_2fa'] = params['require_email_2fa'].in?(TRUE_VALUES) if params.key?('require_email_2fa')
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed') preferences['bcc_completed'] = normalize_bcc_completed(params['bcc_completed']) if params.key?('bcc_completed')
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to') preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last') preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url') preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
@@ -160,6 +160,14 @@ module Submitters
preferences preferences
end end
def normalize_bcc_completed(value)
if value.is_a?(Array)
value.compact_blank.uniq.join(', ')
else
value
end
end
def fetch_values_for_delegate(submitter) def fetch_values_for_delegate(submitter)
fields = submitter.submission.template_fields || submitter.template.fields fields = submitter.submission.template_fields || submitter.template.fields
default_values = submitter.preferences['default_values'] || {} default_values = submitter.preferences['default_values'] || {}
+1
View File
@@ -65,6 +65,7 @@ module Submitters
submissions = submissions.where(source:) if source submissions = submissions.where(source:) if source
Submitter Submitter
.where(template_id: template.id)
.where(submission: submissions) .where(submission: submissions)
.order(id: :desc) .order(id: :desc)
.where(declined_at: nil) .where(declined_at: nil)
+31 -1
View File
@@ -9,6 +9,8 @@ module Submitters
PHONE_REGEXP = /[+\d()\s-]+/ PHONE_REGEXP = /[+\d()\s-]+/
NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze
REQUIRED_FIELD_TYPES = %w[payment kba verification].freeze REQUIRED_FIELD_TYPES = %w[payment kba verification].freeze
CHOICE_FIELD_TYPES = %w[radio multiple select].freeze
BOOLEAN_VALUES = [true, false].freeze
STRFTIME_MAP = { STRFTIME_MAP = {
'hour' => '%-k', 'hour' => '%-k',
@@ -55,6 +57,7 @@ module Submitters
validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request) validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request)
if params[:completed] == 'true' if params[:completed] == 'true'
validate_invite_submitters!(submitter)
maybe_invite_via_field(submitter, request) maybe_invite_via_field(submitter, request)
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
@@ -504,7 +507,7 @@ module Submitters
submitter submitter
end end
def validate_value!(_value, field, _params, submitter, _request) def validate_value!(value, field, _params, submitter, _request)
raise ValidationError, 'Missing field' unless field raise ValidationError, 'Missing field' unless field
raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid
@@ -514,7 +517,34 @@ module Submitters
raise ValidationError, 'Read-only field' raise ValidationError, 'Read-only field'
end end
raise ValidationError, 'Value is invalid' if field['type'] == 'checkbox' && !value.in?(BOOLEAN_VALUES)
validate_choice_value!(value, field, submitter) if field['type'].in?(CHOICE_FIELD_TYPES)
true true
end end
def validate_choice_value!(value, field, submitter)
return if value.blank?
raise ValidationError, 'Value is invalid' unless value.is_a?(field['type'] == 'multiple' ? Array : String)
option_names = field['options'].to_a.map.with_index do |option, index|
option['value'].presence ||
"#{I18n.t('option', locale: submitter.metadata.fetch('lang', submitter.account.locale))} #{index + 1}"
end
raise ValidationError, 'Value is invalid' unless Array.wrap(value).all? { |item| option_names.include?(item) }
end
def validate_invite_submitters!(submitter)
submission = submitter.submission
is_pending_invite = (submission.template_submitters || submission.template.submitters).any? do |s|
s['invite_by_uuid'] == submitter.uuid && submission.submitters.none? { |e| e.uuid == s['uuid'] }
end
raise ValidationError, 'Invite required' if is_pending_invite
end
end end
end end
+21
View File
@@ -6,6 +6,7 @@ module Templates
ZIP_CONTENT_TYPE = 'application/zip' ZIP_CONTENT_TYPE = 'application/zip'
X_ZIP_CONTENT_TYPE = 'application/x-zip-compressed' X_ZIP_CONTENT_TYPE = 'application/x-zip-compressed'
JSON_CONTENT_TYPE = 'application/json' JSON_CONTENT_TYPE = 'application/json'
WORD_EXTENSIONS = %w[.docx .doc .odt .rtf].freeze
DOCUMENT_EXTENSIONS = %w[.docx .doc .xlsx .xls .odt .rtf].freeze DOCUMENT_EXTENSIONS = %w[.docx .doc .xlsx .xls .odt .rtf].freeze
DOCUMENT_CONTENT_TYPES = %w[ DOCUMENT_CONTENT_TYPES = %w[
@@ -134,7 +135,27 @@ module Templates
return [handle_pdf_or_image(template, file, file.read, params, extract_fields:), []] return [handle_pdf_or_image(template, file, file.read, params, extract_fields:), []]
end end
if File.extname(file.original_filename.to_s).downcase.in?(WORD_EXTENSIONS)
return [handle_word_document(template, file, params), []]
end
raise InvalidFileType, "#{file.content_type}/#{dynamic}" raise InvalidFileType, "#{file.content_type}/#{dynamic}"
end end
def handle_word_document(template, file, params)
pdf = WordCraft.open_buffer(file, &:to_pdf)
pdf_file = ActionDispatch::Http::UploadedFile.new(
tempfile: StringIO.new(pdf),
filename: "#{File.basename(file.original_filename, '.*')}.pdf",
type: PDF_CONTENT_TYPE
)
handle_pdf_or_image(template, pdf_file, pdf, params)
end
def document_extensions
Docuseal.advanced_formats? ? DOCUMENT_EXTENSIONS : WORD_EXTENSIONS
end
end end
end end
+7 -2
View File
@@ -90,8 +90,10 @@ module Templates
pool = Concurrent::FixedThreadPool.new(concurrency) pool = Concurrent::FixedThreadPool.new(concurrency)
blobs =
range.each_slice(concurrency).flat_map do |page_numbers|
promises = promises =
range.map do |page_number| page_numbers.map do |page_number|
doc_page = doc.get_page(page_number) doc_page = doc.get_page(page_number)
hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages) hide_placeholder_widgets(doc_page, hide_empty: !flatten_pages)
@@ -106,7 +108,10 @@ module Templates
doc_page&.close doc_page&.close
end end
Concurrent::Promise.zip(*promises).value!.each do |blob| Concurrent::Promise.zip(*promises).value!
end
blobs.each do |blob|
next unless blob next unless blob
ApplicationRecord.no_touching do ApplicationRecord.no_touching do
+4 -1
View File
@@ -180,8 +180,11 @@ module VerifyPdfSignature
return false if byte_range.size != 4 || byte_range.any?(&:negative?) || byte_range[0].positive? return false if byte_range.size != 4 || byte_range.any?(&:negative?) || byte_range[0].positive?
return false if signature.signed_end > io.size return false if signature.signed_end > io.size
return false if byte_range[2] != byte_range[1] + (signature.contents.bytesize * 2) + 2
byte_range[2] == byte_range[1] + (signature.contents.bytesize * 2) + 2 io.seek(byte_range[1])
io.read(byte_range[2] - byte_range[1]).casecmp?("<#{signature.contents.unpack1('H*')}>")
end end
def unsigned_changes?(document, io, signed_end) def unsigned_changes?(document, io, signed_end)