mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a1835c00 | ||
|
|
c35407d7a9 | ||
|
|
e55ccc0b76 | ||
|
|
b8d00870b4 | ||
|
|
5413fbc248 | ||
|
|
2d2eb7ce4d | ||
|
|
ed2ddbf74e | ||
|
|
b99e5d1262 | ||
|
|
87f6878eda | ||
|
|
99b815eb4b | ||
|
|
e40a29b9ab | ||
|
|
151890bb72 | ||
|
|
78edd87cc1 | ||
|
|
88c99a2b35 | ||
|
|
282b8cfa73 | ||
|
|
e9888c7466 | ||
|
|
0647befb3a | ||
|
|
85aa3d0451 | ||
|
|
6d2b3bb654 | ||
|
|
b394854e9d | ||
|
|
2e586e930e | ||
|
|
8f8f69f0bc | ||
|
|
04663d55eb | ||
|
|
93d0b935e2 | ||
|
|
829b2b2855 | ||
|
|
cf6de444f4 | ||
|
|
126f5d58a2 | ||
|
|
09b93067f4 | ||
|
|
f14458f238 | ||
|
|
445139f05c | ||
|
|
e5e4965f05 | ||
|
|
1c8f7b0316 | ||
|
|
9fe3656652 | ||
|
|
17ae46bd61 | ||
|
|
a036483938 |
No files matched your search
@@ -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
@@ -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,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
|
||||||
|
|||||||
+12
-1
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -228,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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ export default class extends HTMLElement {
|
|||||||
toggle = (event) => {
|
toggle = (event) => {
|
||||||
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
|
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
|
||||||
|
|
||||||
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field)
|
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')) ||
|
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
|
||||||
(this.isChecked('send_sms') && !isNameOnly('phone'))
|
(this.isChecked('send_sms') && !isNameOnly('phone'))
|
||||||
@@ -29,12 +36,17 @@ export default class extends HTMLElement {
|
|||||||
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
|
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) {
|
fieldSelector (field) {
|
||||||
return `[name$="[][${field}]"]`
|
return `[name$="[][${field}]"]`
|
||||||
}
|
}
|
||||||
|
|
||||||
get events () {
|
get events () {
|
||||||
return ['change', 'input', 'linked-input.update']
|
return ['change', 'input', 'linked-input.update', 'focusin', 'focusout']
|
||||||
}
|
}
|
||||||
|
|
||||||
get form () {
|
get form () {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -415,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"
|
||||||
@@ -640,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"
|
||||||
@@ -1379,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,
|
||||||
@@ -1662,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()
|
||||||
@@ -1685,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 = []
|
||||||
@@ -2241,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()
|
||||||
},
|
},
|
||||||
@@ -4036,6 +4079,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.autosave && !force) {
|
if (!this.autosave && !force) {
|
||||||
|
this.hasUnsavedChanges = true
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4058,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 } %>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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') %>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -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,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,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,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>
|
||||||
@@ -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? %>
|
||||||
@@ -227,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? %>
|
||||||
@@ -394,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 %>
|
||||||
@@ -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">
|
||||||
@@ -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 %>
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
@@ -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>
|
||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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'
|
||||||
+1
-9
@@ -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
@@ -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|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user