Compare commits

...
27 Commits
Author SHA1 Message Date
Pete Matsyburka 44f3910266 fix build 2024-01-07 18:18:24 +02:00
Pete Matsyburka 35c6798a80 fix RTL form area 2024-01-07 15:09:36 +02:00
Pete Matsyburka 0995c7ee63 adjust RTL regexp 2024-01-07 14:52:10 +02:00
Pete Matsyburka 0a25f78329 touch .version 2024-01-07 14:31:10 +02:00
Pete Matsyburka bb8fffc491 link to releases page 2024-01-07 14:25:38 +02:00
Alex Turchyn 6f6c51fb32 add tooltips to personalize emails 2024-01-07 14:22:43 +02:00
Alex Turchyn 39d14c15d6 add display of app version in UI 2024-01-07 14:20:34 +02:00
Pete Matsyburka 17de28965b add LTR script support 2024-01-07 14:19:21 +02:00
Pete Matsyburka ae7865e508 add reorder parties button 2024-01-06 22:25:13 +02:00
Pete Matsyburka dc8e8cc5c9 draw field on field type click 2024-01-06 21:16:27 +02:00
Pete Matsyburka 5208470c38 make current field area more visible 2024-01-06 19:55:20 +02:00
Pete Matsyburka e7ef7b552f show field area pages list inthe form 2024-01-06 17:21:15 +02:00
Pete Matsyburka a478112b07 fix multiple area fields draw 2024-01-06 16:19:40 +02:00
Pete Matsyburka 7d0f32e1f9 handle multiple checkboxes required 2024-01-06 15:38:03 +02:00
Pete Matsyburka d35d8bead3 support checkbox option without name 2024-01-06 15:05:15 +02:00
Pete Matsyburka ab9cc52bcc add rotate api token button 2024-01-06 12:34:20 +02:00
Pete Matsyburka 7dc342a6f9 make it possible to remove default signature 2024-01-05 22:48:55 +02:00
Pete Matsyburka 80a3ef9193 add submission URL to webhooks serializer 2024-01-05 20:27:38 +02:00
Pete Matsyburka bb723d20cb add builder props 2024-01-05 18:40:45 +02:00
Pete Matsyburka 0c24a2016c use Pro instead of Enterprise 2024-01-04 15:28:46 +02:00
Pete Matsyburka 2124965996 add builder i18n 2024-01-04 01:44:16 +02:00
Pete Matsyburka 6f1e104067 add builder autosave prop 2024-01-04 01:44:11 +02:00
Pete Matsyburka b20f40b5a3 fix webhook data fields order 2024-01-03 20:43:47 +02:00
Pete Matsyburka 1093ca5fc4 add md breakpoint prop to form 2024-01-03 18:19:44 +02:00
Pete Matsyburka 0d99c55d3f disable pdf validate on write 2024-01-02 22:59:34 +02:00
Pete Matsyburka 86dc1a88ed Revert "do not use puma pidfile"
This reverts commit 8961294a29.
2024-01-02 22:39:23 +02:00
Pete Matsyburka 968c20d8ca completed email bcc sending via to address 2024-01-02 11:41:28 +02:00
63 changed files with 667 additions and 218 deletions

No files matched your search

