mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6eae66127 | ||
|
|
08aa902e07 | ||
|
|
1690337699 | ||
|
|
f78cf8cc6e | ||
|
|
343118d00d | ||
|
|
a3bd37c9a2 | ||
|
|
8bc13a5900 | ||
|
|
21138c1d02 | ||
|
|
38674a9fc9 | ||
|
|
a7ed0b86a4 | ||
|
|
91a96f06c8 | ||
|
|
23631b75f2 | ||
|
|
0fb9c2bbf6 | ||
|
|
14fa8fc8e7 | ||
|
|
047c192988 | ||
|
|
bfa244c8fa | ||
|
|
019222ae40 | ||
|
|
8a10852fe2 | ||
|
|
01232aed5a | ||
|
|
82f3ff4824 | ||
|
|
7be3f5a931 | ||
|
|
9d013a7384 | ||
|
|
0c71d7bff8 | ||
|
|
fdd030652e | ||
|
|
f2bf1461f0 | ||
|
|
fc6f796f30 | ||
|
|
5f44806287 | ||
|
|
318b65af18 | ||
|
|
91b182b04a | ||
|
|
15b886d4ff | ||
|
|
0122a237e3 | ||
|
|
2dca7d8a8e | ||
|
|
87265e2f22 | ||
|
|
3166defa1a | ||
|
|
93ba17f42e | ||
|
|
751be1dae5 | ||
|
|
3f8d2831e2 |
No files matched your search
@@ -157,7 +157,10 @@ jobs:
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
sudo apt-get install -y libvips
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
||||
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
|
||||
rm -f pdfium-linux.tgz
|
||||
- name: Run
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
|
||||
+16
-5
@@ -1,8 +1,17 @@
|
||||
FROM ruby:3.4.2-alpine AS fonts
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||
RUN apk --no-cache add fontforge wget && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||
|
||||
@@ -41,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -70,8 +79,10 @@ COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY .version ./public/version
|
||||
|
||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountConfigsController < ApplicationController
|
||||
before_action :load_account_config
|
||||
authorize_resource :account_config
|
||||
before_action :load_account_config, only: :create
|
||||
authorize_resource :account_config, only: :create
|
||||
|
||||
load_and_authorize_resource :account_config, only: :destroy
|
||||
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
@@ -30,6 +32,14 @@ class AccountConfigsController < ApplicationController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
|
||||
@account_config.destroy!
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
|
||||
@@ -162,7 +162,7 @@ module Api
|
||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||
|
||||
if params[:template_id].present?
|
||||
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||
end
|
||||
|
||||
maybe_filder_by_completed_at(submitters, params)
|
||||
|
||||
@@ -10,16 +10,17 @@ class SendSubmissionEmailController < ApplicationController
|
||||
SEND_DURATION = 30.minutes
|
||||
|
||||
def create
|
||||
@submitter =
|
||||
if params[:template_slug]
|
||||
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
elsif params[:submission_slug]
|
||||
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
else
|
||||
Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
if params[:template_slug]
|
||||
@submitter = Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
elsif params[:submission_slug]
|
||||
@submitter = Submitter.joins(:submission).find_by(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
|
||||
@@ -43,9 +43,11 @@ class StartFormController < ApplicationController
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
enqueue_submission_create_webhooks(@submitter)
|
||||
|
||||
if @submitter.submission.expire_at?
|
||||
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
||||
'submission_id' => @submitter.submission_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -64,6 +66,13 @@ class StartFormController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def enqueue_submission_create_webhooks(submitter)
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsPreviewController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
|
||||
@users.active.where.not(role: 'integration')
|
||||
end
|
||||
|
||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
@@ -165,11 +165,27 @@ export default targetable(class extends HTMLElement {
|
||||
onDragover (e) {
|
||||
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
||||
this.style.backgroundColor = '#F7F3F0'
|
||||
|
||||
if (this.classList.contains('before:border-base-300')) {
|
||||
this.classList.remove('before:border-base-300')
|
||||
this.classList.add('before:border-base-content/30')
|
||||
} else if (this.classList.contains('border-base-300')) {
|
||||
this.classList.remove('border-base-300')
|
||||
this.classList.add('border-base-content/30')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDragleave () {
|
||||
this.style.backgroundColor = null
|
||||
|
||||
if (this.classList.contains('before:border-base-content/30')) {
|
||||
this.classList.remove('before:border-base-content/30')
|
||||
this.classList.add('before:border-base-300')
|
||||
} else if (this.classList.contains('border-base-content/30')) {
|
||||
this.classList.remove('border-base-content/30')
|
||||
this.classList.add('border-base-300')
|
||||
}
|
||||
}
|
||||
|
||||
onWindowDragleave = (e) => {
|
||||
|
||||
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'loading',
|
||||
'icon',
|
||||
'input'
|
||||
'input',
|
||||
'area'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
this.addEventListener('drop', this.onDrop)
|
||||
|
||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||
|
||||
this.addEventListener('drop', this.onDrop)
|
||||
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
||||
this.area?.addEventListener('dragover', this.onDragover)
|
||||
this.area?.addEventListener('dragleave', this.onDragleave)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.removeEventListener('drop', this.onDrop)
|
||||
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
||||
this.area?.removeEventListener('dragover', this.onDragover)
|
||||
this.area?.removeEventListener('dragleave', this.onDragleave)
|
||||
}
|
||||
|
||||
onDragover (e) {
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
this.style.backgroundColor = '#F7F3F0'
|
||||
this.classList.remove('border-base-300', 'hover:bg-base-200/30')
|
||||
this.classList.add('border-base-content/30')
|
||||
}
|
||||
}
|
||||
|
||||
onDragleave () {
|
||||
this.style.backgroundColor = null
|
||||
this.classList.remove('border-base-content/30')
|
||||
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
||||
}
|
||||
|
||||
onDrop (e) {
|
||||
|
||||
@@ -55,10 +55,12 @@
|
||||
>
|
||||
<div
|
||||
v-else-if="field.type === 'signature' && signature"
|
||||
class="flex flex-col justify-between h-full overflow-hidden"
|
||||
class="flex justify-between h-full gap-1 overflow-hidden"
|
||||
:class="isNarrow ? 'flex-row' : 'flex-col'"
|
||||
>
|
||||
<div
|
||||
class="flex-grow flex overflow-hidden"
|
||||
class="flex overflow-hidden"
|
||||
:class="isNarrow ? 'w-1/2' : 'flex-grow'"
|
||||
style="min-height: 50%"
|
||||
>
|
||||
<img
|
||||
@@ -68,7 +70,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="withSignatureId"
|
||||
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||
>
|
||||
<div class="truncate uppercase">
|
||||
ID: {{ signature.uuid }}
|
||||
@@ -444,6 +447,9 @@ export default {
|
||||
}
|
||||
|
||||
return style
|
||||
},
|
||||
isNarrow () {
|
||||
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div
|
||||
v-show="isFormVisible"
|
||||
id="form_container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
||||
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
>
|
||||
@@ -1133,7 +1133,7 @@ export default {
|
||||
parent.style.overflow = 'hidden'
|
||||
|
||||
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||
scrollbox.parentNode.classList.add('h-screen', 'h-[100dvh]', 'overflow-y-auto')
|
||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-replace-and-clone="withReplaceAndCloneUpload"
|
||||
hover-class="bg-base-200/50"
|
||||
@add="[updateFromUpload($event), isDragFile = false]"
|
||||
@replace="[onDocumentsReplace($event), isDragFile = false]"
|
||||
@replace-and-clone="onDocumentsReplaceAndTemplateClone($event)"
|
||||
@@ -1280,7 +1279,7 @@ export default {
|
||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||
}
|
||||
},
|
||||
onDraw (area) {
|
||||
onDraw ({ area, isTooSmall }) {
|
||||
if (this.drawField) {
|
||||
if (this.drawOption) {
|
||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||
@@ -1375,7 +1374,7 @@ export default {
|
||||
area.y -= area.h / 2
|
||||
}
|
||||
|
||||
if (area.w) {
|
||||
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
|
||||
this.addField(type, area)
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||
@remove-area="$emit('remove-area', $event)"
|
||||
@scroll-to="scrollToArea"
|
||||
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
id="document_dropzone"
|
||||
class="w-full relative rounded-md border-2 border-base-content/10 border-dashed"
|
||||
:for="inputId"
|
||||
:class="[{ 'opacity-50': isLoading, 'hover:bg-base-200/30': !hoverClass }, isDragEntering && hoverClass ? hoverClass : '']"
|
||||
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
|
||||
>
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="flex flex-col items-center">
|
||||
@@ -79,16 +79,16 @@ export default {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
withHoverClass: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'IconCloudUpload'
|
||||
},
|
||||
hoverClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
cloneTemplateOnUpload: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
||||
<Dropzone
|
||||
class="flex-1 h-full"
|
||||
hover-class="bg-base-200/50"
|
||||
icon="IconFilePlus"
|
||||
:template-id="templateId"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-hover-class="false"
|
||||
:with-description="false"
|
||||
:title="t('upload_a_new_document')"
|
||||
:title="t('add_a_new_document')"
|
||||
type="add_files"
|
||||
@loading="isLoading = $event"
|
||||
@success="$emit('add', $event)"
|
||||
@@ -20,10 +20,10 @@
|
||||
<div class="flex-1 flex gap-2 w-full">
|
||||
<Dropzone
|
||||
class="flex-1 h-full"
|
||||
hover-class="bg-base-200/50"
|
||||
icon="IconFileSymlink"
|
||||
:template-id="templateId"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-hover-class="false"
|
||||
:with-description="false"
|
||||
:title="t('replace_existing_document')"
|
||||
@loading="isLoading = $event"
|
||||
@@ -33,10 +33,10 @@
|
||||
<Dropzone
|
||||
v-if="withReplaceAndClone"
|
||||
class="flex-1 h-full"
|
||||
hover-class="bg-base-200/50"
|
||||
icon="IconFiles"
|
||||
:template-id="templateId"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-hover-class="false"
|
||||
:with-description="false"
|
||||
:clone-template-on-upload="true"
|
||||
:title="t('clone_and_replace_documents')"
|
||||
|
||||
@@ -63,7 +63,7 @@ const en = {
|
||||
processing_: 'Processing...',
|
||||
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||
add_documents_or_images: 'Add documents or images',
|
||||
upload_a_new_document: 'Upload a new document',
|
||||
add_a_new_document: 'Add a new document',
|
||||
replace_existing_document: 'Replace existing document',
|
||||
clone_and_replace_documents: 'Clone and replace documents',
|
||||
required: 'Required',
|
||||
@@ -228,7 +228,7 @@ const es = {
|
||||
processing_: 'Procesando...',
|
||||
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
||||
add_documents_or_images: 'Agregar documentos o imágenes',
|
||||
upload_a_new_document: 'Subir un nuevo documento',
|
||||
add_a_new_document: 'Agregar un nuevo documento',
|
||||
replace_existing_document: 'Reemplazar documento existente',
|
||||
clone_and_replace_documents: 'Clonar y reemplazar documentos',
|
||||
required: 'Requerido',
|
||||
@@ -399,7 +399,7 @@ const it = {
|
||||
processing_: 'Elaborazione...',
|
||||
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
||||
add_documents_or_images: 'Aggiungi documenti o immagini',
|
||||
upload_a_new_document: 'Carica un nuovo documento',
|
||||
add_a_new_document: 'Aggiungi un nuovo documento',
|
||||
replace_existing_document: 'Sostituisci documento esistente',
|
||||
clone_and_replace_documents: 'Clona e sostituisci documenti',
|
||||
required: 'Obbligatorio',
|
||||
@@ -564,7 +564,7 @@ const pt = {
|
||||
processing_: 'Processando...',
|
||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||
upload_a_new_document: 'Enviar um novo documento',
|
||||
add_a_new_document: 'Ajouter un nouveau document',
|
||||
replace_existing_document: 'Substituir documento existente',
|
||||
clone_and_replace_documents: 'Clonar e substituir documentos',
|
||||
required: 'Obrigatório',
|
||||
@@ -731,7 +731,7 @@ const fr = {
|
||||
processing_: 'Traitement en cours...',
|
||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
||||
add_documents_or_images: 'Ajoutez des documents ou des images',
|
||||
upload_a_new_document: 'Téléverser un nouveau document',
|
||||
add_a_new_document: 'Adicionar um novo documento',
|
||||
replace_existing_document: 'Remplacer le document existant',
|
||||
clone_and_replace_documents: 'Cloner et remplacer les documents',
|
||||
required: 'Requis',
|
||||
@@ -899,7 +899,7 @@ const de = {
|
||||
processing_: 'Verarbeitung...',
|
||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
||||
upload_a_new_document: 'Neues Dokument hochladen',
|
||||
add_a_new_document: 'Neues Dokument hinzufügen',
|
||||
replace_existing_document: 'Vorhandenes Dokument ersetzen',
|
||||
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
|
||||
required: 'Erforderlich',
|
||||
|
||||
@@ -290,7 +290,12 @@ export default {
|
||||
area.cell_w = this.newArea.cell_w
|
||||
}
|
||||
|
||||
this.$emit('draw', area)
|
||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
|
||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
|
||||
|
||||
const isTooSmall = dx < 8 && dy < 8
|
||||
|
||||
this.$emit('draw', { area, isTooSmall })
|
||||
}
|
||||
|
||||
this.showMask = false
|
||||
|
||||
@@ -20,7 +20,7 @@ class ApplicationMailer < ActionMailer::Base
|
||||
end
|
||||
|
||||
def set_message_metadata
|
||||
message.instance_variable_set(:@message_metadata, @message_metadata)
|
||||
message.instance_variable_set(:@message_metadata, @message_metadata || {})
|
||||
end
|
||||
|
||||
def set_message_uuid
|
||||
@@ -28,10 +28,14 @@ class ApplicationMailer < ActionMailer::Base
|
||||
end
|
||||
|
||||
def assign_message_metadata(tag, record)
|
||||
@message_metadata = {
|
||||
@message_metadata = (@message_metadata || {}).merge(
|
||||
'tag' => tag,
|
||||
'record_id' => record.id,
|
||||
'record_type' => record.class.name
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def put_metadata(attrs)
|
||||
@message_metadata = (@message_metadata || {}).merge(attrs)
|
||||
end
|
||||
end
|
||||
@@ -233,9 +233,17 @@ class SubmitterMailer < ApplicationMailer
|
||||
def from_address_for_submitter(submitter)
|
||||
if submitter.submission.source.in?(%w[api embed]) &&
|
||||
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
|
||||
user = submitter.account.users.find_by(email: from_email)
|
||||
|
||||
put_metadata('from_user_id' => user.id)
|
||||
|
||||
from_email
|
||||
else
|
||||
(submitter.submission.created_by_user || submitter.submission.template.author).friendly_name
|
||||
user = submitter.submission.created_by_user || submitter.submission.template.author
|
||||
|
||||
put_metadata('from_user_id' => user.id)
|
||||
|
||||
user.friendly_name
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
|
||||
has_many :account_linked_accounts, dependent: :destroy
|
||||
has_many :email_events, dependent: :destroy
|
||||
has_many :webhook_urls, dependent: :destroy
|
||||
has_many :account_accesses, dependent: :destroy
|
||||
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||
class_name: 'AccountLinkedAccount',
|
||||
inverse_of: :account
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_accesses
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_account_accesses_on_account_id_and_user_id (account_id,user_id) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
class AccountAccess < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :user
|
||||
end
|
||||
@@ -71,7 +71,7 @@ class Submission < ApplicationRecord
|
||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) }
|
||||
scope :expired, -> { where(expire_at: ..Time.current) }
|
||||
scope :expired, -> { pending.where(expire_at: ..Time.current) }
|
||||
|
||||
enum :source, {
|
||||
invite: 'invite',
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
<div class="text-center">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
|
||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||
Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
Europe
|
||||
</a>
|
||||
<% eu_server = request.host == 'docuseal.eu' %>
|
||||
<server-selector>
|
||||
<div id="global_server_selector" class="text-center hidden">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-40 <%= 'bg-base-300' unless eu_server %>">
|
||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||
Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="relative btn bg-base-200 join-item w-40 <%= 'bg-base-300' if eu_server %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
Europe
|
||||
<% unless eu_server %>
|
||||
<span id="eu_server_alert" class="absolute flex space-x-0.5 hidden" style="top: -1.5rem;">
|
||||
<span class="text-xs font-normal leading-none text-base-content normal-case">
|
||||
<%= t('eu_data_residency') %>
|
||||
</span>
|
||||
<%= svg_icon 'corner_right_down', class: 'w-4 h-5 stroke-1 shrink-0 pt-1' %>
|
||||
</span>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="us_server_selector" class="flex justify-center hidden">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
||||
<% if eu_server %>
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
<span>EU Cloud</span>
|
||||
<% else %>
|
||||
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||
<span>US Cloud</span>
|
||||
<% end %>
|
||||
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
|
||||
<li>
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
|
||||
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
|
||||
US Cloud
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
EU Cloud
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</server-selector>
|
||||
<%= render 'scripts/server_selector' %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M6 6h6a3 3 0 0 1 3 3v10l-4 -4m8 0l-4 4"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 640 480">
|
||||
<path fill="#bd3d44" d="M0 0h640v480H0" />
|
||||
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640" />
|
||||
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
|
||||
<marker id="um-a" markerHeight="30" markerWidth="30">
|
||||
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
|
||||
</marker>
|
||||
<path fill="none" marker-mid="url(#um-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 671 B |
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
if (!window.customElements.get('server-selector')) {
|
||||
customElements.define('server-selector', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const usServerSelector = this.querySelector('#us_server_selector');
|
||||
const globalServerSelector = this.querySelector('#global_server_selector');
|
||||
const euServerAlert = this.querySelector('#eu_server_alert');
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
||||
|
||||
if (usTimezones.test(timezone)) {
|
||||
usServerSelector.classList.remove('hidden');
|
||||
} else if (timezone.includes('Europe')) {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
euServerAlert?.classList?.remove('hidden');
|
||||
} else {
|
||||
globalServerSelector.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1 +1 @@
|
||||
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open' %>
|
||||
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30' %>
|
||||
@@ -5,12 +5,13 @@
|
||||
<% font_type = field.dig('preferences', 'font_type') %>
|
||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||
<% if field['type'] == 'signature' %>
|
||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
|
||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow ? 'flex-row' : 'flex-col' %>">
|
||||
<div class="flex overflow-hidden <%= is_narrow ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
||||
</div>
|
||||
<% if local_assigns[:with_signature_id] && attachment = attachments_index[value] %>
|
||||
<div class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]">
|
||||
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
|
||||
<div class="truncate uppercase">
|
||||
ID: <%= attachment.uuid %>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<%= f.hidden_field :submission_slug, value: @submission.slug %>
|
||||
<%= f.label :email, t('email'), class: 'label' %>
|
||||
<%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: t('send_copy_to_email') %>
|
||||
<% if params[:status] == 'error' %>
|
||||
<span class="label-text-alt text-red-400 mt-1">
|
||||
<%= t('please_enter_your_email_address_associated_with_the_completed_submission') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<toggle-submit dir="auto" class="form-control">
|
||||
<%= f.button button_title(title: t('send_copy_to_email'), disabled_with: t('starting')), class: 'base-button' %>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||
<button type="submit" class="hidden"></button>
|
||||
<file-dropzone data-submit-on-upload="true" class="w-full">
|
||||
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2">
|
||||
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border-2 border-base-300 border-dashed" data-target="file-dropzone.area">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2 pointer-events-none">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
|
||||
<span>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<% submitter = submitters.first %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex flex-col md:flex-row md:items-center gap-3">
|
||||
<% if submission.expired? %>
|
||||
<% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
|
||||
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||
<%= t('expired') %>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<% if @templates.present? %>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<%= render partial: 'templates/template', collection: @templates %>
|
||||
<% if show_dropzone && current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
|
||||
<% if show_dropzone && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') && current_user == true_user %>
|
||||
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
|
||||
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
||||
<div class="hidden md:block">
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
<%= user.email %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info badge-outline">
|
||||
<%= user.role %>
|
||||
<span class="badge badge-info badge-outline whitespace-nowrap">
|
||||
<%= t(user.role) %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
+67
-10
@@ -22,6 +22,9 @@ en: &en
|
||||
hi_there: Hi there
|
||||
thanks: Thanks
|
||||
private: Private
|
||||
stripe_integration: Stripe Integration
|
||||
stripe_account_has_been_connected: Stripe account has been connected.
|
||||
re_connect_stripe: Re-connect Stripe
|
||||
bcc_recipients: BCC recipients
|
||||
resend_pending: Re-send pending
|
||||
always_enforce_signing_order: Always enforce the signing order
|
||||
@@ -46,7 +49,7 @@ en: &en
|
||||
team_member_permissions: Team member permissions
|
||||
entire_team: Entire team
|
||||
admin_only: Admin only
|
||||
accessiable_by: Accessiable by
|
||||
accessiable_by: Accessible by
|
||||
team_access: Team access
|
||||
document_download_filename_format: Document download filename format
|
||||
document_name: Document Name
|
||||
@@ -469,7 +472,9 @@ en: &en
|
||||
upload_initials: Upload Initials
|
||||
draw: Draw
|
||||
upload_signature: Upload Signature
|
||||
integration: Integration
|
||||
admin: Admin
|
||||
tenant_admin: Tenant Admin
|
||||
editor: Editor
|
||||
viewer: Viewer
|
||||
member: Member
|
||||
@@ -653,7 +658,7 @@ en: &en
|
||||
completed_at: Completed at
|
||||
edit_recipient: Edit Recipient
|
||||
update_recipient: Update Recipient
|
||||
use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
|
||||
use_international_format_1xxx_: 'Use international format: +1xxx...'
|
||||
submitter_cannot_be_updated: Submitter cannot be updated.
|
||||
at_least_one_field_must_be_filled: At least one field must be filled.
|
||||
archived_users: Archived Users
|
||||
@@ -738,11 +743,13 @@ en: &en
|
||||
one_month: 1 month
|
||||
two_months: 2 months
|
||||
three_months: 3 months
|
||||
eu_data_residency: EU data residency
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Please enter your email address associated with the completed submission.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Bulk Send
|
||||
embed: Embedding
|
||||
invie: Invite
|
||||
invite: Invite
|
||||
link: Link
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||
@@ -824,6 +831,9 @@ en: &en
|
||||
read: Read your data
|
||||
|
||||
es: &es
|
||||
stripe_integration: Integración con Stripe
|
||||
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
|
||||
re_connect_stripe: Volver a conectar Stripe
|
||||
private: Privado
|
||||
resend_pending: Reenviar pendiente
|
||||
ensure_unique_recipients: Asegurar destinatarios únicos
|
||||
@@ -845,7 +855,7 @@ es: &es
|
||||
select_user: Seleccionar usuario
|
||||
team_member_permissions: Permisos de miembros del equipo
|
||||
entire_team: Todo el equipo
|
||||
admin_only: Solo administrador
|
||||
admin_only: Solo admin
|
||||
accessiable_by: Accesible por
|
||||
team_access: Acceso del equipo
|
||||
remove_filter: Eliminar filtro
|
||||
@@ -1274,7 +1284,9 @@ es: &es
|
||||
upload_initials: Subir iniciales
|
||||
draw: Dibujar
|
||||
upload_signature: Subir firma
|
||||
integration: Integración
|
||||
admin: Administrador
|
||||
tenant_admin: Tenant Administrador
|
||||
editor: Editor
|
||||
viewer: Visor
|
||||
member: Miembro
|
||||
@@ -1543,6 +1555,8 @@ es: &es
|
||||
one_month: 1 mes
|
||||
two_months: 2 meses
|
||||
three_months: 3 meses
|
||||
eu_data_residency: Datos alojados UE
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Por favor, introduce tu dirección de correo electrónico asociada con el envío completado.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envío masivo
|
||||
@@ -1629,6 +1643,9 @@ es: &es
|
||||
read: Leer tus datos
|
||||
|
||||
it: &it
|
||||
stripe_integration: Integrazione Stripe
|
||||
stripe_account_has_been_connected: L'account Stripe è stato collegato.
|
||||
re_connect_stripe: Ricollega Stripe
|
||||
private: Privato
|
||||
resend_pending: Reinvia in sospeso
|
||||
ensure_unique_recipients: Assicurarsi destinatari unici
|
||||
@@ -1649,7 +1666,7 @@ it: &it
|
||||
select_user: Seleziona utente
|
||||
team_member_permissions: Permessi membri del team
|
||||
entire_team: Intero team
|
||||
admin_only: Solo amministratore
|
||||
admin_only: Solo admin
|
||||
accessiable_by: Accessibile da
|
||||
team_access: Accesso al team
|
||||
remove_filter: Rimuovi filtro
|
||||
@@ -2078,6 +2095,7 @@ it: &it
|
||||
upload_initials: Carica iniziali
|
||||
draw: Disegna
|
||||
upload_signature: Carica firma
|
||||
integration: Integrazione
|
||||
admin: Amministratore
|
||||
editor: Editor
|
||||
viewer: Visualizzatore
|
||||
@@ -2347,6 +2365,8 @@ it: &it
|
||||
one_month: 1 mese
|
||||
two_months: 2 mesi
|
||||
three_months: 3 mesi
|
||||
eu_data_residency: "Dati nell'UE"
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: "Inserisci il tuo indirizzo email associato all'invio completato."
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Invio massivo
|
||||
@@ -2433,6 +2453,9 @@ it: &it
|
||||
read: Leggi i tuoi dati
|
||||
|
||||
fr: &fr
|
||||
stripe_integration: Intégration Stripe
|
||||
stripe_account_has_been_connected: Le compte Stripe a été connecté.
|
||||
re_connect_stripe: Reconnecter Stripe
|
||||
private: Privé
|
||||
resend_pending: Renvoyer en attente
|
||||
ensure_unique_recipients: Assurer l'unicité des destinataires
|
||||
@@ -2454,7 +2477,7 @@ fr: &fr
|
||||
select_user: Sélectionner un utilisateur
|
||||
team_member_permissions: Permissions des membres de l'équipe
|
||||
entire_team: Équipe entière
|
||||
admin_only: Administrateur uniquement
|
||||
admin_only: Admin seul
|
||||
accessiable_by: Accessible par
|
||||
team_access: Accès à l'équipe
|
||||
remove_filter: Supprimer le filtre
|
||||
@@ -2884,7 +2907,9 @@ fr: &fr
|
||||
upload_initials: Télécharger les initiales
|
||||
draw: Dessiner
|
||||
upload_signature: Télécharger la signature
|
||||
integration: Intégration
|
||||
admin: Administrateur
|
||||
tenant_admin: Tenant Administrateur
|
||||
editor: Éditeur
|
||||
viewer: Lecteur
|
||||
member: Membre
|
||||
@@ -3153,6 +3178,8 @@ fr: &fr
|
||||
one_month: 1 mois
|
||||
two_months: 2 mois
|
||||
three_months: 3 mois
|
||||
eu_data_residency: "Données dans l'UE"
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: "Veuillez saisir l'adresse e-mail associée à l'envoi complété."
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envoi en masse
|
||||
@@ -3239,6 +3266,9 @@ fr: &fr
|
||||
read: Lire vos données
|
||||
|
||||
pt: &pt
|
||||
stripe_integration: Integração com Stripe
|
||||
stripe_account_has_been_connected: Conta Stripe foi conectada.
|
||||
re_connect_stripe: Reconectar Stripe
|
||||
private: Privado
|
||||
resend_pending: Re-enviar pendente
|
||||
ensure_unique_recipients: Garantir destinatários únicos
|
||||
@@ -3260,7 +3290,7 @@ pt: &pt
|
||||
select_user: Selecionar usuário
|
||||
team_member_permissions: Permissões de membro da equipe
|
||||
entire_team: Toda a equipe
|
||||
admin_only: Somente administrador
|
||||
admin_only: Somente admin
|
||||
accessiable_by: Acessível por
|
||||
team_access: Acesso à equipe
|
||||
remove_filter: Remover filtro
|
||||
@@ -3689,7 +3719,9 @@ pt: &pt
|
||||
upload_initials: Enviar iniciais
|
||||
draw: Desenhar
|
||||
upload_signature: Enviar assinatura
|
||||
integration: Integração
|
||||
admin: Administrador
|
||||
tenant_admin: Tenant Administrador
|
||||
editor: Editor
|
||||
viewer: Visualizador
|
||||
member: Membro
|
||||
@@ -3958,6 +3990,8 @@ pt: &pt
|
||||
one_month: 1 mês
|
||||
two_months: 2 meses
|
||||
three_months: 3 meses
|
||||
eu_data_residency: Dados na UE
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Por favor, insira seu e-mail associado ao envio concluído.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envio em massa
|
||||
@@ -4045,6 +4079,9 @@ pt: &pt
|
||||
read: Ler seus dados
|
||||
|
||||
de: &de
|
||||
stripe_integration: Stripe-Integration
|
||||
stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
|
||||
re_connect_stripe: Stripe erneut verbinden
|
||||
private: Privat
|
||||
resend_pending: Ausstehende erneut senden
|
||||
ensure_unique_recipients: Stellen Sie einzigartige Empfänger sicher
|
||||
@@ -4066,7 +4103,7 @@ de: &de
|
||||
select_user: Benutzer auswählen
|
||||
team_member_permissions: Berechtigungen für Teammitglieder
|
||||
entire_team: Gesamtes Team
|
||||
admin_only: Nur Administratoren
|
||||
admin_only: Nur Admins
|
||||
accessiable_by: Zugänglich für
|
||||
team_access: Teamzugang
|
||||
remove_filter: Filter entfernen
|
||||
@@ -4150,9 +4187,9 @@ de: &de
|
||||
sending: Senden
|
||||
resubmit: Erneut einreichen
|
||||
or: oder
|
||||
download_documents: Dokumente herunterladen
|
||||
download_documents: Dokumente downloaden
|
||||
downloading: Lädt herunter
|
||||
download: Laden
|
||||
download: Download
|
||||
decline: Ablehnen
|
||||
declined: Abgelehnt
|
||||
decline_reason: Ablehnungsgrund
|
||||
@@ -4495,7 +4532,9 @@ de: &de
|
||||
upload_initials: Initialen hochladen
|
||||
draw: Zeichnen
|
||||
upload_signature: Unterschrift hochladen
|
||||
integration: Integration
|
||||
admin: Administrator
|
||||
tenant_admin: Tenant Administrator
|
||||
editor: Redakteur
|
||||
viewer: Betrachter
|
||||
member: Mitglied
|
||||
@@ -4764,6 +4803,8 @@ de: &de
|
||||
one_month: 1 Monat
|
||||
two_months: 2 Monate
|
||||
three_months: 3 Monate
|
||||
eu_data_residency: EU-Datenspeicher
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Bitte gib deine E-Mail-Adresse ein, die mit der abgeschlossenen Übermittlung verknüpft ist.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Massenversand
|
||||
@@ -4915,6 +4956,8 @@ pl:
|
||||
powered_by: 'Napędzany prze'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> dokumentów podpisanych za pomocą'
|
||||
open_source_documents_software: 'oprogramowanie do dokumentów open source'
|
||||
eu_data_residency: Dane w UE
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
|
||||
|
||||
uk:
|
||||
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
|
||||
@@ -4982,6 +5025,8 @@ uk:
|
||||
powered_by: 'Працює на базі'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> документів підписано за допомогою'
|
||||
open_source_documents_software: 'відкрите програмне забезпечення для документів'
|
||||
eu_data_residency: 'Зберігання даних в ЄС'
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
|
||||
|
||||
cs:
|
||||
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
|
||||
@@ -5049,6 +5094,8 @@ cs:
|
||||
powered_by: 'Poháněno'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> dokumentů podepsáno pomocí'
|
||||
open_source_documents_software: 'open source software pro dokumenty'
|
||||
eu_data_residency: 'Uložení dat v EU'
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
|
||||
|
||||
he:
|
||||
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
|
||||
@@ -5116,6 +5163,8 @@ he:
|
||||
powered_by: 'מופעל על ידי'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> מסמכים נחתמו באמצעות'
|
||||
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
|
||||
eu_data_residency: 'נתונים באיחוד האירופי '
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
|
||||
|
||||
nl:
|
||||
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
|
||||
@@ -5183,6 +5232,8 @@ nl:
|
||||
powered_by: 'Aangedreven door'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> documenten ondertekend met'
|
||||
open_source_documents_software: 'open source documenten software'
|
||||
eu_data_residency: Gegevens EU
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
|
||||
|
||||
ar:
|
||||
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
||||
@@ -5250,6 +5301,8 @@ ar:
|
||||
powered_by: 'مشغل بواسطة'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> مستندات تم توقيعها باستخدام'
|
||||
open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
|
||||
eu_data_residency: 'بيانات في الاتحاد الأوروبي'
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
|
||||
|
||||
ko:
|
||||
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
|
||||
@@ -5317,6 +5370,8 @@ ko:
|
||||
powered_by: '제공:'
|
||||
count_documents_signed_with_html: '<b>%{count}</b>개의 문서가 다음을 통해 서명됨'
|
||||
open_source_documents_software: '오픈소스 문서 소프트웨어'
|
||||
eu_data_residency: 'EU 데이터 보관'
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
|
||||
|
||||
ja:
|
||||
require_phone_2fa_to_open: 電話による2段階認証が必要です
|
||||
@@ -5384,6 +5439,8 @@ ja:
|
||||
powered_by: '提供元:'
|
||||
count_documents_signed_with_html: '<b>%{count}</b> 件のドキュメントが以下で署名されました'
|
||||
open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
|
||||
eu_data_residency: 'EU データ居住'
|
||||
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
|
||||
|
||||
en-US:
|
||||
<<: *en
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :verify_pdf_signature, only: %i[create]
|
||||
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
||||
resources :account_configs, only: %i[create]
|
||||
resources :account_configs, only: %i[create destroy]
|
||||
resources :user_configs, only: %i[create]
|
||||
resources :encrypted_user_configs, only: %i[destroy]
|
||||
resources :timestamp_server, only: %i[create]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountAccesses < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :account_accesses do |t|
|
||||
t.references :account, null: false, foreign_key: true, index: false
|
||||
t.references :user, null: false, foreign_key: false, index: false
|
||||
|
||||
t.index %i[account_id user_id], unique: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
+10
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_05_18_070555) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -24,6 +24,14 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
|
||||
t.index ["user_id"], name: "index_access_tokens_on_user_id"
|
||||
end
|
||||
|
||||
create_table "account_accesses", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "user_id"], name: "index_account_accesses_on_account_id_and_user_id", unique: true
|
||||
end
|
||||
|
||||
create_table "account_configs", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.string "key", null: false
|
||||
@@ -415,6 +423,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
|
||||
end
|
||||
|
||||
add_foreign_key "access_tokens", "users"
|
||||
add_foreign_key "account_accesses", "accounts"
|
||||
add_foreign_key "account_configs", "accounts"
|
||||
add_foreign_key "account_linked_accounts", "accounts"
|
||||
add_foreign_key "account_linked_accounts", "accounts", column: "linked_account_id"
|
||||
|
||||
+2238
File diff suppressed because it is too large.
Load diff
+2657
File diff suppressed because it is too large.
Load diff
+2219
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+2420
File diff suppressed because it is too large.
Load diff
+2277
File diff suppressed because it is too large.
Load diff
+2334
File diff suppressed because it is too large.
Load diff
+2334
File diff suppressed because it is too large.
Load diff
+2219
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,513 @@
|
||||
# Angular Form Builder
|
||||
|
||||
### Example Code
|
||||
|
||||
```angular
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DocusealBuilderComponent } from '@docuseal/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [DocusealBuilderComponent],
|
||||
template: `
|
||||
<div class="app">
|
||||
<ng-container *ngIf="token">
|
||||
<docuseal-builder [token]="token"></docuseal-builder>
|
||||
</ng-container>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
token: string = ''
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.http.post('/api/docuseal/builder_token', {}).subscribe((data: any) => {
|
||||
this.token = data.token;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```javascript
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const token = jwt.sign({
|
||||
user_email: '{{admin_user_email}}',
|
||||
integration_email: '{{signer_email}}',
|
||||
external_id: 'TestForm123',
|
||||
name: 'Integration W-9 Test Form',
|
||||
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
|
||||
}, '{{api_key}}');
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"type": "string",
|
||||
"doc_type": "object",
|
||||
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
|
||||
"required": true,
|
||||
"properties": {
|
||||
"user_email": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email of the owner of the API signing key - admin user email."
|
||||
},
|
||||
"integration_email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email of the user to create a template for.",
|
||||
"example": "signer@example.com"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this template within your app.",
|
||||
"required": false
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "The folder name in which the template should be created.",
|
||||
"required": false
|
||||
},
|
||||
"document_urls": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
|
||||
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "New template name when creating a template with document_urls specified.",
|
||||
"example": "Integration W-9 Test Form"
|
||||
},
|
||||
"extract_fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
|
||||
"example": "yourdomain.com"
|
||||
},
|
||||
"customButton": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Custom button will be displayed on the top right corner of the form builder.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Custom button title."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Custom button URL. Only absolute URLs are supported."
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Submitter role names to be used by default in the form."
|
||||
},
|
||||
"fieldTypes": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field type names to be used in the form builder. All field types are used by default."
|
||||
},
|
||||
"drawFieldType": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "text",
|
||||
"description": "Field type to be used by default with the field drawing tool.",
|
||||
"example": "signature"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default custom field properties with `name` to be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default submitters with `role` name to be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter email."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Submitter role name."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter name."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter phone number, formatted according to the E.164 standard."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requiredFields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default required custom field properties with `name` that should be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"emailMessage": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Email subject and body for the signature request.",
|
||||
"properties": {
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email subject for the signature request."
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email body for the signature request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"withTitle": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove document title from the builder."
|
||||
},
|
||||
"withSendButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Send\" button."
|
||||
},
|
||||
"withUploadButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Upload\" button."
|
||||
},
|
||||
"withAddPageButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show the \"Add Blank Page\" button."
|
||||
},
|
||||
"withSignYourselfButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Sign Yourself\" button."
|
||||
},
|
||||
"withDocumentsList": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
|
||||
},
|
||||
"withFieldsList": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
|
||||
},
|
||||
"withFieldPlaceholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"onlyDefinedFields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow to add fields only defined in the `fields` prop."
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show template in preview mode without ability to edit it."
|
||||
},
|
||||
"autosave": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable form changes autosaving."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "en",
|
||||
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The form builder background color. Only HEX color codes are supported.",
|
||||
"example": "#ffffff"
|
||||
},
|
||||
"customCss": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form builder.",
|
||||
"example": "#sign_yourself_button { background-color: #FFA500; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"onLoad": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted on loading the form builder template data.",
|
||||
"example": "handleLoad($event)"
|
||||
},
|
||||
"onUpload": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted on uploading a document to the template.",
|
||||
"example": "handleUpload($event)"
|
||||
},
|
||||
"onSend": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted on sending documents for signature to recipients.",
|
||||
"example": "handleSend($event)"
|
||||
},
|
||||
"onChange": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called when changes are made to the template form.",
|
||||
"example": "handleChange($event)"
|
||||
},
|
||||
"onSave": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted on saving changes of the template form.",
|
||||
"example": "handleSave($event)"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,477 @@
|
||||
# JavaScript Form Builder
|
||||
|
||||
### Example Code
|
||||
|
||||
```javascript
|
||||
<script src="https://docuseal.com/js/builder.js"></script>
|
||||
|
||||
<docuseal-builder
|
||||
data-token="<%= JWT.encode({
|
||||
user_email: '{{admin_user_email}}',
|
||||
integration_email: '{{signer_email}}',
|
||||
name: 'Integration W-9 Test Form',
|
||||
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
|
||||
}, '{{api_key}}') %>">
|
||||
</docuseal-builder>
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"data-token": {
|
||||
"type": "string",
|
||||
"doc_type": "object",
|
||||
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
|
||||
"required": true,
|
||||
"properties": {
|
||||
"user_email": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email of the owner of the API signing key - admin user email."
|
||||
},
|
||||
"integration_email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email of the user to create a template for.",
|
||||
"example": "signer@example.com"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this template within your app.",
|
||||
"required": false
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "The folder name in which the template should be created.",
|
||||
"required": false
|
||||
},
|
||||
"document_urls": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
|
||||
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "New template name when creating a template with document_urls specified.",
|
||||
"example": "Integration W-9 Test Form"
|
||||
},
|
||||
"extract_fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
|
||||
}
|
||||
}
|
||||
},
|
||||
"data-host": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
|
||||
"example": "yourdomain.com"
|
||||
},
|
||||
"data-roles": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Comma separated submitter role names to be used by default in the form.",
|
||||
"example": "Company,Customer"
|
||||
},
|
||||
"data-fields": {
|
||||
"type": "string",
|
||||
"doc_type": "array",
|
||||
"required": false,
|
||||
"description": "A list of default custom fields with `name` to be added to the document. Should contain an array of field properties as a JSON encoded string.",
|
||||
"example": "[{ \"name\": \"FIELD_1\", \"type\": \"date\", \"role\": \"Customer\", \"default_value\": \"2021-01-01\" }]",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"data-submitters": {
|
||||
"type": "string",
|
||||
"doc_type": "array",
|
||||
"required": false,
|
||||
"description": "A list of default submitters with `role` name to be added to the document. Should contain an array of field properties as a JSON encoded string.",
|
||||
"example": "[{ \"email\": \"example@company.com\", \"name\": \"John Doe\", \"phone\": \"+1234567890\", \"role\": \"Customer\" }]",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter email."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Submitter role name."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter name."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter phone number, formatted according to the E.164 standard."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"data-required-fields": {
|
||||
"type": "string",
|
||||
"doc_type": "array",
|
||||
"required": false,
|
||||
"description": "A list of required default custom fields with `name` that should be added to the document. Should contain an array of field properties as a JSON encoded string.",
|
||||
"example": "[{ \"name\": \"FIELD_1\", \"type\": \"date\", \"role\": \"Customer\", \"default_value\": \"2021-01-01\" }]",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"data-field-types": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Comma separated field type names to be used in the form builder. All field types are used by default.",
|
||||
"example": "text,date"
|
||||
},
|
||||
"data-draw-field-type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "text",
|
||||
"description": "Field type to be used by default with the field drawing tool.",
|
||||
"example": "signature"
|
||||
},
|
||||
"data-custom-button-title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom button title. This button will be displayed on the top right corner of the form builder."
|
||||
},
|
||||
"data-custom-button-url": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom button URL. Only absolute URLs are supported."
|
||||
},
|
||||
"data-with-title": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove document title from the builder."
|
||||
},
|
||||
"email-subject": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email subject for the signature request. Required if `email-body` specified"
|
||||
},
|
||||
"email-body": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email body for the signature request. Required if `email-subject` specified"
|
||||
},
|
||||
"data-with-send-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Recipients\" button."
|
||||
},
|
||||
"data-with-upload-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Upload\" button."
|
||||
},
|
||||
"data-with-add-page-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show the \"Add Blank Page\" button."
|
||||
},
|
||||
"data-with-sign-yourself-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Sign Yourself\" button."
|
||||
},
|
||||
"data-with-documents-list": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
|
||||
},
|
||||
"data-with-fields-list": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
|
||||
},
|
||||
"data-with-field-placeholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"data-preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show template in preview mode without ability to edit it."
|
||||
},
|
||||
"data-only-defined-fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow to add fields only defined in the `data-fields` attribute."
|
||||
},
|
||||
"data-autosave": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable form changes autosaving."
|
||||
},
|
||||
"data-i18n": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "JSON encoded string that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"data-language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "en",
|
||||
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
|
||||
},
|
||||
"data-background-color": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The form builder background color. Only HEX color codes are supported.",
|
||||
"example": "#ffffff"
|
||||
},
|
||||
"data-custom-css": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form builder.",
|
||||
"example": "#sign_yourself_button { background-color: #FFA500; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"load": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered on loading the form builder template data.",
|
||||
"example": "document.querySelector('docuseal-builder').addEventListener('load', (e) => e.detail)"
|
||||
},
|
||||
"upload": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered on uploading a document to the template.",
|
||||
"example": "document.querySelector('docuseal-builder').addEventListener('upload', (e) => e.detail)"
|
||||
},
|
||||
"send": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered on sending documents for signature to recipients.",
|
||||
"example": "document.querySelector('docuseal-builder').addEventListener('send', (e) => e.detail)"
|
||||
},
|
||||
"change": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered every time a change to the template is made.",
|
||||
"example": "document.querySelector('docuseal-builder').addEventListener('change', (e) => e.detail)"
|
||||
},
|
||||
"save": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered on saving changes of the template form.",
|
||||
"example": "document.querySelector('docuseal-builder').addEventListener('save', (e) => e.detail)"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,504 @@
|
||||
# React Form Builder
|
||||
|
||||
### Example Code
|
||||
|
||||
```react
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DocusealBuilder } from '@docuseal/react'
|
||||
|
||||
const App = () => {
|
||||
const [token, setToken] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/docuseal/builder_token', {
|
||||
method: 'POST',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setToken(data.token);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return token && <DocusealBuilder token={token} />;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
```javascript
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const token = jwt.sign({
|
||||
user_email: '{{admin_user_email}}',
|
||||
integration_email: '{{signer_email}}',
|
||||
external_id: 'TestForm123',
|
||||
name: 'Integration W-9 Test Form',
|
||||
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
|
||||
}, '{{api_key}}');
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"type": "string",
|
||||
"doc_type": "object",
|
||||
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
|
||||
"required": true,
|
||||
"properties": {
|
||||
"user_email": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email of the owner of the API signing key - admin user email."
|
||||
},
|
||||
"integration_email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email of the user to create a template for.",
|
||||
"example": "signer@example.com"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this template within your app.",
|
||||
"required": false
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "The folder name in which the template should be created.",
|
||||
"required": false
|
||||
},
|
||||
"document_urls": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
|
||||
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "New template name when creating a template with document_urls specified.",
|
||||
"example": "Integration W-9 Test Form"
|
||||
},
|
||||
"extract_fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
|
||||
"example": "yourdomain.com"
|
||||
},
|
||||
"customButton": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Custom button will be displayed on the top right corner of the form builder.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Custom button title."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Custom button URL. Only absolute URLs are supported."
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Submitter role names to be used by default in the form."
|
||||
},
|
||||
"fieldTypes": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field type names to be used in the form builder. All field types are used by default."
|
||||
},
|
||||
"drawFieldType": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "text",
|
||||
"description": "Field type to be used by default with the field drawing tool.",
|
||||
"example": "signature"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default custom field properties with `name` to be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default submitters with `role` name to be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter email."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Submitter role name."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter name."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter phone number, formatted according to the E.164 standard."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requiredFields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default required custom field properties with `name` that should be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"emailMessage": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Email subject and body for the signature request.",
|
||||
"properties": {
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email subject for the signature request."
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email body for the signature request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"withTitle": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove document title from the builder."
|
||||
},
|
||||
"withSendButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Send\" button."
|
||||
},
|
||||
"withUploadButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Upload\" button."
|
||||
},
|
||||
"withAddPageButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show the \"Add Blank Page\" button."
|
||||
},
|
||||
"withSignYourselfButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Sign Yourself\" button."
|
||||
},
|
||||
"withDocumentsList": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
|
||||
},
|
||||
"withFieldsList": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
|
||||
},
|
||||
"withFieldPlaceholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"onlyDefinedFields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow to add fields only defined in the `fields` prop."
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show template in preview mode without ability to edit it."
|
||||
},
|
||||
"autosave": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable form changes autosaving."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "en",
|
||||
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The form builder background color. Only HEX color codes are supported.",
|
||||
"example": "#ffffff"
|
||||
},
|
||||
"customCss": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form builder.",
|
||||
"example": "#sign_yourself_button { background-color: #FFA500; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"onLoad": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on loading the form builder template data.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
},
|
||||
"onUpload": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on uploading a document to the template.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
},
|
||||
"onSend": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on sending documents for signature to recipients.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
},
|
||||
"onChange": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called when changes are made to the template form.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
},
|
||||
"onSave": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on saving changes of the template form.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,513 @@
|
||||
# Vue Form Builder
|
||||
|
||||
### Example Code
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<DocusealBuilder
|
||||
v-if="token"
|
||||
:token="token"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DocusealBuilder } from '@docuseal/vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { DocusealBuilder },
|
||||
data () {
|
||||
return { token: '' }
|
||||
},
|
||||
mounted () {
|
||||
fetch('/api/docuseal/builder_token', {
|
||||
method: 'POST'
|
||||
}).then(async (resp) => {
|
||||
const data = await resp.json()
|
||||
|
||||
this.token = data.token
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
```
|
||||
|
||||
```javascript
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const token = jwt.sign({
|
||||
user_email: '{{admin_user_email}}',
|
||||
integration_email: '{{signer_email}}',
|
||||
external_id: 'TestForm123',
|
||||
name: 'Integration W-9 Test Form',
|
||||
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
|
||||
}, '{{api_key}}');
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"type": "string",
|
||||
"doc_type": "object",
|
||||
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
|
||||
"required": true,
|
||||
"properties": {
|
||||
"user_email": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email of the owner of the API signing key - admin user email."
|
||||
},
|
||||
"integration_email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email of the user to create a template for.",
|
||||
"example": "signer@example.com"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this template within your app.",
|
||||
"required": false
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "The folder name in which the template should be created.",
|
||||
"required": false
|
||||
},
|
||||
"document_urls": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
|
||||
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "New template name when creating a template with document_urls specified.",
|
||||
"example": "Integration W-9 Test Form"
|
||||
},
|
||||
"extract_fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
|
||||
"example": "yourdomain.com"
|
||||
},
|
||||
"custom-button": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Custom button will be displayed on the top right corner of the form builder.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Custom button title."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Custom button URL. Only absolute URLs are supported."
|
||||
}
|
||||
}
|
||||
},
|
||||
"only-defined-fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow to add fields only defined in the `:fields` prop."
|
||||
},
|
||||
"with-send-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Recipients\" button."
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Submitter role names to be used by default in the form."
|
||||
},
|
||||
"field-types": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field type names to be used in the form builder. All field types are used by default."
|
||||
},
|
||||
"draw-field-type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "text",
|
||||
"description": "Field type to be used by default with the field drawing tool.",
|
||||
"example": "signature"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default custom field properties with `name` to be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default submitters with `role` name to be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter email."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Submitter role name."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter name."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter phone number, formatted according to the E.164 standard."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required-fields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of default required custom field properties with `name` that should be added to the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Field name."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field type.",
|
||||
"enum": [
|
||||
"heading",
|
||||
"text",
|
||||
"signature",
|
||||
"initials",
|
||||
"date",
|
||||
"number",
|
||||
"image",
|
||||
"checkbox",
|
||||
"multiple",
|
||||
"file",
|
||||
"radio",
|
||||
"select",
|
||||
"cells",
|
||||
"stamp",
|
||||
"payment",
|
||||
"phone",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Submitter role name for the field."
|
||||
},
|
||||
"default_value": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Default value of the field."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field description displayed on the signing form. Supports Markdown."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field width in pixels."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"description": "Field height in pixels."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field format. Depends on the field type."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Field options. Required for the select field type."
|
||||
},
|
||||
"validation": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Field validation rules.",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Field pattern.",
|
||||
"example": "^[0-9]{5}$"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Validation error message."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"email-message": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Email subject and body for the signature request.",
|
||||
"properties": {
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email subject for the signature request."
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Email body for the signature request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"with-title": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove document title from the builder."
|
||||
},
|
||||
"with-upload-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Upload\" button."
|
||||
},
|
||||
"with-add-page-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show the \"Add Blank Page\" button."
|
||||
},
|
||||
"with-sign-yourself-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Show the \"Sign Yourself\" button."
|
||||
},
|
||||
"with-documents-list": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
|
||||
},
|
||||
"with-fields-list": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
|
||||
},
|
||||
"with-field-placeholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"autosave": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable form changes autosaving."
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show template in preview mode without ability to edit it."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "en",
|
||||
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"background-color": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The form builder background color. Only HEX color codes are supported.",
|
||||
"example": "#ffffff"
|
||||
},
|
||||
"custom-css": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form builder.",
|
||||
"example": "#sign_yourself_button { background-color: #FFA500; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"@load": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on loading the form builder template data.",
|
||||
"example": "onBuilderLoad"
|
||||
},
|
||||
"@upload": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on uploading a document to the template.",
|
||||
"example": "onBuilderUpload"
|
||||
},
|
||||
"@send": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on sending documents for signature to recipients.",
|
||||
"example": "onBuilderSend"
|
||||
},
|
||||
"@change": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called when changes are made to the template form.",
|
||||
"example": "onBuilderChange"
|
||||
},
|
||||
"@save": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on saving changes of the template form.",
|
||||
"example": "onBuilderSave"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,295 @@
|
||||
# Angular Signing Form
|
||||
|
||||
### Example Code
|
||||
|
||||
```angular
|
||||
import { Component } from '@angular/core';
|
||||
import { DocusealFormComponent } from '@docuseal/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [DocusealFormComponent],
|
||||
template: `
|
||||
<div class="app">
|
||||
<docuseal-form
|
||||
[src]="'https://docuseal.com/d/{{template_slug}}'"
|
||||
[email]="'{{signer_email}}'">
|
||||
</docuseal-form>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"src": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Name of the signer."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The role name or title of the signer.",
|
||||
"example": "First Party"
|
||||
},
|
||||
"expand": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Expand form on open.",
|
||||
"default": true
|
||||
},
|
||||
"minimize": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
|
||||
"default": false
|
||||
},
|
||||
"orderAsOnPage": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Order form fields based on their position on the pages."
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Your application-specific unique string key to identify signer within your app."
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Public logo image URL to use in the signing form."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show form in preview mode without ability to submit it."
|
||||
},
|
||||
"goToLast": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Navigate to the last unfinished step."
|
||||
},
|
||||
"withFieldNames": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
},
|
||||
"withFieldPlaceholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"skipFields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow skipping form fields."
|
||||
},
|
||||
"autoscrollFields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable auto-scrolling to the next document field."
|
||||
},
|
||||
"sendCopyEmail": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Form background color. Only HEX color codes are supported.",
|
||||
"example": "#d9d9d9"
|
||||
},
|
||||
"completedRedirectUrl": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "URL to redirect to after the submission completion.",
|
||||
"example": "https://docuseal.com/success"
|
||||
},
|
||||
"completedMessage": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Message displayed after the form completion.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message title.",
|
||||
"example": "Documents have been signed!"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message content.",
|
||||
"example": "If you have any questions, please contact us."
|
||||
}
|
||||
}
|
||||
},
|
||||
"completedButton": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Customizable button after form completion.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Button label.",
|
||||
"example": "Go Back"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Button link. Only absolute URLs are supported.",
|
||||
"example": "https://example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"withTitle": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the document title from the form."
|
||||
},
|
||||
"withDecline": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the decline button in the form."
|
||||
},
|
||||
"withDownloadButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document download button from the completed form card."
|
||||
},
|
||||
"withSendCopyButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document send email button from the completed form card."
|
||||
},
|
||||
"withCompleteButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the complete button in the form header."
|
||||
},
|
||||
"allowToResubmit": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow user to re-submit the form."
|
||||
},
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
},
|
||||
"rememberSignature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
|
||||
},
|
||||
"reuseSignature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
|
||||
},
|
||||
"allowTypedSignature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow users to type their signature."
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Pre-assigned values for form fields.",
|
||||
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Metadata object with additional signer information.",
|
||||
"example": "{ customData: 'custom value' }"
|
||||
},
|
||||
"readonlyFields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "List of read-only fields.",
|
||||
"example": "['First Name','Last Name']"
|
||||
},
|
||||
"customCss": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form.",
|
||||
"example": "#submit_form_button { background-color: #d9d9d9; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"onInit": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted on initializing the form component.",
|
||||
"example": "handleInit($event)"
|
||||
},
|
||||
"onLoad": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted on loading the form data.",
|
||||
"example": "handleLoad($event)"
|
||||
},
|
||||
"onComplete": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted the form completion.",
|
||||
"example": "handleComplete($event)"
|
||||
},
|
||||
"onDecline": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted after the form decline.",
|
||||
"example": "handleDecline($event)"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,275 @@
|
||||
# JavaScript Signing Form
|
||||
|
||||
### Example Code
|
||||
|
||||
```javascript
|
||||
<script src="https://cdn.docuseal.com/js/form.js"></script>
|
||||
|
||||
<docuseal-form
|
||||
id="docusealForm"
|
||||
data-src="https://docuseal.com/d/{{template_slug}}"
|
||||
data-email="{{signer_email}}">
|
||||
</docuseal-form>
|
||||
|
||||
<script>
|
||||
window.docusealForm.addEventListener('completed', (e) => e.detail)
|
||||
</script>
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"data-src": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
|
||||
},
|
||||
"data-email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
|
||||
},
|
||||
"data-name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Name of the signer."
|
||||
},
|
||||
"data-role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The role name or title of the signer.",
|
||||
"example": "First Party"
|
||||
},
|
||||
"data-expand": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Expand form on open.",
|
||||
"default": true
|
||||
},
|
||||
"data-minimize": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
|
||||
"default": false
|
||||
},
|
||||
"data-order-as-on-page": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Order form fields based on their position on the pages."
|
||||
},
|
||||
"data-preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show form in preview mode without ability to submit it."
|
||||
},
|
||||
"data-logo": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Public logo image URL to use in the signing form."
|
||||
},
|
||||
"data-language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
},
|
||||
"data-i18n": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "JSON encoded string that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"data-go-to-last": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Navigate to the last unfinished step."
|
||||
},
|
||||
"data-skip-fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow skipping form fields."
|
||||
},
|
||||
"data-autoscroll-fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable auto-scrolling to the next document field."
|
||||
},
|
||||
"data-send-copy-email": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
|
||||
},
|
||||
"data-with-title": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the document title from the form."
|
||||
},
|
||||
"data-with-decline": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the decline button in the form."
|
||||
},
|
||||
"data-with-field-names": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
},
|
||||
"data-with-field-placeholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"data-with-download-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document download button from the completed form card."
|
||||
},
|
||||
"data-with-send-copy-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document send email button from the completed form card."
|
||||
},
|
||||
"data-with-complete-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the complete button in the form header."
|
||||
},
|
||||
"data-allow-to-resubmit": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow users to re-submit the form."
|
||||
},
|
||||
"data-allow-typed-signature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow users to type their signature."
|
||||
},
|
||||
"data-signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
},
|
||||
"data-remember-signature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
|
||||
},
|
||||
"data-reuse-signature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
|
||||
},
|
||||
"data-background-color": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Form background color. Only HEX color codes are supported.",
|
||||
"example": "#d9d9d9"
|
||||
},
|
||||
"data-values": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Pre-assigned values for form fields.",
|
||||
"example": "{\"First Name\":\"Jon\",\"Last Name\":\"Doe\"}"
|
||||
},
|
||||
"data-external-id": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Your application-specific unique string key to identify signer within your app."
|
||||
},
|
||||
"data-metadata": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Signer metadata Object in JSON format. ",
|
||||
"example": "{\"customData\":\"customValue\"}"
|
||||
},
|
||||
"data-readonly-fields": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Comma separated read-only field names",
|
||||
"example": "First Name,Last Name"
|
||||
},
|
||||
"data-completed-redirect-url": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "URL to redirect to after the submission completion.",
|
||||
"example": "https://docuseal.com/success"
|
||||
},
|
||||
"data-completed-message-title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message title displayed after the form completion.",
|
||||
"example": "Documents have been completed"
|
||||
},
|
||||
"data-completed-message-body": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message body displayed after the form completion.",
|
||||
"example": "If you have any questions, please contact us."
|
||||
},
|
||||
"data-completed-button-title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Button title displayed after the form completion.",
|
||||
"example": "Go Back"
|
||||
},
|
||||
"data-completed-button-url": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "URL of the button displayed after the form completion.",
|
||||
"example": "https://example.com"
|
||||
},
|
||||
"data-custom-css": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form.",
|
||||
"example": "#submit_form_button { background-color: #d9d9d9; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"init": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered on initializing the form component.",
|
||||
"example": "document.querySelector('docuseal-form').addEventListener('init', () => console.log('init'))"
|
||||
},
|
||||
"load": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered on loading the form data.",
|
||||
"example": "document.querySelector('docuseal-form').addEventListener('load', (e) => e.detail)"
|
||||
},
|
||||
"completed": {
|
||||
"type": "event",
|
||||
"required": false,
|
||||
"description": "Custom event to be triggered after form completion.",
|
||||
"example": "document.querySelector('docuseal-form').addEventListener('completed', (e) => e.detail)"
|
||||
},
|
||||
"declined": {
|
||||
"type": "event",
|
||||
"description": "Custom event to be triggered after form decline.",
|
||||
"example": "document.querySelector('docuseal-form').addEventListener('declined', (e) => e.detail)"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,292 @@
|
||||
# React Signing Form
|
||||
|
||||
### Example Code
|
||||
|
||||
```react
|
||||
import React from "react"
|
||||
import { DocusealForm } from '@docuseal/react'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div className="app">
|
||||
<DocusealForm
|
||||
src="https://docuseal.com/d/{{template_slug}}"
|
||||
email="{{signer_email}}"
|
||||
onComplete={(data) => console.log(data)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"src": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Name of the signer."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The role name or title of the signer.",
|
||||
"example": "First Party"
|
||||
},
|
||||
"expand": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Expand form on open.",
|
||||
"default": true
|
||||
},
|
||||
"minimize": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
|
||||
"default": false
|
||||
},
|
||||
"orderAsOnPage": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Order form fields based on their position on the pages."
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Your application-specific unique string key to identify signer within your app."
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Public logo image URL to use in the signing form."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show form in preview mode without ability to submit it."
|
||||
},
|
||||
"goToLast": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Navigate to the last unfinished step."
|
||||
},
|
||||
"withFieldNames": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
},
|
||||
"withFieldPlaceholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"skipFields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow skipping form fields."
|
||||
},
|
||||
"autoscrollFields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable auto-scrolling to the next document field."
|
||||
},
|
||||
"sendCopyEmail": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Form background color. Only HEX color codes are supported.",
|
||||
"example": "#d9d9d9"
|
||||
},
|
||||
"completedRedirectUrl": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "URL to redirect to after the submission completion.",
|
||||
"example": "https://docuseal.com/success"
|
||||
},
|
||||
"completedMessage": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Message displayed after the form completion.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message title.",
|
||||
"example": "Documents have been signed!"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message content.",
|
||||
"example": "If you have any questions, please contact us."
|
||||
}
|
||||
}
|
||||
},
|
||||
"completedButton": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Customizable button after form completion.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Button label.",
|
||||
"example": "Go Back"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Button link. Only absolute URLs are supported.",
|
||||
"example": "https://example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"withTitle": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the document title from the form."
|
||||
},
|
||||
"withDecline": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the decline button in the form."
|
||||
},
|
||||
"withDownloadButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document download button from the completed form card."
|
||||
},
|
||||
"withSendCopyButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document send email button from the completed form card."
|
||||
},
|
||||
"withCompleteButton": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the complete button in the form header."
|
||||
},
|
||||
"allowToResubmit": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow user to re-submit the form."
|
||||
},
|
||||
"allowTypedSignature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow users to type their signature."
|
||||
},
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
},
|
||||
"rememberSignature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
|
||||
},
|
||||
"reuseSignature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Pre-assigned values for form fields.",
|
||||
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Metadata object with additional signer information.",
|
||||
"example": "{ customData: 'custom value' }"
|
||||
},
|
||||
"readonlyFields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "List of read-only fields.",
|
||||
"example": "['First Name','Last Name']"
|
||||
},
|
||||
"customCss": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form.",
|
||||
"example": "#submit_form_button { background-color: #d9d9d9; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"onInit": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on initializing the form component.",
|
||||
"example": "() => { console.log(\"Loaded\") }"
|
||||
},
|
||||
"onLoad": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on loading the form data.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
},
|
||||
"onComplete": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called after the form completion.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
},
|
||||
"onDecline": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called after the form decline.",
|
||||
"example": "(data) => { console.log(data) }"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,302 @@
|
||||
# Vue Signing Form
|
||||
|
||||
### Example Code
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<DocusealForm
|
||||
:src="'https://docuseal.com/d/{{template_slug}}'"
|
||||
:email="'{{signer_email}}'"
|
||||
@complete="onFormComplete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DocusealForm } from '@docuseal/vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
DocusealForm
|
||||
},
|
||||
methods: {
|
||||
onFormComplete (data) {
|
||||
console.log(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
```json
|
||||
{
|
||||
"src": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Name of the signer."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The role name or title of the signer.",
|
||||
"example": "First Party"
|
||||
},
|
||||
"external-id": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Your application-specific unique string key to identify signer within your app."
|
||||
},
|
||||
"expand": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Expand form on open.",
|
||||
"default": true
|
||||
},
|
||||
"minimize": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
|
||||
"default": false
|
||||
},
|
||||
"order-as-on-page": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Order form fields based on their position on the pages."
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Public logo image URL to use in the signing form."
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
},
|
||||
"i18n": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Show form in preview mode without ability to submit it."
|
||||
},
|
||||
"go-to-last": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Navigate to the last unfinished step."
|
||||
},
|
||||
"skip-fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Allow skipping form fields."
|
||||
},
|
||||
"autoscroll-fields": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable auto-scrolling to the next document field."
|
||||
},
|
||||
"with-field-names": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
},
|
||||
"with-field-placeholder": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display field name placeholders instead of the field type icons."
|
||||
},
|
||||
"send-copy-email": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
|
||||
},
|
||||
"background-color": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Form background color. Only HEX color codes are supported.",
|
||||
"example": "#d9d9d9"
|
||||
},
|
||||
"with-title": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the document title from the form."
|
||||
},
|
||||
"with-decline": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the decline button in the form."
|
||||
},
|
||||
"with-download-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document download button from the completed form card."
|
||||
},
|
||||
"with-send-copy-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to remove the signed document send email button from the completed form card."
|
||||
},
|
||||
"with-complete-button": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": false,
|
||||
"description": "Set `true` to display the complete button in the form header."
|
||||
},
|
||||
"allow-to-resubmit": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow user to re-submit the form."
|
||||
},
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
},
|
||||
"remember-signature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
|
||||
},
|
||||
"reuse-signature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
|
||||
},
|
||||
"allow-typed-signature": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to disallow users to type their signature."
|
||||
},
|
||||
"completed-redirect-url": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "URL to redirect to after the submission completion.",
|
||||
"example": "https://docuseal.com/success"
|
||||
},
|
||||
"completed-message": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Message displayed after the form completion.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message title.",
|
||||
"example": "Documents have been signed!"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Message content.",
|
||||
"example": "If you have any questions, please contact us."
|
||||
}
|
||||
}
|
||||
},
|
||||
"completed-button": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Customizable button after form completion.",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Button label.",
|
||||
"example": "Go Back"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Button link. Only absolute URLs are supported.",
|
||||
"example": "https://example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Pre-assigned values for form fields.",
|
||||
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Metadata object with additional signer information.",
|
||||
"example": "{ customData: 'custom value' }"
|
||||
},
|
||||
"readonly-fields": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "List of read-only fields.",
|
||||
"example": "['First Name','Last Name']"
|
||||
},
|
||||
"custom-css": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Custom CSS styles to be applied to the form.",
|
||||
"example": "#submit_form_button { background-color: #d9d9d9; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Callback
|
||||
|
||||
```json
|
||||
{
|
||||
"@init": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on initializing the form component.",
|
||||
"example": "onFormLoad"
|
||||
},
|
||||
"@load": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called on loading the form data.",
|
||||
"example": "onFormLoad"
|
||||
},
|
||||
"@complete": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called after the form completion.",
|
||||
"example": "onFormComplete"
|
||||
},
|
||||
"@decline": {
|
||||
"type": "function",
|
||||
"required": false,
|
||||
"description": "Function to be called after the form decline.",
|
||||
"example": "onFormDecline"
|
||||
}
|
||||
}
|
||||
```
|
||||
+7568
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,247 @@
|
||||
# Form Webhook
|
||||
|
||||
During the form filling and signing process, 3 types of events may occur and are dispatched at different stages:
|
||||
|
||||
- **'form.viewed'** event is triggered when the submitter first opens the form.
|
||||
- **'form.started'** event is triggered when the submitter initiates filling out the form.
|
||||
- **'form.completed'** event is triggered upon successful form completion and signing by one of the parties.
|
||||
- **'form.declined'** event is triggered when a signer declines the submission.
|
||||
|
||||
It's important to note that each of these events contain information available at the time of dispatch, so some data may be missing or incomplete depending on the specific event. Failed webhook requests (4xx, 5xx) are automatically retried multiple times within 48 hours (every 2^attempt minutes) for all production accounts.
|
||||
**Related Guides**
|
||||
[Download Signed Documents](https://www.docuseal.com/guides/download-signed-documents)
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": {
|
||||
"type": "string",
|
||||
"description": "The event type.",
|
||||
"enum": [
|
||||
"form.viewed",
|
||||
"form.started",
|
||||
"form.completed"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "The event timestamp.",
|
||||
"example": "2023-09-24T11:20:42Z",
|
||||
"format": "date-time"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Submitted data object.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The submitter's unique identifier."
|
||||
},
|
||||
"submission_id": {
|
||||
"type": "number",
|
||||
"description": "The unique submission identifier."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The submitter's email address",
|
||||
"format": "email",
|
||||
"example": "john.doe@example.com"
|
||||
},
|
||||
"ua": {
|
||||
"type": "string",
|
||||
"description": "The user agent string that provides information about the submitter's web browser.",
|
||||
"example": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"description": "The submitter's IP address."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The submitter's name."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"description": "The submitter's phone number, formatted according to the E.164 standard.",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "The submitter's role name or title.",
|
||||
"example": "First Party"
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify submitter within your app."
|
||||
},
|
||||
"application_key": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify submitter within your app. Backward compatibility with the previous version of the API. Use external_id instead."
|
||||
},
|
||||
"decline_reason": {
|
||||
"type": "string",
|
||||
"description": "Submitter provided decline message."
|
||||
},
|
||||
"sent_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The submitter status.",
|
||||
"enum": [
|
||||
"completed",
|
||||
"declined",
|
||||
"opened",
|
||||
"sent",
|
||||
"awaiting"
|
||||
]
|
||||
},
|
||||
"opened_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"declined_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"submission": {
|
||||
"type": "object",
|
||||
"description": "The submission details.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The submission's unique identifier."
|
||||
},
|
||||
"audit_log_url": {
|
||||
"type": "string",
|
||||
"description": "The audit log PDF URL. Available only if the submission was completed by all submitters."
|
||||
},
|
||||
"combined_document_url": {
|
||||
"type": "string",
|
||||
"description": "The URL of the combined documents with audit log. Combined documents can be enabled via <a href=\"https://docuseal.com/settings/account\" target=\"_blank\" class=\"link\">/settings/accounts</a>."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The submission status.",
|
||||
"enum": [
|
||||
"completed",
|
||||
"declined",
|
||||
"expired",
|
||||
"pending"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The submission URL."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "The submission creation date.",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "Base template details.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The template's unique identifier."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The template's name."
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify template within your app."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "Template folder name."
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"send_email": {
|
||||
"type": "boolean",
|
||||
"description": "The flag indicating whether the submitter has opted to receive an email."
|
||||
},
|
||||
"send_sms": {
|
||||
"type": "boolean",
|
||||
"description": "The flag indicating whether the submitter has opted to receive an SMS."
|
||||
}
|
||||
}
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"description": "List of the filled values passed by the submitter.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"description": "The field name."
|
||||
},
|
||||
"values": {
|
||||
"type": "string",
|
||||
"description": "The field value."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata object with additional submitter information."
|
||||
},
|
||||
"audit_log_url": {
|
||||
"type": "string",
|
||||
"description": "The audit log PDF URL. Available only if the submission was completed by all submitters."
|
||||
},
|
||||
"submission_url": {
|
||||
"type": "string",
|
||||
"description": "The submission URL."
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The document file name."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The document file URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,317 @@
|
||||
# Submission Webhook
|
||||
|
||||
Get submission creation, completion, expiration, and archiving notifications using these events:
|
||||
|
||||
- **'submission.created'** event is triggered when the submission is created.
|
||||
- **'submission.completed'** event is triggered when the submission is completed by all signing parties.
|
||||
- **'submission.expired'** event is triggered when the submission expires.
|
||||
- **'submission.archived'** event is triggered when the submission is archived.
|
||||
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": {
|
||||
"type": "string",
|
||||
"description": "The event type.",
|
||||
"enum": [
|
||||
"submission.created",
|
||||
"submission.archived"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "The event timestamp.",
|
||||
"example": "2023-09-24T11:20:42Z",
|
||||
"format": "date-time"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Submitted data object.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The submission's unique identifier."
|
||||
},
|
||||
"archived_at": {
|
||||
"type": "string",
|
||||
"description": "The submission archive date."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "The submission creation date."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"description": "The submission update date."
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "The submission source.",
|
||||
"enum": [
|
||||
"invite",
|
||||
"bulk",
|
||||
"api",
|
||||
"embed",
|
||||
"link"
|
||||
]
|
||||
},
|
||||
"submitters_order": {
|
||||
"type": "string",
|
||||
"description": "The submitters order.",
|
||||
"enum": [
|
||||
"random",
|
||||
"preserved"
|
||||
]
|
||||
},
|
||||
"audit_log_url": {
|
||||
"type": "string",
|
||||
"description": "Audit log file URL."
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"description": "The list of submitters for the submission.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The submitter's unique identifier."
|
||||
},
|
||||
"submission_id": {
|
||||
"type": "number",
|
||||
"description": "The unique submission identifier."
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The submitter UUID."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The email address of the submitter.",
|
||||
"format": "email",
|
||||
"example": "john.doe@example.com"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "The unique slug of the document template."
|
||||
},
|
||||
"sent_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the signing request was sent to the submitter."
|
||||
},
|
||||
"opened_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter opened the signing form."
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter completed the signing form."
|
||||
},
|
||||
"declined_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter declined the signing form."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter was created."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter was last updated."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the submitter."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"description": "The phone number of the submitter, formatted according to the E.164 standard.",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "The role name or title of the submitter.",
|
||||
"example": "First Party"
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this submitter within your app."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata object with additional submitter information.",
|
||||
"example": "{ 'customField': 'value' }"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The submitter status.",
|
||||
"enum": [
|
||||
"completed",
|
||||
"declined",
|
||||
"opened",
|
||||
"sent",
|
||||
"awaiting"
|
||||
]
|
||||
},
|
||||
"application_key": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this submitter within your app."
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"description": "An object with pre-filled values for the submission. Use field names for keys of the object. For more configurations see `fields` param."
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"description": "The list of documents for the submission.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The document file name."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The document file URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"type": "object",
|
||||
"description": "The submitter preferences."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "Base template details.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The template's unique identifier."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The template's name."
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify template within your app."
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "The folder name."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the template was created."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the template was last updated."
|
||||
}
|
||||
}
|
||||
},
|
||||
"created_by_user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the user who created the submission."
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string",
|
||||
"description": "The first name of the user who created the submission."
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"description": "The last name of the user who created the submission."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The email address of the user who created the submission."
|
||||
}
|
||||
}
|
||||
},
|
||||
"submission_events": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Submission event unique ID number."
|
||||
},
|
||||
"submitter_id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the submitter that triggered the event."
|
||||
},
|
||||
"event_type": {
|
||||
"type": "string",
|
||||
"description": "Event type.",
|
||||
"enum": [
|
||||
"send_email",
|
||||
"bounce_email",
|
||||
"complaint_email",
|
||||
"send_reminder_email",
|
||||
"send_sms",
|
||||
"send_2fa_sms",
|
||||
"open_email",
|
||||
"click_email",
|
||||
"click_sms",
|
||||
"phone_verified",
|
||||
"start_form",
|
||||
"start_verification",
|
||||
"complete_verification",
|
||||
"view_form",
|
||||
"invite_party",
|
||||
"complete_form",
|
||||
"decline_form",
|
||||
"api_complete_form"
|
||||
]
|
||||
},
|
||||
"event_timestamp": {
|
||||
"type": "string",
|
||||
"description": "Date and time when the event was triggered."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Document name."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Document URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "The status of the submission.",
|
||||
"enum": [
|
||||
"completed",
|
||||
"declined",
|
||||
"expired",
|
||||
"pending"
|
||||
]
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submission was fully completed."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,235 @@
|
||||
# Template Webhook
|
||||
|
||||
Get template creation and update notifications using these events:
|
||||
|
||||
- **'template.created'** is triggered when the template is created.
|
||||
- **'tempate.updated'** is triggered when the template is updated.
|
||||
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": {
|
||||
"type": "string",
|
||||
"description": "The event type.",
|
||||
"enum": [
|
||||
"template.created",
|
||||
"template.updated"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "The event timestamp.",
|
||||
"example": "2023-09-24T11:20:42Z",
|
||||
"format": "date-time"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Submitted data object.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The template's unique identifier."
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "The template's unique slug."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The template's name."
|
||||
},
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"description": "The template document files.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attachment_uuid": {
|
||||
"type": "string",
|
||||
"description": "The attachment UUID."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The attachment name."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"description": "The template fields.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The field UUID."
|
||||
},
|
||||
"submitter_uuid": {
|
||||
"type": "string",
|
||||
"description": "The submitter role UUID."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The field name."
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"description": "The flag indicating whether the field is required."
|
||||
},
|
||||
"preferences": {
|
||||
"type": "object",
|
||||
"description": "The field preferences."
|
||||
},
|
||||
"areas": {
|
||||
"type": "array",
|
||||
"description": "List of areas where the field is located in the document.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "X coordinate of the area where the field is located in the document."
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": "Y coordinate of the area where the field is located in the document."
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"description": "Width of the area where the field is located in the document."
|
||||
},
|
||||
"h": {
|
||||
"type": "number",
|
||||
"description": "Height of the area where the field is located in the document."
|
||||
},
|
||||
"attachment_uuid": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier of the attached document where the field is located."
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number of the attached document where the field is located."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Submitter name."
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier of the submitter."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"author_id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the author of the template."
|
||||
},
|
||||
"account_id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the account of the template."
|
||||
},
|
||||
"archived_at": {
|
||||
"type": "string",
|
||||
"description": "Date and time when the template was archived."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "Date and time when the template was created."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"description": "Date and time when the template was updated."
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Source of the template.",
|
||||
"enum": [
|
||||
"native",
|
||||
"api",
|
||||
"embed"
|
||||
]
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Identifier of the template in the external system."
|
||||
},
|
||||
"folder_id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the folder where the template is placed."
|
||||
},
|
||||
"folder_name": {
|
||||
"type": "string",
|
||||
"description": "Folder name where the template is placed."
|
||||
},
|
||||
"application_key": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify tempate_id within your app."
|
||||
},
|
||||
"author": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the author."
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string",
|
||||
"description": "First name of the author."
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"description": "Last name of the author."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Author email."
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"description": "List of documents attached to the template.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the document."
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier of the document."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL of the document."
|
||||
},
|
||||
"preview_image_url": {
|
||||
"type": "string",
|
||||
"description": "Document preview image URL."
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Document filename."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -9,7 +9,11 @@ module AccountConfigs
|
||||
'twelve_hours' => '12 hours',
|
||||
'twenty_four_hours' => '24 hours',
|
||||
'two_days' => '2 days',
|
||||
'three_days' => '3 days',
|
||||
'four_days' => '4 days',
|
||||
'five_days' => '5 days',
|
||||
'six_days' => '6 days',
|
||||
'seven_days' => '7 days',
|
||||
'eight_days' => '8 days',
|
||||
'fifteen_days' => '15 days'
|
||||
}.freeze
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module LoadBmp
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(bmp_bytes)
|
||||
bmp_bytes = bmp_bytes.b
|
||||
|
||||
header_data = parse_bmp_headers(bmp_bytes)
|
||||
|
||||
raw_pixel_data_from_file = extract_raw_pixel_data_blob(
|
||||
bmp_bytes,
|
||||
header_data[:pixel_data_offset],
|
||||
header_data[:bmp_stride],
|
||||
header_data[:height]
|
||||
)
|
||||
|
||||
final_pixel_data = prepare_unpadded_pixel_data_string(
|
||||
raw_pixel_data_from_file,
|
||||
header_data[:bpp],
|
||||
header_data[:width],
|
||||
header_data[:height],
|
||||
header_data[:bmp_stride]
|
||||
)
|
||||
|
||||
bands = header_data[:bpp] / 8
|
||||
|
||||
unless header_data[:bpp] == 24 || header_data[:bpp] == 32
|
||||
raise ArgumentError, "Conversion for #{header_data[:bpp]}-bpp BMP not implemented."
|
||||
end
|
||||
|
||||
image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
|
||||
|
||||
image = image.flip(:vertical) if header_data[:orientation] == -1
|
||||
|
||||
image_rgb =
|
||||
if bands == 3
|
||||
image.recomb(band3_recomb)
|
||||
elsif bands == 4
|
||||
image.recomb(band4_recomb)
|
||||
end
|
||||
|
||||
image_rgb = image_rgb.copy(interpretation: :srgb) if image_rgb.interpretation != :srgb
|
||||
|
||||
image_rgb
|
||||
end
|
||||
|
||||
def parse_bmp_headers(bmp_bytes)
|
||||
raise ArgumentError, 'BMP data too short for file header (14 bytes).' if bmp_bytes.bytesize < 14
|
||||
|
||||
signature, pixel_data_offset = bmp_bytes.unpack('a2@10L<')
|
||||
|
||||
raise ArgumentError, "Not a valid BMP file (invalid signature 'BM')." if signature != 'BM'
|
||||
|
||||
raise ArgumentError, 'BMP data too short for info header size field (4 bytes).' if bmp_bytes.bytesize < (14 + 4)
|
||||
|
||||
info_header_size = bmp_bytes.unpack1('@14L<')
|
||||
|
||||
min_expected_info_header_size = 40
|
||||
|
||||
if info_header_size < min_expected_info_header_size
|
||||
raise ArgumentError,
|
||||
"Unsupported BMP info header size: #{info_header_size}. Expected at least #{min_expected_info_header_size}."
|
||||
end
|
||||
|
||||
header_and_info_header_min_bytes = 14 + min_expected_info_header_size
|
||||
|
||||
if bmp_bytes.bytesize < header_and_info_header_min_bytes
|
||||
raise ArgumentError,
|
||||
'BMP data too short for essential BITMAPINFOHEADER fields ' \
|
||||
"(requires #{header_and_info_header_min_bytes} bytes total)."
|
||||
end
|
||||
|
||||
_header_size_check, width, raw_height_from_header, planes, bpp, compression =
|
||||
bmp_bytes.unpack('@14L<l<l<S<S<L<')
|
||||
|
||||
height = 0
|
||||
orientation = -1
|
||||
|
||||
if raw_height_from_header.negative?
|
||||
height = -raw_height_from_header
|
||||
orientation = 1
|
||||
else
|
||||
height = raw_height_from_header
|
||||
end
|
||||
|
||||
raise ArgumentError, 'BMP width must be positive.' if width <= 0
|
||||
raise ArgumentError, 'BMP height must be positive.' if height <= 0
|
||||
|
||||
if compression != 0
|
||||
raise ArgumentError,
|
||||
"Unsupported BMP compression type: #{compression}. Only uncompressed (0) is supported."
|
||||
end
|
||||
|
||||
unless [24, 32].include?(bpp)
|
||||
raise ArgumentError, "Unsupported BMP bits per pixel: #{bpp}. Only 24-bit and 32-bit are supported."
|
||||
end
|
||||
|
||||
raise ArgumentError, "Unsupported BMP planes: #{planes}. Expected 1." if planes != 1
|
||||
|
||||
bytes_per_pixel = bpp / 8
|
||||
row_size_unpadded = width * bytes_per_pixel
|
||||
bmp_stride = (row_size_unpadded + 3) & ~3
|
||||
|
||||
{
|
||||
width:,
|
||||
height:,
|
||||
bpp:,
|
||||
pixel_data_offset:,
|
||||
bmp_stride:,
|
||||
orientation:
|
||||
}
|
||||
end
|
||||
|
||||
def extract_raw_pixel_data_blob(bmp_bytes, pixel_data_offset, bmp_stride, height)
|
||||
expected_pixel_data_size = bmp_stride * height
|
||||
|
||||
if pixel_data_offset + expected_pixel_data_size > bmp_bytes.bytesize
|
||||
actual_available = bmp_bytes.bytesize - pixel_data_offset
|
||||
actual_available = 0 if actual_available.negative?
|
||||
raise ArgumentError,
|
||||
"Pixel data segment (offset #{pixel_data_offset}, expected size #{expected_pixel_data_size}) " \
|
||||
"exceeds BMP file size (#{bmp_bytes.bytesize}). " \
|
||||
"Only #{actual_available} bytes available after offset."
|
||||
end
|
||||
|
||||
raw_pixel_data_from_file = bmp_bytes.byteslice(pixel_data_offset, expected_pixel_data_size)
|
||||
|
||||
if raw_pixel_data_from_file.nil? || raw_pixel_data_from_file.bytesize < expected_pixel_data_size
|
||||
raise ArgumentError,
|
||||
"Extracted pixel data is smaller (#{raw_pixel_data_from_file&.bytesize || 0} bytes) " \
|
||||
"than expected (#{expected_pixel_data_size} bytes based on stride and height)."
|
||||
end
|
||||
|
||||
raw_pixel_data_from_file
|
||||
end
|
||||
|
||||
def prepare_unpadded_pixel_data_string(raw_pixel_data_from_file, bpp, width, height, bmp_stride)
|
||||
bytes_per_pixel = bpp / 8
|
||||
actual_row_width_bytes = width * bytes_per_pixel
|
||||
|
||||
unpadded_rows = Array.new(height)
|
||||
current_offset_in_blob = 0
|
||||
|
||||
height.times do |i|
|
||||
if current_offset_in_blob + actual_row_width_bytes > raw_pixel_data_from_file.bytesize
|
||||
raise ArgumentError,
|
||||
"Not enough data in pixel blob for row #{i}. Offset #{current_offset_in_blob}, " \
|
||||
"row width #{actual_row_width_bytes}, blob size #{raw_pixel_data_from_file.bytesize}"
|
||||
end
|
||||
|
||||
unpadded_row_slice = raw_pixel_data_from_file.byteslice(current_offset_in_blob, actual_row_width_bytes)
|
||||
|
||||
if unpadded_row_slice.nil? || unpadded_row_slice.bytesize < actual_row_width_bytes
|
||||
raise ArgumentError, "Failed to slice a full unpadded row from pixel data blob for row #{i}."
|
||||
end
|
||||
|
||||
unpadded_rows[i] = unpadded_row_slice
|
||||
current_offset_in_blob += bmp_stride
|
||||
end
|
||||
|
||||
unpadded_rows.join
|
||||
end
|
||||
|
||||
def band3_recomb
|
||||
@band3_recomb ||=
|
||||
Vips::Image.new_from_array(
|
||||
[
|
||||
[0, 0, 1],
|
||||
[0, 1, 0],
|
||||
[1, 0, 0]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def band4_recomb
|
||||
@band4_recomb ||= Vips::Image.new_from_array(
|
||||
[
|
||||
[0, 0, 1, 0],
|
||||
[0, 1, 0, 0],
|
||||
[1, 0, 0, 0]
|
||||
]
|
||||
)
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module LoadIco
|
||||
BI_RGB = 0
|
||||
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(ico_bytes)
|
||||
io = StringIO.new(ico_bytes)
|
||||
_reserved, type, count = io.read(6)&.unpack('S<S<S<')
|
||||
|
||||
raise ArgumentError, 'Unable to load' unless type == 1 && count&.positive?
|
||||
|
||||
ico_entries_parsed = []
|
||||
|
||||
count.times do
|
||||
entry_bytes = io.read(16)
|
||||
|
||||
raise ArgumentError, 'Unable to load' unless entry_bytes && entry_bytes.bytesize == 16
|
||||
|
||||
width_byte, height_byte, _num_colors_palette, _rsvd_entry, _planes_icon_entry, bpp_icon_entry,
|
||||
img_data_size, img_data_offset = entry_bytes.unpack('CCCCS<S<L<L<')
|
||||
|
||||
width = width_byte.zero? ? 256 : width_byte
|
||||
height = height_byte.zero? ? 256 : height_byte
|
||||
sort_bpp = bpp_icon_entry.zero? ? 32 : bpp_icon_entry
|
||||
|
||||
ico_entries_parsed << {
|
||||
width: width, height: height,
|
||||
sort_bpp: sort_bpp,
|
||||
size: img_data_size, offset: img_data_offset
|
||||
}
|
||||
end
|
||||
|
||||
best_entry = ico_entries_parsed.min_by { |e| [-e[:width] * e[:height], -e[:sort_bpp]] }
|
||||
|
||||
raise ArgumentError, 'Unable to load' unless best_entry
|
||||
|
||||
io.seek(best_entry[:offset])
|
||||
image_data_bytes = io.read(best_entry[:size])
|
||||
|
||||
raise ArgumentError, 'Unable to load' unless image_data_bytes && image_data_bytes.bytesize == best_entry[:size]
|
||||
|
||||
image = load_image_entry(image_data_bytes, best_entry[:width], best_entry[:height])
|
||||
|
||||
raise ArgumentError, 'Unable to load' unless image
|
||||
|
||||
image
|
||||
end
|
||||
|
||||
def load_image_entry(image_data_bytes, ico_entry_width, ico_entry_height)
|
||||
dib_io = StringIO.new(image_data_bytes)
|
||||
|
||||
dib_header_size_arr = dib_io.read(4)&.unpack('L<')
|
||||
return nil unless dib_header_size_arr
|
||||
|
||||
dib_header_size = dib_header_size_arr.first
|
||||
return nil unless dib_header_size && dib_header_size >= 40
|
||||
|
||||
dib_params_bytes = dib_io.read(36)
|
||||
return nil unless dib_params_bytes && dib_params_bytes.bytesize == 36
|
||||
|
||||
dib_width, dib_actual_height_field, dib_planes, dib_bpp,
|
||||
dib_compression, _dib_image_size, _xpels, _ypels,
|
||||
dib_clr_used, _dib_clr_important = dib_params_bytes.unpack('l<l<S<S<L<L<l<l<L<L<')
|
||||
|
||||
return nil unless dib_width && dib_actual_height_field && dib_planes && dib_bpp && dib_compression && dib_clr_used
|
||||
return nil unless dib_width == ico_entry_width
|
||||
|
||||
image_pixel_height = ico_entry_height
|
||||
|
||||
expected_dib_height_no_mask = image_pixel_height
|
||||
expected_dib_height_with_mask = image_pixel_height * 2
|
||||
actual_dib_pixel_rows_abs = dib_actual_height_field.abs
|
||||
|
||||
unless actual_dib_pixel_rows_abs == expected_dib_height_no_mask ||
|
||||
actual_dib_pixel_rows_abs == expected_dib_height_with_mask
|
||||
return nil
|
||||
end
|
||||
|
||||
return nil unless dib_planes == 1
|
||||
return nil unless dib_compression == BI_RGB
|
||||
return nil unless [1, 4, 8, 24, 32].include?(dib_bpp)
|
||||
|
||||
has_and_mask = (actual_dib_pixel_rows_abs == expected_dib_height_with_mask) && (dib_bpp < 32)
|
||||
|
||||
dib_io.seek(dib_header_size, IO::SEEK_SET)
|
||||
|
||||
palette = []
|
||||
if dib_bpp <= 8
|
||||
num_palette_entries = dib_clr_used.zero? ? (1 << dib_bpp) : dib_clr_used
|
||||
num_palette_entries.times do
|
||||
palette_color_bytes = dib_io.read(4)
|
||||
return nil unless palette_color_bytes && palette_color_bytes.bytesize == 4
|
||||
|
||||
b, g, r, _a_reserved = palette_color_bytes.unpack('CCCC')
|
||||
palette << [r, g, b, 255]
|
||||
end
|
||||
end
|
||||
|
||||
xor_mask_data_offset = dib_io.pos
|
||||
xor_scanline_stride = (((dib_width * dib_bpp) + 31) / 32) * 4
|
||||
|
||||
and_mask_data_offset = 0
|
||||
and_scanline_stride = 0
|
||||
if has_and_mask
|
||||
and_mask_data_offset = xor_mask_data_offset + (image_pixel_height * xor_scanline_stride)
|
||||
and_scanline_stride = (((dib_width * 1) + 31) / 32) * 4
|
||||
end
|
||||
|
||||
flat_rgba_pixels = []
|
||||
|
||||
(0...image_pixel_height).each do |y_row|
|
||||
y_dib_row = image_pixel_height - 1 - y_row
|
||||
|
||||
dib_io.seek(xor_mask_data_offset + (y_dib_row * xor_scanline_stride))
|
||||
xor_scanline_bytes = dib_io.read(xor_scanline_stride)
|
||||
min_xor_bytes_needed = ((dib_width * dib_bpp) + 7) / 8
|
||||
return nil unless xor_scanline_bytes && xor_scanline_bytes.bytesize >= min_xor_bytes_needed
|
||||
|
||||
and_mask_bits_for_row = []
|
||||
if has_and_mask
|
||||
dib_io.seek(and_mask_data_offset + (y_dib_row * and_scanline_stride))
|
||||
and_mask_scanline_bytes = dib_io.read(and_scanline_stride)
|
||||
min_and_bytes_needed = ((dib_width * 1) + 7) / 8
|
||||
return nil unless and_mask_scanline_bytes && and_mask_scanline_bytes.bytesize >= min_and_bytes_needed
|
||||
|
||||
(0...dib_width).each do |x_pixel|
|
||||
byte_index = x_pixel / 8
|
||||
bit_index_in_byte = 7 - (x_pixel % 8)
|
||||
byte_val = and_mask_scanline_bytes.getbyte(byte_index)
|
||||
and_mask_bits_for_row << ((byte_val >> bit_index_in_byte) & 1)
|
||||
end
|
||||
end
|
||||
|
||||
(0...dib_width).each do |x_pixel|
|
||||
r = 0
|
||||
g = 0
|
||||
b = 0
|
||||
a = 255
|
||||
|
||||
case dib_bpp
|
||||
when 32
|
||||
offset = x_pixel * 4
|
||||
blue = xor_scanline_bytes.getbyte(offset)
|
||||
green = xor_scanline_bytes.getbyte(offset + 1)
|
||||
red = xor_scanline_bytes.getbyte(offset + 2)
|
||||
alpha_val = xor_scanline_bytes.getbyte(offset + 3)
|
||||
r = red
|
||||
g = green
|
||||
b = blue
|
||||
a = alpha_val
|
||||
when 24
|
||||
offset = x_pixel * 3
|
||||
blue = xor_scanline_bytes.getbyte(offset)
|
||||
green = xor_scanline_bytes.getbyte(offset + 1)
|
||||
red = xor_scanline_bytes.getbyte(offset + 2)
|
||||
r = red
|
||||
g = green
|
||||
b = blue
|
||||
when 8
|
||||
idx = xor_scanline_bytes.getbyte(x_pixel)
|
||||
r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
|
||||
r = r_p
|
||||
g = g_p
|
||||
b = b_p
|
||||
a = a_p
|
||||
when 4
|
||||
byte_val = xor_scanline_bytes.getbyte(x_pixel / 2)
|
||||
idx = (x_pixel.even? ? (byte_val >> 4) : (byte_val & 0x0F))
|
||||
r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
|
||||
r = r_p
|
||||
g = g_p
|
||||
b = b_p
|
||||
a = a_p
|
||||
when 1
|
||||
byte_val = xor_scanline_bytes.getbyte(x_pixel / 8)
|
||||
idx = (byte_val >> (7 - (x_pixel % 8))) & 1
|
||||
r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0]
|
||||
r = r_p
|
||||
g = g_p
|
||||
b = b_p
|
||||
a = a_p
|
||||
end
|
||||
|
||||
if has_and_mask && !and_mask_bits_for_row.empty?
|
||||
a = and_mask_bits_for_row[x_pixel] == 1 ? 0 : 255
|
||||
end
|
||||
flat_rgba_pixels.push(r, g, b, a)
|
||||
end
|
||||
end
|
||||
|
||||
pixel_data_string = flat_rgba_pixels.pack('C*')
|
||||
|
||||
expected_bytes = dib_width * image_pixel_height * 4
|
||||
|
||||
return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive?
|
||||
|
||||
Vips::Image.new_from_memory(
|
||||
pixel_data_string,
|
||||
dib_width,
|
||||
image_pixel_height,
|
||||
4,
|
||||
:uchar
|
||||
)
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
@@ -72,8 +72,8 @@ module Params
|
||||
return if params[key].blank?
|
||||
return if params[key].to_s.include?('<')
|
||||
|
||||
if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank
|
||||
.all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) }
|
||||
if params[key].to_s.strip.split(%r{\s*[;,/]\s*}).compact_blank
|
||||
.all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) || email.include?('--') }
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Pdfium
|
||||
extend FFI::Library
|
||||
|
||||
LIB_NAME = 'pdfium'
|
||||
|
||||
begin
|
||||
ffi_lib case FFI::Platform::OS
|
||||
when 'darwin'
|
||||
[
|
||||
"lib#{LIB_NAME}.dylib",
|
||||
'/Applications/LibreOffice.app/Contents/Frameworks/libpdfiumlo.dylib'
|
||||
]
|
||||
else
|
||||
"lib#{LIB_NAME}.so"
|
||||
end
|
||||
rescue LoadError => e
|
||||
raise "Could not load libpdfium library. Make sure it's installed and in your library path. Error: #{e.message}"
|
||||
end
|
||||
|
||||
typedef :pointer, :FPDF_STRING
|
||||
typedef :pointer, :FPDF_DOCUMENT
|
||||
typedef :pointer, :FPDF_PAGE
|
||||
typedef :pointer, :FPDF_BITMAP
|
||||
typedef :pointer, :FPDF_FORMHANDLE
|
||||
|
||||
MAX_SIZE = 32_767
|
||||
|
||||
FPDF_ANNOT = 0x01
|
||||
FPDF_LCD_TEXT = 0x02
|
||||
FPDF_NO_NATIVETEXT = 0x04
|
||||
FPDF_GRAYSCALE = 0x08
|
||||
FPDF_REVERSE_BYTE_ORDER = 0x10
|
||||
FPDF_RENDER_LIMITEDIMAGECACHE = 0x200
|
||||
FPDF_RENDER_FORCEHALFTONE = 0x400
|
||||
FPDF_PRINTING = 0x800
|
||||
|
||||
# rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
class FPDF_LIBRARY_CONFIG < FFI::Struct
|
||||
layout :version, :int,
|
||||
:m_pUserFontPaths, :pointer,
|
||||
:m_pIsolate, :pointer,
|
||||
:m_v8EmbedderSlot, :uint,
|
||||
:m_pPlatform, :pointer,
|
||||
:m_RendererType, :int
|
||||
end
|
||||
# rubocop:enable Naming/ClassAndModuleCamelCase
|
||||
|
||||
attach_function :FPDF_InitLibraryWithConfig, [:pointer], :void
|
||||
attach_function :FPDF_DestroyLibrary, [], :void
|
||||
|
||||
attach_function :FPDF_LoadDocument, %i[string FPDF_STRING], :FPDF_DOCUMENT
|
||||
attach_function :FPDF_LoadMemDocument, %i[pointer int FPDF_STRING], :FPDF_DOCUMENT
|
||||
attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void
|
||||
attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int
|
||||
attach_function :FPDF_GetLastError, [], :ulong
|
||||
|
||||
attach_function :FPDF_LoadPage, %i[FPDF_DOCUMENT int], :FPDF_PAGE
|
||||
attach_function :FPDF_ClosePage, [:FPDF_PAGE], :void
|
||||
attach_function :FPDF_GetPageWidthF, [:FPDF_PAGE], :float
|
||||
attach_function :FPDF_GetPageHeightF, [:FPDF_PAGE], :float
|
||||
|
||||
attach_function :FPDFBitmap_Create, %i[int int int], :FPDF_BITMAP
|
||||
attach_function :FPDFBitmap_CreateEx, %i[int int int pointer int], :FPDF_BITMAP
|
||||
attach_function :FPDFBitmap_Destroy, [:FPDF_BITMAP], :void
|
||||
attach_function :FPDFBitmap_GetBuffer, [:FPDF_BITMAP], :pointer
|
||||
attach_function :FPDFBitmap_GetWidth, [:FPDF_BITMAP], :int
|
||||
attach_function :FPDFBitmap_GetHeight, [:FPDF_BITMAP], :int
|
||||
attach_function :FPDFBitmap_GetStride, [:FPDF_BITMAP], :int
|
||||
attach_function :FPDFBitmap_FillRect, %i[FPDF_BITMAP int int int int ulong], :void
|
||||
|
||||
attach_function :FPDF_RenderPageBitmap, %i[FPDF_BITMAP FPDF_PAGE int int int int int int], :void
|
||||
|
||||
typedef :int, :FPDF_BOOL
|
||||
typedef :pointer, :IPDF_JSPLATFORM
|
||||
|
||||
# rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
class FPDF_FORMFILLINFO_V2 < FFI::Struct
|
||||
layout :version, :int,
|
||||
:Release, :pointer,
|
||||
:FFI_Invalidate, :pointer,
|
||||
:FFI_OutputSelectedRect, :pointer,
|
||||
:FFI_SetCursor, :pointer,
|
||||
:FFI_SetTimer, :pointer,
|
||||
:FFI_KillTimer, :pointer,
|
||||
:FFI_GetLocalTime, :pointer,
|
||||
:FFI_OnChange, :pointer,
|
||||
:FFI_GetPage, :pointer,
|
||||
:FFI_GetCurrentPage, :pointer,
|
||||
:FFI_GetRotation, :pointer,
|
||||
:FFI_ExecuteNamedAction, :pointer,
|
||||
:FFI_SetTextFieldFocus, :pointer,
|
||||
:FFI_DoURIAction, :pointer,
|
||||
:FFI_DoGoToAction, :pointer,
|
||||
:m_pJsPlatform, :IPDF_JSPLATFORM,
|
||||
:xfa_disabled, :FPDF_BOOL,
|
||||
:FFI_DisplayCaret, :pointer,
|
||||
:FFI_GetCurrentPageIndex, :pointer,
|
||||
:FFI_SetCurrentPage, :pointer,
|
||||
:FFI_GotoURL, :pointer,
|
||||
:FFI_GetPageViewRect, :pointer,
|
||||
:FFI_PageEvent, :pointer,
|
||||
:FFI_PopupMenu, :pointer,
|
||||
:FFI_OpenFile, :pointer,
|
||||
:FFI_EmailTo, :pointer,
|
||||
:FFI_UploadTo, :pointer,
|
||||
:FFI_GetPlatform, :pointer,
|
||||
:FFI_GetLanguage, :pointer,
|
||||
:FFI_DownloadFromURL, :pointer,
|
||||
:FFI_PostRequestURL, :pointer,
|
||||
:FFI_PutRequestURL, :pointer,
|
||||
:FFI_OnFocusChange, :pointer,
|
||||
:FFI_DoURIActionWithKeyboardModifier, :pointer
|
||||
end
|
||||
# rubocop:enable Naming/ClassAndModuleCamelCase
|
||||
|
||||
attach_function :FPDFDOC_InitFormFillEnvironment, %i[FPDF_DOCUMENT pointer], :FPDF_FORMHANDLE
|
||||
attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void
|
||||
attach_function :FPDF_FFLDraw, %i[FPDF_FORMHANDLE FPDF_BITMAP FPDF_PAGE int int int int int int], :void
|
||||
|
||||
FPDF_ERR_SUCCESS = 0
|
||||
FPDF_ERR_UNKNOWN = 1
|
||||
FPDF_ERR_FILE = 2
|
||||
FPDF_ERR_FORMAT = 3
|
||||
FPDF_ERR_PASSWORD = 4
|
||||
FPDF_ERR_SECURITY = 5
|
||||
FPDF_ERR_PAGE = 6
|
||||
|
||||
PDFIUM_ERRORS = {
|
||||
FPDF_ERR_SUCCESS => 'Success',
|
||||
FPDF_ERR_UNKNOWN => 'Unknown error',
|
||||
FPDF_ERR_FILE => 'Error open file',
|
||||
FPDF_ERR_FORMAT => 'Invalid format',
|
||||
FPDF_ERR_PASSWORD => 'Incorrect password',
|
||||
FPDF_ERR_SECURITY => 'Security scheme error',
|
||||
FPDF_ERR_PAGE => 'Page not found'
|
||||
}.freeze
|
||||
|
||||
class PdfiumError < StandardError; end
|
||||
|
||||
def self.error_message(code)
|
||||
PDFIUM_ERRORS[code] || "Unknown error code: #{code}"
|
||||
end
|
||||
|
||||
def self.check_last_error(context_message = 'PDFium operation failed')
|
||||
error_code = FPDF_GetLastError()
|
||||
|
||||
return if error_code == FPDF_ERR_SUCCESS
|
||||
|
||||
raise PdfiumError, "#{context_message}: #{error_message(error_code)} (Code: #{error_code})"
|
||||
end
|
||||
|
||||
class Document
|
||||
attr_reader :document_ptr, :form_handle
|
||||
|
||||
def initialize(document_ptr, source_buffer = nil)
|
||||
raise ArgumentError, 'document_ptr cannot be nil' if document_ptr.nil? || document_ptr.null?
|
||||
|
||||
@document_ptr = document_ptr
|
||||
|
||||
@pages = {}
|
||||
@closed = false
|
||||
@source_buffer = source_buffer
|
||||
@form_handle = FFI::Pointer::NULL
|
||||
@form_fill_info_mem = FFI::Pointer::NULL
|
||||
|
||||
init_form_fill_environment
|
||||
end
|
||||
|
||||
def init_form_fill_environment
|
||||
return if @document_ptr.null?
|
||||
|
||||
@form_fill_info_mem = FFI::MemoryPointer.new(FPDF_FORMFILLINFO_V2.size)
|
||||
|
||||
form_fill_info_struct = FPDF_FORMFILLINFO_V2.new(@form_fill_info_mem)
|
||||
form_fill_info_struct[:version] = 2
|
||||
|
||||
@form_handle = Pdfium.FPDFDOC_InitFormFillEnvironment(@document_ptr, @form_fill_info_mem)
|
||||
end
|
||||
|
||||
def page_count
|
||||
@page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr)
|
||||
end
|
||||
|
||||
def self.open_file(file_path, password = nil)
|
||||
doc_ptr = Pdfium.FPDF_LoadDocument(file_path, password)
|
||||
|
||||
if doc_ptr.null?
|
||||
Pdfium.check_last_error("Failed to load document from file '#{file_path}'")
|
||||
|
||||
raise PdfiumError, "Failed to load document from file '#{file_path}', pointer is NULL."
|
||||
end
|
||||
|
||||
doc = new(doc_ptr)
|
||||
|
||||
return doc unless block_given?
|
||||
|
||||
begin
|
||||
yield doc
|
||||
ensure
|
||||
doc.close
|
||||
end
|
||||
end
|
||||
|
||||
def self.open_bytes(bytes, password = nil)
|
||||
buffer = FFI::MemoryPointer.new(:char, bytes.bytesize)
|
||||
buffer.put_bytes(0, bytes)
|
||||
|
||||
doc_ptr = Pdfium.FPDF_LoadMemDocument(buffer, bytes.bytesize, password)
|
||||
|
||||
if doc_ptr.null?
|
||||
Pdfium.check_last_error('Failed to load document from memory')
|
||||
|
||||
raise PdfiumError, 'Failed to load document from memory, pointer is NULL.'
|
||||
end
|
||||
|
||||
doc = new(doc_ptr, buffer)
|
||||
|
||||
return doc unless block_given?
|
||||
|
||||
begin
|
||||
yield doc
|
||||
ensure
|
||||
doc.close
|
||||
end
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def ensure_not_closed!
|
||||
raise PdfiumError, 'Document is closed.' if closed?
|
||||
end
|
||||
|
||||
def get_page(page_index)
|
||||
ensure_not_closed!
|
||||
|
||||
unless page_index.is_a?(Integer) && page_index >= 0 && page_index < page_count
|
||||
raise PdfiumError, "Page index #{page_index} out of range (0..#{page_count - 1})"
|
||||
end
|
||||
|
||||
@pages[page_index] ||= Page.new(self, page_index)
|
||||
end
|
||||
|
||||
def close
|
||||
return if closed?
|
||||
|
||||
@pages.each_value { |page| page.close unless page.closed? }
|
||||
@pages.clear
|
||||
|
||||
unless @form_handle.null?
|
||||
Pdfium.FPDFDOC_ExitFormFillEnvironment(@form_handle)
|
||||
|
||||
@form_handle = FFI::Pointer::NULL
|
||||
end
|
||||
|
||||
if @form_fill_info_mem && !@form_fill_info_mem.null?
|
||||
@form_fill_info_mem.free
|
||||
@form_fill_info_mem = FFI::Pointer::NULL
|
||||
end
|
||||
|
||||
Pdfium.FPDF_CloseDocument(@document_ptr) unless @document_ptr.null?
|
||||
|
||||
@document_ptr = FFI::Pointer::NULL
|
||||
@source_buffer = nil
|
||||
|
||||
@closed = true
|
||||
end
|
||||
end
|
||||
|
||||
class Page
|
||||
attr_reader :document, :page_index, :page_ptr
|
||||
|
||||
def initialize(document, page_index)
|
||||
raise ArgumentError, 'Document object is required' unless document.is_a?(Pdfium::Document)
|
||||
|
||||
@document = document
|
||||
@document.ensure_not_closed!
|
||||
|
||||
@page_index = page_index
|
||||
|
||||
@page_ptr = Pdfium.FPDF_LoadPage(document.document_ptr, page_index)
|
||||
|
||||
if @page_ptr.null?
|
||||
Pdfium.check_last_error("Failed to load page #{page_index}")
|
||||
|
||||
raise PdfiumError, "Failed to load page #{page_index}, pointer is NULL."
|
||||
end
|
||||
|
||||
@closed = false
|
||||
end
|
||||
|
||||
def width
|
||||
@width ||= Pdfium.FPDF_GetPageWidthF(@page_ptr)
|
||||
end
|
||||
|
||||
def height
|
||||
@height ||= Pdfium.FPDF_GetPageHeightF(@page_ptr)
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def form_handle
|
||||
@document.form_handle
|
||||
end
|
||||
|
||||
def ensure_not_closed!
|
||||
raise PdfiumError, 'Page is closed.' if closed?
|
||||
|
||||
@document.ensure_not_closed!
|
||||
end
|
||||
|
||||
def render_to_bitmap(width: nil, height: nil, scale: nil, background_color: 0xFFFFFFFF,
|
||||
flags: FPDF_ANNOT | FPDF_LCD_TEXT | FPDF_NO_NATIVETEXT | FPDF_REVERSE_BYTE_ORDER)
|
||||
ensure_not_closed!
|
||||
|
||||
render_width, render_height = calculate_render_dimensions(width, height, scale)
|
||||
|
||||
bitmap_ptr = Pdfium.FPDFBitmap_Create(render_width, render_height, 1)
|
||||
|
||||
if bitmap_ptr.null?
|
||||
Pdfium.check_last_error('Failed to create bitmap (potential pre-existing error)')
|
||||
|
||||
raise PdfiumError, 'Failed to create bitmap (FPDFBitmap_Create returned NULL)'
|
||||
end
|
||||
|
||||
Pdfium.FPDFBitmap_FillRect(bitmap_ptr, 0, 0, render_width, render_height, background_color)
|
||||
|
||||
Pdfium.FPDF_RenderPageBitmap(bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags)
|
||||
|
||||
Pdfium.check_last_error('Failed to render page to bitmap')
|
||||
|
||||
unless form_handle.null?
|
||||
Pdfium.FPDF_FFLDraw(form_handle, bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags)
|
||||
|
||||
Pdfium.check_last_error('Call to FPDF_FFLDraw completed (check for rendering issues if any)')
|
||||
end
|
||||
|
||||
buffer_ptr = Pdfium.FPDFBitmap_GetBuffer(bitmap_ptr)
|
||||
stride = Pdfium.FPDFBitmap_GetStride(bitmap_ptr)
|
||||
|
||||
bitmap_data = buffer_ptr.read_bytes(stride * render_height)
|
||||
|
||||
[bitmap_data, render_width, render_height]
|
||||
ensure
|
||||
Pdfium.FPDFBitmap_Destroy(bitmap_ptr) if bitmap_ptr && !bitmap_ptr.null?
|
||||
end
|
||||
|
||||
def close
|
||||
return if closed?
|
||||
|
||||
Pdfium.FPDF_ClosePage(@page_ptr) unless @page_ptr.null?
|
||||
|
||||
@page_ptr = FFI::Pointer::NULL
|
||||
|
||||
@closed = true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculate_render_dimensions(width_param, height_param, scale_param)
|
||||
if scale_param
|
||||
render_width = (width * scale_param).round
|
||||
render_height = (height * scale_param).round
|
||||
elsif width_param || height_param
|
||||
if width_param && height_param
|
||||
render_width = width_param
|
||||
render_height = height_param
|
||||
elsif width_param
|
||||
scale_factor = width_param.to_f / width
|
||||
render_width = width_param
|
||||
render_height = (height * scale_factor).round
|
||||
else
|
||||
scale_factor = height_param.to_f / height
|
||||
render_width = (width * scale_factor).round
|
||||
render_height = height_param
|
||||
end
|
||||
else
|
||||
render_width = width.to_i
|
||||
render_height = height.to_i
|
||||
end
|
||||
|
||||
[render_width.clamp(1, MAX_SIZE), render_height.clamp(1, MAX_SIZE)]
|
||||
end
|
||||
end
|
||||
|
||||
def self.initialize_library
|
||||
config_mem = FFI::MemoryPointer.new(FPDF_LIBRARY_CONFIG.size)
|
||||
|
||||
config_struct = FPDF_LIBRARY_CONFIG.new(config_mem)
|
||||
config_struct[:version] = 2
|
||||
config_struct[:m_pUserFontPaths] = FFI::Pointer::NULL
|
||||
config_struct[:m_pIsolate] = FFI::Pointer::NULL
|
||||
config_struct[:m_v8EmbedderSlot] = 0
|
||||
|
||||
FPDF_InitLibraryWithConfig(config_mem)
|
||||
end
|
||||
|
||||
def self.cleanup_library
|
||||
FPDF_DestroyLibrary()
|
||||
end
|
||||
|
||||
initialize_library
|
||||
|
||||
at_exit do
|
||||
cleanup_library
|
||||
end
|
||||
end
|
||||
@@ -13,6 +13,7 @@ module ReplaceEmailVariables
|
||||
SUBMITTER_FIRST_NAME = /\{+submitter\.first_name\}+/i
|
||||
SUBMITTER_ID = /\{+submitter\.id\}+/i
|
||||
SUBMITTER_SLUG = /\{+submitter\.slug\}+/i
|
||||
SUBMITTER_FIELD_VALUE = /\{+submitter\.(?<field_name>[^}]+)\}+/i
|
||||
SUBMISSION_LINK = /\{+submission\.link\}+/i
|
||||
SUBMISSION_ID = /\{+submission\.id\}+/i
|
||||
SUBMISSION_EXPIRE_AT = /\{+submission\.expire_at\}+/i
|
||||
@@ -72,6 +73,13 @@ module ReplaceEmailVariables
|
||||
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :values, match[:field_name].to_s.strip)
|
||||
end
|
||||
|
||||
text = replace(text, SUBMITTER_FIELD_VALUE, html_escape:) do |match|
|
||||
submitters = submitter.submission.template_submitters || submitter.submission.template.submitters
|
||||
index = submitters.find_index { |e| e['uuid'] == submitter.uuid }
|
||||
|
||||
build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip)
|
||||
end
|
||||
|
||||
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
+16
-5
@@ -74,7 +74,13 @@ module Submissions
|
||||
preferences:,
|
||||
sent_at: mark_as_sent ? Time.current : nil)
|
||||
|
||||
submission.tap(&:save!)
|
||||
submission.save!
|
||||
|
||||
if submission.expire_at?
|
||||
ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id)
|
||||
end
|
||||
|
||||
submission
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,18 +118,23 @@ module Submissions
|
||||
return if email.blank?
|
||||
return if email.is_a?(Numeric)
|
||||
|
||||
return email.downcase if email.to_s.include?(',') ||
|
||||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
|
||||
email.to_s.exclude?('.')
|
||||
email = email.to_s.tr('/', ',')
|
||||
|
||||
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
|
||||
|
||||
return email.downcase if email.include?(',') ||
|
||||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
|
||||
email.exclude?('.')
|
||||
|
||||
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
||||
|
||||
return fixed_email if fixed_email == email
|
||||
|
||||
domain = email.to_s.split('@').last.to_s.downcase
|
||||
domain = email.split('@').last.to_s.downcase
|
||||
fixed_domain = fixed_email.to_s.split('@').last
|
||||
|
||||
return email.downcase if domain == fixed_domain
|
||||
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
|
||||
|
||||
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3
|
||||
Rails.logger.info("Skipped email fix #{domain}")
|
||||
|
||||
@@ -309,7 +309,7 @@ module Submissions
|
||||
|
||||
image =
|
||||
begin
|
||||
Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||
Submissions::GenerateResultAttachments.load_vips_image(attachment).autorot
|
||||
rescue Vips::Error
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
|
||||
@@ -11,6 +11,9 @@ module Submissions
|
||||
'Helvetica'
|
||||
end
|
||||
|
||||
ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
|
||||
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
|
||||
|
||||
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
|
||||
FONT_BOLD_PATH
|
||||
else
|
||||
@@ -250,25 +253,7 @@ module Submissions
|
||||
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
|
||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||
|
||||
attachments_data_cache[attachment.uuid] ||= attachment.download
|
||||
|
||||
image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
||||
|
||||
id_string = "ID: #{attachment.uuid}".upcase
|
||||
|
||||
while true
|
||||
text = HexaPDF::Layout::TextFragment.create(id_string,
|
||||
font:,
|
||||
font_size: (font_size / 1.8).to_i)
|
||||
|
||||
result = layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
|
||||
|
||||
break if result.status == :success
|
||||
|
||||
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
|
||||
|
||||
break if id_string.length < 8
|
||||
end
|
||||
image = load_vips_image(attachment, attachments_data_cache).autorot
|
||||
|
||||
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
|
||||
|
||||
@@ -284,43 +269,101 @@ module Submissions
|
||||
font:,
|
||||
font_size: (font_size / 1.8).to_i)
|
||||
|
||||
reason_result = layouter.fit([reason_text], area['w'] * width, height)
|
||||
if area['h']&.positive? && (area['w'].to_f / area['h']) > 6
|
||||
area_x = area['x'] * width
|
||||
area_y = area['y'] * height
|
||||
area_w = area['w'] * width
|
||||
area_h = area['h'] * height
|
||||
|
||||
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
|
||||
half_width = area_w / 2.0
|
||||
scale = [half_width / image.width, area_h / image.height].min
|
||||
image_width = image.width * scale
|
||||
image_height = image.height * scale
|
||||
image_x = area_x + ((half_width - image_width) / 2.0)
|
||||
image_y = height - area_y - image_height
|
||||
|
||||
image_height = (area['h'] * height) - text_height
|
||||
image_height = (area['h'] * height) / 2 if image_height < (area['h'] * height) / 2
|
||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
||||
|
||||
scale = [(area['w'] * width) / image.width, image_height / image.height].min
|
||||
canvas.image(io, at: [image_x, image_y], width: image_width, height: image_height)
|
||||
|
||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
||||
id_string = "ID: #{attachment.uuid}".upcase
|
||||
|
||||
layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
|
||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
||||
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
|
||||
while true
|
||||
text = HexaPDF::Layout::TextFragment.create(id_string,
|
||||
font:,
|
||||
font_size: (font_size / 1.8).to_i)
|
||||
|
||||
layouter.fit([reason_text], area['w'] * width, reason_result.lines.sum(&:height))
|
||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
||||
height - (area['y'] * height) - TEXT_TOP_MARGIN -
|
||||
result.lines.sum(&:height) - image_height)
|
||||
result = layouter.fit([text], half_width, (font_size / 1.8) / 0.65)
|
||||
|
||||
canvas.image(
|
||||
io,
|
||||
at: [
|
||||
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
|
||||
height - (area['y'] * height) - (image.height * scale / 2) - (image_height / 2)
|
||||
],
|
||||
width: image.width * scale,
|
||||
height: image.height * scale
|
||||
)
|
||||
break if result.status == :success
|
||||
|
||||
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
|
||||
|
||||
break if id_string.length < 8
|
||||
end
|
||||
|
||||
text_x = area_x + half_width
|
||||
text_y = height - area_y
|
||||
|
||||
reason_result = layouter.fit([reason_text], half_width, height)
|
||||
|
||||
layouter.fit([text], half_width, (font_size / 1.8) / 0.65)
|
||||
.draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y)
|
||||
|
||||
layouter.fit([reason_text], half_width, reason_result.lines.sum(&:height))
|
||||
.draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y - TEXT_TOP_MARGIN - result.lines.sum(&:height))
|
||||
else
|
||||
id_string = "ID: #{attachment.uuid}".upcase
|
||||
|
||||
loop do
|
||||
text = HexaPDF::Layout::TextFragment.create(id_string,
|
||||
font:,
|
||||
font_size: (font_size / 1.8).to_i)
|
||||
|
||||
result = layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
|
||||
|
||||
break if result.status == :success
|
||||
|
||||
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
|
||||
|
||||
break if id_string.length < 8
|
||||
end
|
||||
|
||||
reason_result = layouter.fit([reason_text], area['w'] * width, height)
|
||||
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
|
||||
|
||||
image_height = (area['h'] * height) - text_height
|
||||
image_height = (area['h'] * height) / 2 if image_height < (area['h'] * height) / 2
|
||||
|
||||
scale = [(area['w'] * width) / image.width, image_height / image.height].min
|
||||
|
||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
||||
|
||||
layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
|
||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
||||
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
|
||||
|
||||
layouter.fit([reason_text], area['w'] * width, reason_result.lines.sum(&:height))
|
||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
||||
height - (area['y'] * height) - TEXT_TOP_MARGIN -
|
||||
result.lines.sum(&:height) - image_height)
|
||||
|
||||
canvas.image(
|
||||
io,
|
||||
at: [
|
||||
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
|
||||
height - (area['y'] * height) - (image.height * scale / 2) - (image_height / 2)
|
||||
],
|
||||
width: image.width * scale,
|
||||
height: image.height * scale
|
||||
)
|
||||
end
|
||||
when 'image', 'signature', 'initials', 'stamp'
|
||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||
|
||||
attachments_data_cache[attachment.uuid] ||= attachment.download
|
||||
|
||||
image =
|
||||
begin
|
||||
Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
||||
load_vips_image(attachment, attachments_data_cache).autorot
|
||||
rescue Vips::Error
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
@@ -692,14 +735,16 @@ module Submissions
|
||||
|
||||
page = pdf.pages.add
|
||||
|
||||
scale = [A4_SIZE.first / attachment.metadata['width'].to_f,
|
||||
A4_SIZE.last / attachment.metadata['height'].to_f].min
|
||||
image = attachment.preview_images.first
|
||||
|
||||
page.box.width = attachment.metadata['width'] * scale
|
||||
page.box.height = attachment.metadata['height'] * scale
|
||||
scale = [A4_SIZE.first / image.metadata['width'].to_f,
|
||||
A4_SIZE.last / image.metadata['height'].to_f].min
|
||||
|
||||
page.box.width = image.metadata['width'] * scale
|
||||
page.box.height = image.metadata['height'] * scale
|
||||
|
||||
page.canvas.image(
|
||||
StringIO.new(attachment.preview_images.first.download),
|
||||
StringIO.new(image.download),
|
||||
at: [0, 0],
|
||||
width: page.box.width,
|
||||
height: page.box.height
|
||||
@@ -760,6 +805,20 @@ module Submissions
|
||||
[]
|
||||
end
|
||||
|
||||
def load_vips_image(attachment, cache = {})
|
||||
cache[attachment.uuid] ||= attachment.download
|
||||
|
||||
data = cache[attachment.uuid]
|
||||
|
||||
if ICO_REGEXP.match?(attachment.content_type)
|
||||
LoadIco.call(data)
|
||||
elsif BMP_REGEXP.match?(attachment.content_type)
|
||||
LoadBmp.call(data)
|
||||
else
|
||||
Vips::Image.new_from_buffer(data, '')
|
||||
end
|
||||
end
|
||||
|
||||
def h
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
|
||||
@@ -52,10 +52,10 @@ module Submitters
|
||||
|
||||
attachments.push(*new_attachments)
|
||||
|
||||
value = new_value
|
||||
acc[field['uuid']] = normalize_value(field, new_value)
|
||||
else
|
||||
acc[field['uuid']] = normalize_value(field, value)
|
||||
end
|
||||
|
||||
acc[field['uuid']] = normalize_value(field, value)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ module Templates
|
||||
FORMAT = '.png'
|
||||
ATTACHMENT_NAME = 'preview_images'
|
||||
|
||||
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
|
||||
PDF_CONTENT_TYPE = 'application/pdf'
|
||||
CONCURRENCY = 2
|
||||
Q = 95
|
||||
@@ -38,7 +39,13 @@ module Templates
|
||||
def generate_preview_image(attachment, data)
|
||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||
|
||||
image = Vips::Image.new_from_buffer(data, '')
|
||||
image =
|
||||
if BMP_REGEXP.match?(attachment.content_type)
|
||||
LoadBmp.call(data)
|
||||
else
|
||||
Vips::Image.new_from_buffer(data, '')
|
||||
end
|
||||
|
||||
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
|
||||
|
||||
bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
|
||||
@@ -77,11 +84,13 @@ module Templates
|
||||
end
|
||||
|
||||
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY)
|
||||
doc = Pdfium::Document.open_bytes(data)
|
||||
|
||||
pool = Concurrent::FixedThreadPool.new(concurrency)
|
||||
|
||||
promises =
|
||||
range.map do |page_number|
|
||||
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) }
|
||||
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(doc, page_number) }
|
||||
end
|
||||
|
||||
Concurrent::Promise.zip(*promises).value!.each do |blob|
|
||||
@@ -95,31 +104,42 @@ module Templates
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
pool.kill
|
||||
ensure
|
||||
doc&.close
|
||||
pool&.kill
|
||||
end
|
||||
|
||||
def build_and_upload_blob(data, page_number)
|
||||
page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
|
||||
page = page.resize(MAX_WIDTH / page.width.to_f)
|
||||
def build_and_upload_blob(doc, page_number, format = FORMAT)
|
||||
doc_page = doc.get_page(page_number)
|
||||
|
||||
data, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
|
||||
|
||||
page = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
||||
|
||||
page = page.copy(interpretation: :srgb)
|
||||
|
||||
bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
|
||||
|
||||
io = StringIO.new(page.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:,
|
||||
palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0))
|
||||
data =
|
||||
if format == FORMAT
|
||||
page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:,
|
||||
palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0)
|
||||
else
|
||||
page.write_to_buffer(format, interlace: true, Q: JPEG_Q)
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.new(
|
||||
filename: "#{page_number}#{FORMAT}",
|
||||
filename: "#{page_number}#{format}",
|
||||
metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
|
||||
)
|
||||
|
||||
blob.upload(io)
|
||||
blob.upload(StringIO.new(data))
|
||||
|
||||
blob
|
||||
rescue Vips::Error => e
|
||||
rescue Vips::Error, Pdfium::PdfiumError => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
nil
|
||||
ensure
|
||||
doc_page&.close
|
||||
end
|
||||
|
||||
def maybe_flatten_form(data, pdf)
|
||||
@@ -162,35 +182,19 @@ module Templates
|
||||
end
|
||||
|
||||
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
||||
io = StringIO.new
|
||||
doc = Pdfium::Document.open_file(file_path)
|
||||
|
||||
command = [
|
||||
'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{JPEG_Q},optimize=y",
|
||||
'-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
|
||||
'-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
|
||||
'-singlefile', Shellwords.escape(file_path), '-'
|
||||
].join(' ')
|
||||
|
||||
Open3.popen3(command) do |_, stdout, _, _|
|
||||
io.write(stdout.read)
|
||||
|
||||
io.rewind
|
||||
end
|
||||
|
||||
page = Vips::Image.new_from_buffer(io.read, '')
|
||||
|
||||
io.rewind
|
||||
blob = build_and_upload_blob(doc, page_number, '.jpeg')
|
||||
|
||||
ApplicationRecord.no_touching do
|
||||
ActiveStorage::Attachment.create!(
|
||||
blob: ActiveStorage::Blob.create_and_upload!(
|
||||
io:, filename: "#{page_number}.jpg",
|
||||
metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
|
||||
),
|
||||
blob: blob,
|
||||
name: ATTACHMENT_NAME,
|
||||
record: attachment
|
||||
)
|
||||
end
|
||||
ensure
|
||||
doc&.close
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :template_access do
|
||||
template
|
||||
user
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,7 @@ FactoryBot.define do
|
||||
%w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment]
|
||||
end
|
||||
except_field_types { [] }
|
||||
private_access_user { nil }
|
||||
end
|
||||
|
||||
after(:create) do |template, ev|
|
||||
@@ -343,5 +344,17 @@ FactoryBot.define do
|
||||
|
||||
template.save!
|
||||
end
|
||||
|
||||
trait :with_admin_only_access do
|
||||
after(:create) do |template|
|
||||
create(:template_access, template:, user_id: TemplateAccess::ADMIN_USER_ID)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_private_access do
|
||||
after(:create) do |template, ev|
|
||||
create(:template_access, template:, user: ev.private_access_user || template.author)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProcessSubmitterCompletionJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendFormStartedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendFormViewedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Params::BaseValidator do
|
||||
let(:validator) { described_class.new({}) }
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Submission API', type: :request do
|
||||
describe 'Submission API' do
|
||||
let(:account) { create(:account, :with_testing_account) }
|
||||
let(:testing_account) { account.testing_accounts.first }
|
||||
let(:author) { create(:user, account:) }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Submitter API', type: :request do
|
||||
describe 'Submitter API' do
|
||||
let(:account) { create(:account, :with_testing_account) }
|
||||
let(:testing_account) { account.testing_accounts.first }
|
||||
let(:author) { create(:user, account:) }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Templates API', type: :request do
|
||||
describe 'Templates API' do
|
||||
let(:account) { create(:account, :with_testing_account) }
|
||||
let(:testing_account) { account.testing_accounts.first }
|
||||
let(:author) { create(:user, account:) }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Tools API', type: :request do
|
||||
describe 'Tools API' do
|
||||
let(:account) { create(:account) }
|
||||
let(:author) { create(:user, account:) }
|
||||
let(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Account Settings' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'API Settings' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Dashboard Page' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Email Settings' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'PDF Signature Settings' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Newsletter' do
|
||||
let(:user) { create(:user, account: create(:account)) }
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Notifications Settings' do
|
||||
let(:user) { create(:user, account: create(:account)) }
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Personalization Settings', :js do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Personalization' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account:) }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Profile Settings' do
|
||||
let(:user) { create(:user, account: create(:account)) }
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'App Setup' do
|
||||
let(:form_data) do
|
||||
{
|
||||
|
||||
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user