+3
View File
@@ -30,6 +30,9 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
View File
Whitespace-only changes.
+11 -2
View File
@@ -1,3 +1,11 @@
FROM ruby:3.2.2-alpine3.18 as fonts
WORKDIR /fonts
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
FROM ruby:3.2.2-alpine3.18 as webpack FROM ruby:3.2.2-alpine3.18 as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
@@ -31,7 +39,7 @@ ENV BUNDLE_WITHOUT="development:test"
WORKDIR /app WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
@@ -45,8 +53,9 @@ COPY ./log ./log
COPY ./lib ./lib COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts RUN ln -s /fonts /app/public/fonts
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
def index def index
authorize!(:read, current_user.access_token) authorize!(:read, current_user.access_token)
end end
def create
authorize!(:manage, current_user.access_token)
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
end
end end
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing' 'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/templates/html', '/templates/html',
@@ -28,6 +28,12 @@ class UserSignaturesController < ApplicationController
end end
end end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
end
private private
def load_user_config def load_user_config
@@ -32,6 +32,8 @@ export default class extends HTMLElement {
render = (item) => { render = (item) => {
const div = document.createElement('div') const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item.name div.textContent = item.name
return div return div
@@ -56,6 +56,8 @@ export default class extends HTMLElement {
render = (item) => { render = (item) => {
const div = document.createElement('div') const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item[this.dataset.field] div.textContent = item[this.dataset.field]
return div return div
@@ -0,0 +1,51 @@
<template>
<div
v-if="uniqueAreas.length > 1"
class="mb-2"
>
<div class="flex space-x-1 text-sm ml-1 -mt-3">
<span>
{{ t('appears_on') }}:
</span>
<a
v-for="area in uniqueAreas"
:key="area.uuid"
href="#"
class="link pr-0.5"
@click.prevent="scrollIntoArea(area)"
>
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'AppearsOn',
inject: ['t', 'scrollIntoArea'],
props: {
field: {
type: Object,
required: true
}
},
computed: {
isMultipleDocs () {
return this.attachmentUuids.length > 1
},
attachmentUuids () {
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
},
uniqueAreas () {
const areas = {}
this.field.areas.forEach((area) => {
areas[area.attachment_uuid + area.page] ||= area
})
return Object.values(areas).slice(0, 6)
}
}
}
</script>
+37 -15
View File
@@ -1,8 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base" class="flex absolute lg:text-base"
dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed border-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -21,15 +22,20 @@
</span> </span>
</div> </div>
<div <div
v-if="isActive && withLabel && !area.option_uuid" v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
> >
{{ field.name || fieldNames[field.type] }} <template v-if="area.option_uuid && !option.value">
<template v-if="field.type === 'checkbox' && !field.name"> {{ optionValue(option) }}
{{ fieldIndex + 1 }}
</template> </template>
<template v-else-if="!field.required && field.type !== 'checkbox'"> <template v-else>
(optional) {{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
</template>
</template> </template>
</div> </div>
<div <div
@@ -102,11 +108,11 @@
:value="false" :value="false"
class="aspect-square base-radio" class="aspect-square base-radio"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" :checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)" @click="$emit('update:model-value', optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" v-else-if="!!modelValue && modelValue === optionValue(option)"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -121,11 +127,11 @@
:value="false" :value="false"
class="aspect-square base-checkbox" class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" :checked="!!modelValue && modelValue.includes(optionValue(option))"
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)" @change="updateMultipleSelectValue(optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -146,7 +152,8 @@
<div <div
v-else v-else
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" dir="auto"
class="flex items-center px-0.5 w-full"
> >
<span v-if="Array.isArray(modelValue)"> <span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }} {{ modelValue.join(', ') }}
@@ -171,6 +178,7 @@ export default {
IconPaperclip, IconPaperclip,
IconCheck IconCheck
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -241,6 +249,9 @@ export default {
payment: 'Payment' payment: 'Payment'
} }
}, },
option () {
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,
@@ -335,6 +346,17 @@ export default {
} }
}, },
methods: { methods: {
optionValue (option) {
if (option) {
if (option.value) {
return option.value
} else {
const index = this.field.options.indexOf(option)
return `${this.t('option')} ${index + 1}`
}
}
},
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { const monthFormats = {
M: 'numeric', M: 'numeric',
+15 -12
View File
@@ -83,22 +83,25 @@ export default {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`) return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
}, },
scrollIntoField (field) { scrollIntoField (field) {
this.areaRefs.find((area) => { if (field?.areas) {
if (area.field === field) { this.scrollIntoArea(field.areas[0])
const root = this.$root.$el.parentNode.getRootNode() }
const container = root.body || root.querySelector('div') },
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
if (container.style.overflow === 'hidden') { if (areaRef) {
this.scrollInContainer(area.$el) const root = this.$root.$el.parentNode.getRootNode()
} else { const container = root.body || root.querySelector('div')
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
return true if (container.style.overflow === 'hidden') {
this.scrollInContainer(areaRef.$el)
} else { } else {
return null areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
} }
})
return true
}
}, },
scrollInContainer (target) { scrollInContainer (target) {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
+5 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
:for="field.uuid" :for="field.uuid"
@@ -15,6 +15,7 @@
{{ t('set_today') }} {{ t('set_today') }}
</button> </button>
</div> </div>
<AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
ref="input" ref="input"
@@ -31,11 +32,13 @@
<script> <script>
import { IconCalendarCheck } from '@tabler/icons-vue' import { IconCalendarCheck } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'DateStep', name: 'DateStep',
components: { components: {
IconCalendarCheck IconCalendarCheck,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
+38 -19
View File
@@ -24,7 +24,8 @@
<div <div
v-show="isFormVisible" v-show="isFormVisible"
id="form_container" id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded" class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<button <button
@@ -45,7 +46,7 @@
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
class="md:mx-16" :class="{ 'md:mx-16': isBreakpointMd }"
@submit.prevent="submitStep" @submit.prevent="submitStep"
> >
<input <input
@@ -64,7 +65,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
</div> </div>
<DateStep <DateStep
@@ -72,12 +73,13 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
<label <label
v-if="currentField.name" v-if="currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -86,13 +88,15 @@
v-else v-else
class="py-1" class="py-1"
/> />
<AppearsOn :field="currentField" />
<select <select
:id="currentField.uuid" :id="currentField.uuid"
dir="auto"
:required="currentField.required" :required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
> >
<option <option
value="" value=""
@@ -114,6 +118,7 @@
<label <label
v-if="currentField.name" v-if="currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -121,7 +126,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="option in currentField.options" v-for="(option, index) in currentField.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -134,11 +139,11 @@
type="radio" type="radio"
class="base-radio !h-7 !w-7" class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
:value="option.value" :value="option.value || `${t('option')} ${index + 1}`"
:required="currentField.required" :required="currentField.required"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ option.value || `${t('option')} ${index + 1}` }}
</span> </span>
</label> </label>
</div> </div>
@@ -197,7 +202,7 @@
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`" :onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required" :required="field.required"
:checked="!!values[field.uuid]" :checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]" @click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
> >
<span class="text-xl"> <span class="text-xl">
{{ field.name || field.type + ' ' + (index + 1) }} {{ field.name || field.type + ' ' + (index + 1) }}
@@ -215,7 +220,7 @@
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<SignatureStep <SignatureStep
v-else-if="currentField.type === 'signature'" v-else-if="currentField.type === 'signature'"
@@ -229,7 +234,7 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<InitialsStep <InitialsStep
@@ -243,8 +248,8 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<AttachmentStep <AttachmentStep
@@ -255,7 +260,7 @@
:field="currentField" :field="currentField"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<PhoneStep <PhoneStep
v-else-if="currentField.type === 'phone'" v-else-if="currentField.type === 'phone'"
@@ -265,7 +270,7 @@
:field="currentField" :field="currentField"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
<PaymentStep <PaymentStep
@@ -276,7 +281,7 @@
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
</div> </div>
@@ -362,6 +367,7 @@ import TextStep from './text_step'
import DateStep from './date_step' import DateStep from './date_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import { t } from './i18n' import { t } from './i18n'
export default { export default {
@@ -370,6 +376,7 @@ export default {
FieldAreas, FieldAreas,
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AppearsOn,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
@@ -385,6 +392,7 @@ export default {
provide () { provide () {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea,
t: this.t t: this.t
} }
}, },
@@ -420,6 +428,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
isBreakpointMd: {
type: Boolean,
required: false,
default: true
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@@ -521,7 +534,7 @@ export default {
isButtonDisabled () { isButtonDisabled () {
if (this.recalculateButtonDisabledKey) { if (this.recalculateButtonDisabledKey) {
return this.isSubmitting || return this.isSubmitting ||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) || (this.currentField.required && ['image', 'file', 'multiple'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) || (this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted) (this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
} else { } else {
@@ -681,7 +694,7 @@ export default {
this.recalculateButtonDisabledKey = Math.random() this.recalculateButtonDisabledKey = Math.random()
if (scrollToArea) { if (scrollToArea) {
this.$refs.areas.scrollIntoField(step[0]) this.scrollIntoField(step[0])
} }
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus() this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
@@ -709,6 +722,12 @@ export default {
}) })
} }
}, },
scrollIntoField (field) {
return this.$refs.areas.scrollIntoField(field)
},
scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area)
},
async submitStep () { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
@@ -718,7 +737,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid] return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid]?.length
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
+19
View File
@@ -2,6 +2,9 @@ const en = {
submit_form: 'Submit Form', submit_form: 'Submit Form',
type_here: 'Type here', type_here: 'Type here',
optional: 'optional', optional: 'optional',
option: 'Option',
appears_on: 'Appears on',
page: 'Page',
select_your_option: 'Select your option', select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click', complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
submit: 'submit', submit: 'submit',
@@ -47,6 +50,8 @@ const es = {
submit_form: 'Enviar Formulario', submit_form: 'Enviar Formulario',
type_here: 'Escribe aquí', type_here: 'Escribe aquí',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece en',
page: 'Página',
select_your_option: 'Selecciona tu opción', select_your_option: 'Selecciona tu opción',
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic', complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
submit: 'enviar', submit: 'enviar',
@@ -92,6 +97,8 @@ const it = {
submit_form: 'Invia Modulo', submit_form: 'Invia Modulo',
type_here: 'Digita qui', type_here: 'Digita qui',
optional: 'opzionale', optional: 'opzionale',
appears_on: 'Compare su',
page: 'Pagina',
select_your_option: 'Seleziona la tua opzione', select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic', complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia', submit: 'invia',
@@ -137,6 +144,8 @@ const de = {
submit_form: 'Formular absenden', submit_form: 'Formular absenden',
type_here: 'Hier eingeben', type_here: 'Hier eingeben',
optional: 'optional', optional: 'optional',
appears_on: 'Erscheint auf',
page: 'Seite',
select_your_option: 'Wähle deine Option', select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken', complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden', submit: 'absenden',
@@ -182,6 +191,8 @@ const fr = {
submit_form: 'Envoyer le Formulaire', submit_form: 'Envoyer le Formulaire',
type_here: 'Tapez ici', type_here: 'Tapez ici',
optional: 'facultatif', optional: 'facultatif',
appears_on: 'Apparaît sur',
page: 'Page',
select_your_option: 'Sélectionnez votre option', select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez', complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer', submit: 'envoyer',
@@ -227,6 +238,8 @@ const pl = {
submit_form: 'Wyślij Formularz', submit_form: 'Wyślij Formularz',
type_here: 'Wpisz tutaj', type_here: 'Wpisz tutaj',
optional: 'opcjonalny', optional: 'opcjonalny',
appears_on: 'Pojawia się na',
page: 'Strona',
select_your_option: 'Wybierz swoją opcję', select_your_option: 'Wybierz swoją opcję',
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij', complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij', submit: 'wyślij',
@@ -272,6 +285,8 @@ const uk = {
submit_form: 'Надіслати Форму', submit_form: 'Надіслати Форму',
type_here: 'Введіть тут', type_here: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
select_your_option: 'Виберіть варіант', select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть', complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати', submit: 'надіслати',
@@ -317,6 +332,8 @@ const cs = {
submit_form: 'Odeslat formulář', submit_form: 'Odeslat formulář',
type_here: 'Zadejte zde', type_here: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu', select_your_option: 'Vyberte svou volbu',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na', complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat', submit: 'odeslat',
@@ -362,6 +379,8 @@ const pt = {
submit_form: 'Enviar Formulário', submit_form: 'Enviar Formulário',
type_here: 'Digite aqui', type_here: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece em',
page: 'Página',
select_your_option: 'Selecione sua opção', select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique', complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar', submit: 'enviar',
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else v-else
class="tooltip" class="tooltip ml-2"
:data-tip="t('draw_initials')" :data-tip="t('draw_initials')"
> >
<a <a
@@ -70,6 +70,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -103,10 +104,12 @@
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconTextSize, IconTextSize,
IconSignature, IconSignature,
@@ -2,12 +2,20 @@
<label <label
v-if="field.name" v-if="field.name"
:for="field.uuid" :for="field.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }}</label> >{{ field.name }}</label>
<div class="flex w-full"> <div class="flex w-full">
<input
v-if="modelValue.length === 0"
type="text"
:name="`values[${field.uuid}][]`"
:value="''"
class="hidden"
>
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="option in field.options" v-for="(option, index) in field.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -19,13 +27,13 @@
:ref="setInputRef" :ref="setInputRef"
type="checkbox" type="checkbox"
:name="`values[${field.uuid}][]`" :name="`values[${field.uuid}][]`"
:value="option.value" :value="optionValue(option, index)"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(option.value)" :checked="(modelValue || []).includes(optionValue(option, index))"
@change="onChange" @change="onChange"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ optionValue(option, index) }}
</span> </span>
</label> </label>
</div> </div>
@@ -36,6 +44,7 @@
<script> <script>
export default { export default {
name: 'MultiSelectStep', name: 'MultiSelectStep',
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -62,8 +71,15 @@ export default {
this.inputRefs.push(el) this.inputRefs.push(el)
} }
}, },
optionValue (option, index) {
if (option.value) {
return option.value
} else {
return `${this.t('option')} ${index + 1}`
}
},
onChange () { onChange () {
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value)) this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
} }
} }
} }
@@ -1,5 +1,5 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else-if="withTypedSignature" v-else-if="withTypedSignature"
class="tooltip" class="tooltip ml-2"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
<a <a
@@ -89,6 +89,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -122,12 +123,14 @@
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
let isFontLoaded = false let isFontLoaded = false
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconCamera, IconCamera,
IconTextSize, IconTextSize,
+6 -1
View File
@@ -2,6 +2,7 @@
<label <label
v-if="field.name" v-if="field.name"
:for="field.uuid" :for="field.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }} >{{ field.name }}
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
@@ -10,12 +11,14 @@
v-else v-else
class="py-1" class="py-1"
/> />
<AppearsOn :field="field" />
<div class="items-center flex"> <div class="items-center flex">
<input <input
v-if="!isTextArea" v-if="!isTextArea"
:id="field.uuid" :id="field.uuid"
v-model="text" v-model="text"
:maxlength="cellsMaxLegth" :maxlength="cellsMaxLegth"
dir="auto"
class="base-input !text-2xl w-full !pr-11 -mr-10" class="base-input !text-2xl w-full !pr-11 -mr-10"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
@@ -56,11 +59,13 @@
<script> <script>
import { IconAlignBoxLeftTop } from '@tabler/icons-vue' import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'TextStep', name: 'TextStep',
components: { components: {
IconAlignBoxLeftTop IconAlignBoxLeftTop,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
+3 -2
View File
@@ -61,6 +61,7 @@
v-if="field.type !== 'checkbox' || field.name" v-if="field.type !== 'checkbox' || field.name"
ref="name" ref="name"
:contenteditable="editable && !defaultField" :contenteditable="editable && !defaultField"
dir="auto"
class="pr-1 cursor-text outline-none block" class="pr-1 cursor-text outline-none block"
style="min-width: 2px" style="min-width: 2px"
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@@ -83,7 +84,7 @@
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >{{ t('required') }}</label>
</div> </div>
<button <button
v-else-if="editable" v-else-if="editable"
@@ -149,7 +150,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save'], inject: ['template', 'selectedAreaRef', 'save', 't'],
props: { props: {
area: { area: {
type: Object, type: Object,
+72 -12
View File
@@ -43,7 +43,7 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Sign Yourself {{ t('sign_yourself') }}
</span> </span>
</a> </a>
<a <a
@@ -57,7 +57,7 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Send {{ t('send') }}
</span> </span>
</a> </a>
<button <button
@@ -76,7 +76,7 @@
width="22" width="22"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Save {{ t('save') }}
</span> </span>
</button> </button>
</template> </template>
@@ -147,6 +147,7 @@
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="defaultFields"
:allow-draw="!onlyDefinedFields"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -214,16 +215,24 @@
> >
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
<p> <p>
Draw {{ drawField.name }} field on the document {{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
</p> </p>
<p> <div>
<button <button
class="base-button" class="base-button"
@click="[drawField = null, drawOption = null]" @click="clearDrawField"
> >
Cancel {{ t('cancel') }}
</button> </button>
</p> <a
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
href="#"
class="link block mt-3 text-sm"
@click.prevent="[drawField = null, drawOption = null]"
>
{{ t('or_add_field_without_drawing') }}
</a>
</div>
</div> </div>
</div> </div>
<div> <div>
@@ -232,9 +241,11 @@
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:default-fields="defaultFields" :default-fields="defaultFields"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:only-defined-fields="onlyDefinedFields"
:editable="editable" :editable="editable"
@set-draw="[drawField = $event.field, drawOption = $event.option]" @set-draw="[drawField = $event.field, drawOption = $event.option]"
@set-drag="dragField = $event" @set-drag="dragField = $event"
@@ -262,6 +273,7 @@ import MobileFields from './mobile_fields'
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue' import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { en as i18nEn } from './i18n'
export default { export default {
name: 'TemplateBuilder', name: 'TemplateBuilder',
@@ -285,6 +297,7 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
@@ -302,6 +315,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
i18n: {
type: Object,
required: false,
default: () => ({})
},
backgroundColor: { backgroundColor: {
type: String, type: String,
required: false, required: false,
@@ -312,6 +330,16 @@ export default {
required: false, required: false,
default: true default: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
autosave: {
type: Boolean,
required: false,
default: true
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
@@ -379,6 +407,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: false
},
fetchOptions: { fetchOptions: {
type: Object, type: Object,
required: false, required: false,
@@ -470,6 +503,9 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
t (key) {
return this.i18n[key] || i18nEn[key] || key
},
startFieldDraw ({ name, type }) { startFieldDraw ({ name, type }) {
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name) const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
@@ -549,10 +585,21 @@ export default {
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }) ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, },
clearDrawField () {
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
const fieldIndex = this.template.fields.indexOf(this.drawField)
if (fieldIndex !== -1) {
this.template.fields.splice(fieldIndex, 1)
}
}
this.drawField = null
this.drawOption = null
},
onKeyUp (e) { onKeyUp (e) {
if (e.code === 'Escape') { if (e.code === 'Escape') {
this.drawField = null this.clearDrawField()
this.drawOption = null
this.selectedAreaRef.value = null this.selectedAreaRef.value = null
} }
@@ -645,7 +692,16 @@ export default {
} }
this.drawField.areas ||= [] this.drawField.areas ||= []
this.drawField.areas.push(area)
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
})
if (insertBeforeAreaIndex !== -1) {
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
} else {
this.drawField.areas.push(area)
}
if (this.template.fields.indexOf(this.drawField) === -1) { if (this.template.fields.indexOf(this.drawField) === -1) {
this.template.fields.push(this.drawField) this.template.fields.push(this.drawField)
@@ -887,7 +943,11 @@ export default {
headers: { ...this.fetchOptions.headers, ...options.headers } headers: { ...this.fetchOptions.headers, ...options.headers }
}) })
}, },
save () { save ({ force } = { force: false }) {
if (!this.autosave && !force) {
return Promise.resolve({})
}
this.$nextTick(() => { this.$nextTick(() => {
if (this.$el.closest('template-builder')) { if (this.$el.closest('template-builder')) {
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template) this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
@@ -5,6 +5,7 @@
> >
<span <span
ref="contenteditable" ref="contenteditable"
dir="auto"
:contenteditable="editable" :contenteditable="editable"
style="min-width: 2px" style="min-width: 2px"
:class="iconInline ? 'inline' : 'block'" :class="iconInline ? 'inline' : 'block'"
@@ -7,6 +7,7 @@
:number="index" :number="index"
:editable="editable" :editable="editable"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
:default-fields="defaultFields" :default-fields="defaultFields"
:draw-field="drawField" :draw-field="drawField"
@@ -41,6 +42,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
+6 -6
View File
@@ -29,7 +29,7 @@
{{ message }} {{ message }}
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span class="font-medium">Click to upload</span> or drag and drop files <span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
</div> </div>
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@ export default {
IconCloudUpload, IconCloudUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -91,13 +91,13 @@ export default {
}, },
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return 'Uploading...' return this.t('uploading')
} else if (this.isProcessing) { } else if (this.isProcessing) {
return 'Processing...' return this.t('processing_')
} else if (this.acceptFileTypes === 'image/*, application/pdf') { } else if (this.acceptFileTypes === 'image/*, application/pdf') {
return 'Add PDF documents or images' return this.t('add_pdf_documents_or_images')
} else { } else {
return 'Add documents or images' return this.t('add_documents_or_images')
} }
} }
}, },
+19 -16
View File
@@ -47,7 +47,7 @@
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >{{ t('required') }}</label>
</template> </template>
</div> </div>
<div <div
@@ -101,7 +101,8 @@
<input <input
v-model="field.default_value" v-model="field.default_value"
type="text" type="text"
placeholder="Default value" :placeholder="t('default_value')"
dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save" @blur="save"
> >
@@ -111,7 +112,7 @@
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
> >
Default value {{ t('default_value') }}
</label> </label>
</div> </div>
<div <div
@@ -121,7 +122,7 @@
> >
<select <select
v-model="field.preferences.format" v-model="field.preferences.format"
placeholder="Format" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="save" @change="save"
> >
@@ -138,7 +139,7 @@
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
> >
Format {{ t('format') }}
</label> </label>
</div> </div>
<li <li
@@ -152,7 +153,7 @@
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">Required</span> <span class="label-text">{{ t('required') }}</span>
</label> </label>
</li> </li>
<li <li
@@ -166,7 +167,7 @@
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">Read-only</span> <span class="label-text">{{ t('read_only') }}</span>
</label> </label>
</li> </li>
<hr class="pb-0.5 mt-0.5"> <hr class="pb-0.5 mt-0.5">
@@ -183,7 +184,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Page {{ area.page + 1 }} {{ t('page') }} {{ area.page + 1 }}
</a> </a>
</li> </li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"> <li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
@@ -196,7 +197,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Draw New Area {{ t('draw_new_area') }}
</a> </a>
</li> </li>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)"> <li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
@@ -209,7 +210,7 @@
:width="20" :width="20"
:stroke-width="1.6" :stroke-width="1.6"
/> />
Copy to All Pages {{ t('copy_to_all_pages') }}
</a> </a>
</li> </li>
</ul> </ul>
@@ -249,8 +250,9 @@
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6" class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
type="text" type="text"
dir="auto"
required required
:placeholder="`Option ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
@blur="save" @blur="save"
> >
<button <button
@@ -268,9 +270,10 @@
v-else v-else
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`Option ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
type="text" type="text"
required required
dir="auto"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="save"
> >
@@ -287,7 +290,7 @@
class="text-center text-sm w-full pb-1" class="text-center text-sm w-full pb-1"
@click="addOption" @click="addOption"
> >
+ Add option + {{ t('add_option') }}
</button> </button>
</div> </div>
</div> </div>
@@ -313,7 +316,7 @@ export default {
IconCopy, IconCopy,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'], inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -366,7 +369,7 @@ export default {
} else { } else {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field) const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field' const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}` return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
} }
@@ -419,7 +422,7 @@ export default {
this.template.documents.forEach((attachment) => { this.template.documents.forEach((attachment) => {
attachment.preview_images.forEach((page) => { attachment.preview_images.forEach((page) => {
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) { if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
field.areas.push({ ...JSON.parse(areaString), page: parseInt(page.filename) }) field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page: parseInt(page.filename) })
} }
}) })
}) })
@@ -78,7 +78,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -105,6 +105,7 @@
</label> </label>
<label <label
v-else v-else
ref="label"
tabindex="0" tabindex="0"
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between" class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
> >
@@ -155,13 +156,36 @@
{{ submitter.name }} {{ submitter.name }}
</span> </span>
</span> </span>
<button <div
v-if="!compact && submitters.length > 1 && editable" v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2" class="flex"
@click.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
</button> <button
title="Up"
class="relative w-2"
style="font-size: 10px; margin-bottom: -4px"
@click.stop="[move(submitter, -1), $refs.label.focus()] "
>
▲
</button>
<button
title="Down"
class="relative w-2"
style="font-size: 10px; margin-top: -4px"
@click.stop="[move(submitter, 1), $refs.label.focus()] "
>
▼
</button>
</div>
<button
v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2"
@click.stop="remove(submitter)"
>
<IconTrashX :width="18" />
</button>
</div>
</a> </a>
</li> </li>
<li v-if="submitters.length < 10 && editable"> <li v-if="submitters.length < 10 && editable">
@@ -175,7 +199,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -197,6 +221,7 @@ export default {
IconTrashX, IconTrashX,
IconChevronUp IconChevronUp
}, },
inject: ['t', 'save'],
props: { props: {
submitters: { submitters: {
type: Array, type: Array,
@@ -250,16 +275,16 @@ export default {
}, },
names () { names () {
return [ return [
'First Party', this.t('first_party'),
'Second Party', this.t('second_party'),
'Third Party', this.t('third_party'),
'Fourth Party', this.t('fourth_party'),
'Fifth Party', this.t('fifth_party'),
'Sixth Party', this.t('sixth_party'),
'Seventh Party', this.t('seventh_party'),
'Eighth Party', this.t('eighth_party'),
'Ninth Party', this.t('ninth_party'),
'Tenth Party' this.t('tenth_party')
] ]
}, },
selectedSubmitter () { selectedSubmitter () {
@@ -275,6 +300,23 @@ export default {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
move (submitter, direction) {
const currentIndex = this.submitters.indexOf(submitter)
this.submitters.splice(currentIndex, 1)
if (currentIndex + direction > this.submitters.length) {
this.submitters.unshift(submitter)
} else if (currentIndex + direction < 0) {
this.submitters.push(submitter)
} else {
this.submitters.splice(currentIndex + direction, 0, submitter)
}
this.selectSubmitter(submitter)
this.save()
},
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.submitters.length],
+15 -15
View File
@@ -54,7 +54,7 @@ import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckb
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment'], inject: ['withPhone', 'withPayment', 't'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -90,20 +90,20 @@ export default {
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
text: 'Text', text: this.t('text'),
signature: 'Signature', signature: this.t('signature'),
initials: 'Initials', initials: this.t('initials'),
date: 'Date', date: this.t('date'),
image: 'Image', image: this.t('image'),
file: 'File', file: this.t('file'),
select: 'Select', select: this.t('select'),
checkbox: 'Checkbox', checkbox: this.t('checkbox'),
multiple: 'Multiple', multiple: this.t('multiple'),
radio: 'Radio', radio: this.t('radio'),
cells: 'Cells', cells: this.t('cells'),
stamp: 'Stamp', stamp: this.t('stamp'),
payment: 'Payment', payment: this.t('payment'),
phone: 'Phone' phone: this.t('phone')
} }
}, },
fieldIcons () { fieldIcons () {
+15 -5
View File
@@ -64,7 +64,7 @@
</template> </template>
</div> </div>
<div <div
v-if="editable" v-if="editable && !onlyDefinedFields"
class="grid grid-cols-3 gap-1 pb-2" class="grid grid-cols-3 gap-1 pb-2"
> >
<template <template
@@ -120,7 +120,7 @@
</template> </template>
</div> </div>
<div <div
v-if="fields.length < 4 && editable" v-if="fields.length < 4 && editable && withHelp"
class="text-xs p-2 border border-base-200 rounded" class="text-xs p-2 border border-base-200 rounded"
> >
<ul class="list-disc list-outside ml-3"> <ul class="list-disc list-outside ml-3">
@@ -131,7 +131,7 @@
Drag &amp; drop any other field type on the page Drag &amp; drop any other field type on the page
</li> </li>
<li> <li>
Click on the field type above to add it to the form without drawing it on the document Click on the field type above to start drawing it
</li> </li>
</ul> </ul>
</div> </div>
@@ -154,12 +154,17 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'], inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -170,6 +175,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: true
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -285,7 +295,7 @@ export default {
this.fields.push(field) this.fields.push(field)
if (['signature', 'initials', 'cells', 'stamp'].includes(type)) { if (!['payment', 'file'].includes(type)) {
this.$emit('set-draw', { field }) this.$emit('set-draw', { field })
} }
+57
View File
@@ -0,0 +1,57 @@
const en = {
sign_yourself: 'Sign Yourself',
send: 'Send',
save: 'Save',
cancel: 'Cancel',
draw_field_on_the_document: 'Draw {field} field on the document',
click_to_upload: 'Click to upload',
or_drag_and_drop_files: 'or drag and drop files',
uploading: 'Uploading',
processing_: 'Processing...',
add_pdf_documents_or_images: 'Add PDF documents or images',
add_documents_or_images: 'Add documents or images',
required: 'Required',
default_value: 'Default value',
format: 'Format',
read_only: 'Read-only',
page: 'Page',
draw_new_area: 'Draw New Area',
copy_to_all_pages: 'Copy to All Pages',
add_option: 'Add option',
option: 'Option',
first_party: 'First Party',
second_party: 'Second Party',
third_party: 'Third Party',
fourth_party: 'Fourth Party',
fifth_party: 'Fifth Party',
sixth_party: 'Sixth Party',
seventh_party: 'Seventh Party',
eighth_party: 'Eighth Party',
ninth_party: 'Ninth Party',
tenth_party: 'Tenth Party',
add: 'Add',
or_add_field_without_drawing: 'Or add field without drawing',
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
field: 'Field',
group: 'Group',
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
draw_field: 'Draw {field} Field',
replace: 'Replace',
uploading_: 'Uploading...',
add_document: 'Add Document'
}
export { en }
@@ -9,14 +9,14 @@
class="inline" class="inline"
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span> Draw {{ fieldNames[drawField.type] }} Field </span> <span>{{ t('draw_field').replace('{field}', fieldNames[drawField.type]) }}</span>
</div> </div>
<a <a
href="#" href="#"
class="link block text-center" class="link block text-center"
@click.prevent="$emit('cancel')" @click.prevent="$emit('cancel')"
> >
Cancel {{ t('cancel') }}
</a> </a>
</span> </span>
<FieldSubmitter <FieldSubmitter
@@ -43,7 +43,7 @@ export default {
Field, Field,
FieldSubmitter FieldSubmitter
}, },
inject: ['save'], inject: ['save', 't'],
props: { props: {
drawField: { drawField: {
type: Object, type: Object,
@@ -78,7 +78,7 @@ export default {
IconPlus, IconPlus,
IconX IconX
}, },
inject: ['withPhone', 'withPayment', 'backgroundColor'], inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
+9
View File
@@ -75,6 +75,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
@@ -149,6 +154,10 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (!this.allowDraw) {
return
}
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField) {
return return
} }
+4 -4
View File
@@ -26,7 +26,7 @@ import Upload from './upload'
export default { export default {
name: 'ReplaceDocument', name: 'ReplaceDocument',
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -56,11 +56,11 @@ export default {
}, },
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return 'Uploading...' return this.t('uploading_')
} else if (this.isProcessing) { } else if (this.isProcessing) {
return 'Processing...' return this.t('processing_')
} else { } else {
return 'Replace' return this.t('replace')
} }
} }
}, },
+4 -4
View File
@@ -15,13 +15,13 @@
width="20" width="20"
/> />
<span v-if="isLoading"> <span v-if="isLoading">
Uploading... {{ t('uploading_') }}
</span> </span>
<span v-else-if="isProcessing"> <span v-else-if="isProcessing">
Processing... {{ t('processing_') }}
</span> </span>
<span v-else> <span v-else>
Add Document {{ t('add_document') }}
</span> </span>
</label> </label>
<form <form
@@ -50,7 +50,7 @@ export default {
IconUpload, IconUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'], inject: ['baseFetch', 't'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
+11 -5
View File
@@ -26,18 +26,24 @@ class ProcessSubmitterCompletionJob < ApplicationJob
if submitter.template.account.users.exists?(id: user.id) && if submitter.template.account.users.exists?(id: user.id) &&
submitter.submission.preferences['send_email'] != false submitter.submission.preferences['send_email'] != false
bcc = submitter.submission.template.account.account_configs SubmitterMailer.completed_email(submitter, user).deliver_later!
.find_by(key: AccountConfig::BCC_EMAILS)&.value
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later! bcc = submitter.submission.template.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
SubmitterMailer.completed_email(submitter, user, to: bcc).deliver_later! if bcc
end end
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } to = build_to_addresses(submitter)
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present? SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
end end
def build_to_addresses(submitter)
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter)
next_submitter_item = next_submitter_item =
submitter.submission.template_submitters.find do |e| submitter.submission.template_submitters.find do |e|
+2 -3
View File
@@ -33,7 +33,7 @@ class SubmitterMailer < ApplicationMailer
) )
end end
def completed_email(submitter, user, bcc: nil) def completed_email(submitter, user, to: nil)
@current_account = submitter.submission.template.account @current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
@submission = submitter.submission @submission = submitter.submission
@@ -55,8 +55,7 @@ class SubmitterMailer < ApplicationMailer
end end
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name, to: to || (user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name),
bcc:,
subject:) subject:)
end end
+1 -1
View File
@@ -6,7 +6,7 @@
<%= f.fields_for current_account do |ff| %> <%= f.fields_for current_account do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %> <%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %> <%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
+1
View File
@@ -8,6 +8,7 @@
<div class="flex flex-row space-x-4"> <div class="flex flex-row space-x-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -6,7 +6,7 @@
<style> <style>
</style> </style>
</head> </head>
<body> <body dir="auto">
<%= yield %> <%= yield %>
<%= render partial: 'shared/mailer_attribution' %> <%= render partial: 'shared/mailer_attribution' %>
</body> </body>
+9 -3
View File
@@ -4,11 +4,16 @@
<div class="text-center"> <div class="text-center">
<div class="relative flex flex-col items-center"> <div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %> <%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-8"> <h1 class="text-6xl font-bold mt-4 mb-4">
DocuSeal DocuSeal
</h1> </h1>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<h2 class="mb-16 text-lg text-center text-gray-600"> <h2 class="mt-4 mb-16 text-lg text-center text-gray-600">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing. A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2> </h2>
</div> </div>
@@ -62,7 +67,8 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3> <h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>Open-source contributors are always ready to help! Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>
Open-source contributors are always ready to help!
</p> </p>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -11,12 +11,17 @@
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<% end %> <% end %>
@@ -11,7 +11,7 @@
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %> <%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :title, 'Button title', class: 'label' %> <%= ff.label :title, 'Button title', class: 'label' %>
<%= ff.text_field :title, class: 'base-input' %> <%= ff.text_field :title, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :url, 'Button URL', class: 'label' %> <%= ff.label :url, 'Button URL', class: 'label' %>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -11,12 +11,17 @@
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<% end %> <% end %>
@@ -1,4 +1,4 @@
<div class="collapse collapse-plus bg-base-200"> <div class="collapse collapse-plus bg-base-200 overflow-visible">
<input type="checkbox"> <input type="checkbox">
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
<div> <div>
@@ -10,13 +10,23 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, class: 'label' %> <div class="flex items-center">
<%= ff.text_field :subject, required: true, class: 'base-input' %> <%= ff.label :subject, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, 'subject').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :body, class: 'label' %> <div class="flex items-center">
<%= ff.label :body, class: 'label' %>
<span class="tooltip" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<% end %> <% end %>
+4 -3
View File
@@ -6,11 +6,11 @@
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
<%= f.label :first_name, class: 'label' %> <%= f.label :first_name, class: 'label' %>
<%= f.text_field :first_name, required: true, class: 'base-input' %> <%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :last_name, class: 'label' %> <%= f.label :last_name, class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input' %> <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -25,7 +25,8 @@
<p class="text-2xl font-bold mt-8 mb-4">Signature</p> <p class="text-2xl font-bold mt-8 mb-4">Signature</p>
<% signature = UserConfigs.load_signature(current_user) %> <% signature = UserConfigs.load_signature(current_user) %>
<% if signature %> <% if signature %>
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: 'Remove', disabled_with: 'Removing'), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>"> <img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>">
</div> </div>
<% end %> <% end %>
+5
View File
@@ -116,6 +116,11 @@
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center"> <a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %> <%= Docuseal::SUPPORT_EMAIL %>
</a> </a>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
+1 -1
View File
@@ -3,7 +3,7 @@
<div> <div>
<p class="font-bold">Send signature requests via SMS</p> <p class="font-bold">Send signature requests via SMS</p>
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Enterprise Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p> </p>
+1 -1
View File
@@ -3,7 +3,7 @@
<div> <div>
<p class="font-bold">Single Sign On with SAML 2.0</p> <p class="font-bold">Single Sign On with SAML 2.0</p>
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Enterprise Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p> </p>
@@ -18,7 +18,7 @@
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '' %> <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '', dir: 'auto' %>
</submitters-autocomplete> </submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>"> <div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
+2 -2
View File
@@ -23,14 +23,14 @@
</submitters-autocomplete> </submitters-autocomplete>
<% if template.submitters.size > 1 %> <% if template.submitters.size > 1 %>
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
</submitters-autocomplete> </submitters-autocomplete>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size == 1 %> <% if template.submitters.size == 1 %>
<div class="form-control flex"> <div class="form-control flex">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>" dir="auto">
</submitters-autocomplete> </submitters-autocomplete>
</div> </div>
<% end %> <% end %>
+8 -3
View File
@@ -33,12 +33,17 @@
<div class="form-control space-y-2"> <div class="form-control space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :subject, class: 'label' %> <%= f.label :subject, class: 'label' %>
<%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full' %> <%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :message, 'Body', class: 'label' %> <div class="flex items-center">
<%= f.label :message, 'Body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %> <%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<%= render 'message_fields' %> <%= render 'message_fields' %>
+1 -1
View File
@@ -1,4 +1,4 @@
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%"> <field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %> <% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %> <% elsif field['type'].in?(['file', 'payment']) %>
+5 -5
View File
@@ -35,7 +35,7 @@
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<div class="pb-2 pt-1.5 text-center"> <div class="pb-2 pt-1.5 text-center" dir="auto">
<%= item['name'].presence || document.filename.base %> <%= item['name'].presence || document.filename.base %>
</div> </div>
</a> </a>
@@ -74,13 +74,14 @@
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space"> <div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %> <% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %> <% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% submitter_field_counters = Hash.new { 0 } %>
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %> <% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %> <% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10"> <div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
<div class="border border-base-300 rounded-md px-2 py-1 mb-1"> <div class="border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span> <span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
<span class="text-lg"> <span class="text-lg" dir="auto">
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span> </span>
</div> </div>
@@ -132,16 +133,15 @@
</div> </div>
</div> </div>
<div class="px-1.5 mb-4"> <div class="px-1.5 mb-4">
<% submitter_field_counters = Hash.new { 0 } %>
<% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %> <% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %>
<% submitter_field_counters[field['type']] += 1 %> <% submitter_field_counters[field['type']] += 1 %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% next if value.blank? %> <% next if value.blank? %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
<div class="text-xs font-medium uppercase mb-0.5"> <div class="text-xs font-medium uppercase mb-0.5" dir="auto">
<%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %>
</div> </div>
<div> <div dir="auto">
<% if field['type'].in?(%w[signature initials]) %> <% if field['type'].in?(%w[signature initials]) %>
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
+1 -1
View File
@@ -1,7 +1,7 @@
<%= render 'shared/turbo_modal', title: 'Rename Folder' do %> <%= render 'shared/turbo_modal', title: 'Rename Folder' do %>
<%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> <%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<div class="form-control my-6"> <div class="form-control my-6">
<%= f.text_field :name, required: true, placeholder: 'Folder Name...', class: 'base-input w-full', autofocus: true %> <%= f.text_field :name, required: true, placeholder: 'Folder Name...', class: 'base-input w-full', autofocus: true, dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Rename', disabled_with: 'Saving'), class: 'base-button' %> <%= f.button button_title(title: 'Rename', disabled_with: 'Saving'), class: 'base-button' %>
+1 -1
View File
@@ -4,7 +4,7 @@
<%= hidden_field_tag :base_template_id, @base_template.id %> <%= hidden_field_tag :base_template_id, @base_template.id %>
<% end %> <% end %>
<div class="form-control mt-6"> <div class="form-control mt-6">
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input' %> <%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="mt-3 mb-4 flex items-center justify-between"> <div class="mt-3 mb-4 flex items-center justify-between">
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]"> <a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
+2 -2
View File
@@ -2,11 +2,11 @@
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :first_name, class: 'label' %> <%= f.label :first_name, class: 'label' %>
<%= f.text_field :first_name, required: true, class: 'base-input' %> <%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :last_name, class: 'label' %> <%= f.label :last_name, class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input' %> <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
+1 -1
View File
@@ -7,7 +7,7 @@
<% end %> <% end %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>"> <a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %> <%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
Unlock more user roles with DocuSeal Enterprise. Unlock more user roles with DocuSeal Pro.
<span class="link font-medium">Learn More</a> <span class="link font-medium">Learn More</a>
</a> </a>
</div> </div>
+1 -1
View File
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'development') environment ENV.fetch('RAILS_ENV', 'development')
# Specifies the `pidfile` that Puma will use. # Specifies the `pidfile` that Puma will use.
# pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid") pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
# Specifies the number of `workers` to boot in clustered mode. # Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together # Workers are forked web server processes. If using threads and workers together
+2 -2
View File
@@ -59,7 +59,7 @@ Rails.application.routes.draw do
resource :newsletter, only: %i[show update] resource :newsletter, only: %i[show update]
resources :enquiries, only: %i[create] resources :enquiries, only: %i[create]
resources :users, only: %i[new create edit update destroy] resources :users, only: %i[new create edit update destroy]
resource :user_signature, only: %i[edit update] resource :user_signature, only: %i[edit update destroy]
resources :submissions, only: %i[show destroy] resources :submissions, only: %i[show destroy]
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
resource :templates_upload, only: %i[create] resource :templates_upload, only: %i[create]
@@ -109,7 +109,7 @@ Rails.application.routes.draw do
resources :users, only: %i[index] resources :users, only: %i[index]
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
if !Docuseal.multitenant? || Docuseal.demo? if !Docuseal.multitenant? || Docuseal.demo?
resources :api, only: %i[index], controller: 'api_settings' resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings' resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
end end
resource :account, only: %i[show update] resource :account, only: %i[show update]
+5
View File
@@ -29,6 +29,7 @@ module Docuseal
CERTS = JSON.parse(ENV.fetch('CERTS', '{}')) CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
TIMESERVER_URL = ENV.fetch('TIMESERVER_URL', nil) TIMESERVER_URL = ENV.fetch('TIMESERVER_URL', nil)
VERSION_FILE_PATH = Rails.root.join('.version')
DEFAULT_URL_OPTIONS = { DEFAULT_URL_OPTIONS = {
host: HOST, host: HOST,
@@ -37,6 +38,10 @@ module Docuseal
module_function module_function
def version
@version ||= VERSION_FILE_PATH.read.strip if VERSION_FILE_PATH.exist?
end
def multitenant? def multitenant?
ENV['MULTITENANT'] == 'true' ENV['MULTITENANT'] == 'true'
end end
+1 -1
View File
@@ -40,7 +40,7 @@ class PdfProcessor < HexaPDF::Content::Processor
new_io = StringIO.new new_io = StringIO.new
doc.write(new_io) doc.write(new_io, validate: false)
[new_io.tap(&:rewind).read, acc] [new_io.tap(&:rewind).read, acc]
end end
+18 -4
View File
@@ -27,6 +27,8 @@ module Submissions
'GBP' => '£' 'GBP' => '£'
}.freeze }.freeze
RTL_REGEXP = Submissions::GenerateResultAttachments::RTL_REGEXP
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
@@ -147,7 +149,7 @@ module Submissions
[ [
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" }, submission.template_submitters.size > 1 && { text: "#{item['name']}\n" },
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] }, submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] },
submitter.name && { text: "#{submitter.name}\n" }, submitter.name && { text: "#{maybe_rtl_reverse(submitter.name)}\n" },
submitter.phone && { text: "#{submitter.phone}\n" } submitter.phone && { text: "#{submitter.phone}\n" }
].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0] ].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0]
) )
@@ -185,11 +187,13 @@ module Submissions
composer.formatted_text_box( composer.formatted_text_box(
[ [
{ {
text: field['name'].to_s.upcase.presence || text: maybe_rtl_reverse(field['name'].to_s).upcase.presence ||
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase, "#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase,
font_size: 6 font_size: 6
} }
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0] ].compact_blank,
align: field['name'].to_s.match?(RTL_REGEXP) ? :right : :left,
line_spacing: 1.8, padding: [0, 0, 5, 0]
), ),
if field['type'].in?(%w[image signature initials stamp]) if field['type'].in?(%w[image signature initials stamp])
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
@@ -233,7 +237,9 @@ module Submissions
value = value.join(', ') if value.is_a?(Array) value = value.join(', ') if value.is_a?(Array)
composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0]) composer.formatted_text_box([{ text: maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
padding: [0, 0, 10, 0])
end end
] ]
end end
@@ -292,6 +298,14 @@ module Submissions
) )
end end
def maybe_rtl_reverse(text)
if text.match?(RTL_REGEXP)
text.reverse
else
text
end
end
def add_logo(column, _submission = nil) def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float) column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
+22 -8
View File
@@ -13,6 +13,8 @@ module Submissions
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co' SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co' SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co'
RTL_REGEXP = /\A[\p{Hebrew}\p{Arabic}].*[\p{Hebrew}\p{Arabic}]\z/
TEXT_LEFT_MARGIN = 1 TEXT_LEFT_MARGIN = 1
TEXT_TOP_MARGIN = 1 TEXT_TOP_MARGIN = 1
@@ -56,10 +58,12 @@ module Submissions
height = page.box.height height = page.box.height
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, font: pdf.fonts.add(FONT_NAME), font_size:)
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center,
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
font: pdf.fonts.add(FONT_NAME), font_size:)
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
canvas = page.canvas(type: :overlay) canvas = page.canvas(type: :overlay)
@@ -155,7 +159,7 @@ module Submissions
when 'cells' when 'cells'
cell_width = area['cell_w'] * width cell_width = area['cell_w'] * width
value.chars.each_with_index do |char, index| maybe_rtl_reverse(value).chars.each_with_index do |char, index|
text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME), text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME),
font_size:) font_size:)
@@ -168,14 +172,16 @@ module Submissions
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end end
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME), value = maybe_rtl_reverse(Array.wrap(value).join(', '))
font_size:)
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
font_size:)
lines = layouter.fit([text], area['w'] * width, height).lines lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height) box_height = lines.sum(&:height)
if box_height > (area['h'] * height) + 1 if box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), text = HexaPDF::Layout::TextFragment.create(value,
font: pdf.fonts.add(FONT_NAME), font: pdf.fonts.add(FONT_NAME),
font_size: (font_size / 1.4).to_i) font_size: (font_size / 1.4).to_i)
@@ -250,9 +256,9 @@ module Submissions
sign_params[:signature_size] = 10_000 sign_params[:signature_size] = 10_000
end end
pdf.sign(io, **sign_params) pdf.sign(io, write_options: { validate: false }, **sign_params)
else else
pdf.write(io, incremental: true) pdf.write(io, incremental: true, validate: false)
end end
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
@@ -312,6 +318,14 @@ module Submissions
pdf pdf
end end
def maybe_rtl_reverse(text)
if text.match?(RTL_REGEXP)
text.reverse
else
text
end
end
def sign_reason(name) def sign_reason(name)
format(SIGN_REASON, name:) format(SIGN_REASON, name:)
end end
+15 -7
View File
@@ -21,23 +21,27 @@ module Submitters
.merge('values' => values, .merge('values' => values,
'documents' => documents, 'documents' => documents,
'audit_log_url' => submitter.submission.audit_log_url, 'audit_log_url' => submitter.submission.audit_log_url,
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
**Docuseal.default_url_options),
'role' => submitter_name) 'role' => submitter_name)
end end
def build_values_array(submitter) def build_values_array(submitter)
fields_index = (submitter.submission.template.fields + fields = submitter.submission.template_fields.presence || submitter.submission.template.fields
submitter.submission.template_fields.to_a).index_by { |e| e['uuid'] }
attachments_index = submitter.attachments.index_by(&:uuid) attachments_index = submitter.attachments.index_by(&:uuid)
submitter_field_counters = Hash.new { 0 } submitter_field_counters = Hash.new { 0 }
submitter.values.map do |uuid, value| fields.filter_map do |field|
field = fields_index[uuid]
submitter_field_counters[field['type']] += 1 submitter_field_counters[field['type']] += 1
next if field['submitter_uuid'] != submitter.uuid
field_name = field_name =
field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}"
value = fetch_field_value(field, value, attachments_index) next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
{ field: field_name, value: } { field: field_name, value: }
end end
@@ -50,7 +54,7 @@ module Submitters
end end
def fetch_field_value(field, value, attachments_index) def fetch_field_value(field, value, attachments_index)
if field['type'].in?(%w[image signature]) if field['type'].in?(%w[image signature initials stamp])
rails_storage_proxy_url(attachments_index[value]) rails_storage_proxy_url(attachments_index[value])
elsif field['type'] == 'file' elsif field['type'] == 'file'
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) } Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) }
@@ -62,7 +66,11 @@ module Submitters
def rails_storage_proxy_url(attachment) def rails_storage_proxy_url(attachment)
return if attachment.blank? return if attachment.blank?
Rails.application.routes.url_helpers.rails_storage_proxy_url(attachment, **Docuseal.default_url_options) r.rails_storage_proxy_url(attachment, **Docuseal.default_url_options)
end
def r
Rails.application.routes.url_helpers
end end
end end
end end