mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3db7d47746 | ||
|
|
95899336b8 | ||
|
|
f6691c110e | ||
|
|
749722a9df | ||
|
|
265b668cb4 | ||
|
|
0527e6a5c1 | ||
|
|
d989b61d19 | ||
|
|
c1f3e6ff13 | ||
|
|
3c0fff2d82 | ||
|
|
bd599c264f | ||
|
|
0274093c89 | ||
|
|
5845b5bdfd | ||
|
|
3dd0ba6b0b | ||
|
|
e79306f8a3 | ||
|
|
0171b0c2dd | ||
|
|
a963ef53d3 | ||
|
|
344f302c76 | ||
|
|
17ded282b2 | ||
|
|
2ce57df485 | ||
|
|
849289287e | ||
|
|
704fbc2393 | ||
|
|
168fc8bd03 | ||
|
|
c5598d9efe |
No files matched your search
+6
-3
@@ -2,7 +2,9 @@ FROM ruby:3.2.2-alpine3.18 as fonts
|
|||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
@@ -38,11 +40,11 @@ ENV PIDFILE=/dev/null
|
|||||||
|
|
||||||
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 && mkdir /fonts
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
RUN apk add --no-cache build-base && bundle update --bundler && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
@@ -55,6 +57,7 @@ COPY ./tmp ./tmp
|
|||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
|
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
COPY --from=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
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ module Api
|
|||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ module Api
|
|||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
conditions: [%i[field_uuid value action]],
|
conditions: [%i[field_uuid value action]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
if request.path == '/upgrade'
|
||||||
|
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
||||||
|
end
|
||||||
|
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||||
|
|
||||||
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
Submitter.find_by!(slug: params[:submitter_slug])
|
Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import FolderAutocomplete from './elements/folder_autocomplete'
|
|||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
|||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
window.customElements.define('submit-form', SubmitForm)
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
window.customElements.define('prompt-password', PromptPassword)
|
window.customElements.define('prompt-password', PromptPassword)
|
||||||
|
window.customElements.define('emails-textarea', EmailsTextarea)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.limit) {
|
||||||
|
this.textarea.addEventListener('input', () => {
|
||||||
|
const emails = this.textarea.value.match(emailRegexp) || []
|
||||||
|
|
||||||
|
this.updateCounter(emails.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounter (count) {
|
||||||
|
let counter = document.getElementById('emails_counter')
|
||||||
|
let bulkMessage = document.getElementById('bulk_message')
|
||||||
|
|
||||||
|
if (count < 2) {
|
||||||
|
counter?.remove()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((count + 10) > this.dataset.limit) {
|
||||||
|
if (!counter) {
|
||||||
|
counter = document.createElement('span')
|
||||||
|
|
||||||
|
counter.id = 'emails_counter'
|
||||||
|
counter.classList.add('text-xs', 'right-0', 'absolute')
|
||||||
|
counter.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.bulkEnabled !== 'true') {
|
||||||
|
if (!bulkMessage) {
|
||||||
|
bulkMessage = document.createElement('span')
|
||||||
|
|
||||||
|
bulkMessage.id = 'bulk_message'
|
||||||
|
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
|
||||||
|
bulkMessage.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(bulkMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get textarea () {
|
||||||
|
return this.querySelector('textarea')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
if (textarea && item[field]) {
|
if (textarea && item[field]) {
|
||||||
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@
|
|||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="flex items-center px-0.5 w-full"
|
class="flex items-center px-0.5 w-full"
|
||||||
|
:class="alignClasses[field.preferences?.align]"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span v-if="Array.isArray(modelValue)">
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
@@ -167,6 +168,7 @@
|
|||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
>{{ modelValue }}</span>
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,6 +255,13 @@ export default {
|
|||||||
phone: this.t('phone')
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'text-center',
|
||||||
|
left: 'text-left',
|
||||||
|
right: 'text-right'
|
||||||
|
}
|
||||||
|
},
|
||||||
option () {
|
option () {
|
||||||
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||||
},
|
},
|
||||||
@@ -338,17 +347,19 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue () {
|
modelValue () {
|
||||||
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
this.$nextTick(() => {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||||
}
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.field.type === 'text' && this.$refs.textContainer) {
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue}`.length : 0
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
optionValue (option) {
|
optionValue (option) {
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="completedButton.url"
|
:href="sanitizeHref(completedButton.url)"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full"
|
class="white-button flex items-center w-full"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -215,6 +216,11 @@ export default {
|
|||||||
|
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
},
|
},
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||||
:href="extractAttr(item, 'href')"
|
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
:class="extractAttr(item, 'class') || 'link'"
|
:class="extractAttr(item, 'class') || 'link'"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -55,6 +55,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
extractAttr (text, attr) {
|
extractAttr (text, attr) {
|
||||||
if (text.includes(attr)) {
|
if (text.includes(attr)) {
|
||||||
return text.split(attr).pop().split('"')[1]
|
return text.split(attr).pop().split('"')[1]
|
||||||
|
|||||||
@@ -68,14 +68,13 @@
|
|||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
pattern="^\+[0-9\s\-]+$"
|
pattern="^\+[0-9\s\-]+$"
|
||||||
:oninvalid="`this.value ? this.setCustomValidity('${t('use_international_format')}...') : ''`"
|
|
||||||
oninput="this.setCustomValidity('')"
|
|
||||||
type="tel"
|
type="tel"
|
||||||
inputmode="tel"
|
inputmode="tel"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
placeholder="+1 234 567-8900"
|
placeholder="+1 234 567-8900"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@input="$emit('update:model-value', $event.target.value)"
|
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
|
||||||
|
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,11 +35,11 @@
|
|||||||
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:pattern="field.validation?.pattern"
|
:pattern="field.validation?.pattern"
|
||||||
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
|
||||||
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
|
@invalid="field.validation?.message ? $event.target.setCustomValidity(field.validation.message) : ''"
|
||||||
|
@input="field.validation?.message ? $event.target.setCustomValidity('') : ''"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
:class="[bgColors[submitterIndex], field?.default_value ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
@@ -211,6 +211,13 @@ export default {
|
|||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
},
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'justify-center',
|
||||||
|
left: 'justify-start',
|
||||||
|
right: 'justify-end'
|
||||||
|
}
|
||||||
|
},
|
||||||
optionIndexText () {
|
optionIndexText () {
|
||||||
if (this.area.option_uuid && this.field.options) {
|
if (this.area.option_uuid && this.field.options) {
|
||||||
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
@@ -301,17 +308,19 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'field.default_value' () {
|
'field.default_value' () {
|
||||||
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value.length)) {
|
this.$nextTick(() => {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
}
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value)) {
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
|||||||
@@ -116,15 +116,41 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'text' && !defaultField"
|
v-if="['number'].includes(field.type)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="value in ['left', 'right', 'center']"
|
||||||
|
:key="value"
|
||||||
|
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
{{ t(value) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('align') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="field.default_value"
|
v-model="field.default_value"
|
||||||
type="text"
|
|
||||||
:placeholder="t('default_value')"
|
:placeholder="t('default_value')"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
:type="field.type"
|
||||||
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"
|
||||||
>
|
>
|
||||||
@@ -202,7 +228,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'phone'"
|
v-if="field.type != 'phone' && field.type != 'stamp'"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -215,6 +241,20 @@
|
|||||||
<span class="label-text">{{ t('required') }}</span>
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'stamp'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="field.preferences?.with_logo != false"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('with_logo') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -244,7 +284,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type === 'text' && !defaultField"
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -257,8 +297,13 @@
|
|||||||
<span class="label-text">{{ t('read_only') }}</span>
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<hr class="pb-0.5 mt-0.5">
|
<hr
|
||||||
<li>
|
v-if="field.type != 'stamp'"
|
||||||
|
class="pb-0.5 mt-0.5"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="isShowDescriptionModal = !isShowDescriptionModal"
|
@click="isShowDescriptionModal = !isShowDescriptionModal"
|
||||||
@@ -271,7 +316,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="isShowConditionsModal = !isShowConditionsModal"
|
@click="isShowConditionsModal = !isShowConditionsModal"
|
||||||
@@ -664,7 +711,12 @@ export default {
|
|||||||
},
|
},
|
||||||
removeOption (option) {
|
removeOption (option) {
|
||||||
this.field.options.splice(this.field.options.indexOf(option), 1)
|
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||||
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
|
||||||
|
const optionIndex = this.field.areas.findIndex((a) => a.option_uuid === option.uuid)
|
||||||
|
|
||||||
|
if (optionIndex !== -1) {
|
||||||
|
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
align: 'Align',
|
||||||
|
left: 'Left',
|
||||||
|
right: 'Right',
|
||||||
|
center: 'Center',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
display_title: 'Display title',
|
display_title: 'Display title',
|
||||||
|
with_logo: 'With logo',
|
||||||
unchecked: 'Unchecked',
|
unchecked: 'Unchecked',
|
||||||
equal: 'Equal',
|
equal: 'Equal',
|
||||||
not_equal: 'Not equal',
|
not_equal: 'Not equal',
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<style>
|
<style></style>
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body dir="<%= yield&.match?(TextUtils::RTL_REGEXP) ? 'rtl' : 'auto' %>">
|
<body dir="<%= TextUtils.rtl?(yield) ? 'rtl' : 'auto' %>">
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<%= render partial: 'shared/mailer_attribution' %>
|
<%= render partial: 'shared/mailer_attribution' %>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% if template.submitters.size == 1 %>
|
<% if template.submitters.size == 1 %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<submitters-autocomplete data-field="email">
|
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 50 : 1) : nil %>">
|
||||||
<autoresize-textarea>
|
<submitters-autocomplete data-field="email" class="block relative">
|
||||||
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
<autoresize-textarea>
|
||||||
</autoresize-textarea>
|
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
||||||
</submitters-autocomplete>
|
</autoresize-textarea>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
</emails-textarea>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
<% else %>
|
<% else %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="space-y-4">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<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 %>%">
|
<% align = field.dig('preferences', 'align') %>
|
||||||
|
<field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" 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']) %>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<%= auto_link(simple_format(h(local_assigns[:content]))) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])))) %>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config %>
|
||||||
<%= render 'custom_content', content: ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter) %>
|
<%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.</p>
|
<p>"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config %>
|
||||||
<%= render 'custom_content', content: ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter, sig: @sig) %>
|
<%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter, sig: @sig %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<% if @email_config || @body.present? %>
|
<% if @email_config || @body.present? %>
|
||||||
<% body = (@body.presence || @email_config.value['body']).to_s %>
|
<% body = (@body.presence || @email_config.value['body']).to_s %>
|
||||||
<%= render 'custom_content', content: ReplaceEmailVariables.call(body, submitter: @submitter) %>
|
<%= render 'custom_content', content: body, submitter: @submitter %>
|
||||||
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
|
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
|
||||||
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ Rails.application.configure do
|
|||||||
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
|
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
|
||||||
|
|
||||||
config.lograge.custom_payload do |controller|
|
config.lograge.custom_payload do |controller|
|
||||||
params = controller.request.params || {}
|
params = controller.request.try(:params) || {}
|
||||||
|
|
||||||
{
|
{
|
||||||
fwd: controller.request.ip,
|
fwd: controller.request.ip,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
|
|
||||||
module HexaPDF
|
module HexaPDF
|
||||||
module Type
|
module Type
|
||||||
|
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
|
||||||
module AcroForm
|
module AcroForm
|
||||||
class Field
|
class Field
|
||||||
def field_value
|
def field_value
|
||||||
@@ -10,5 +10,34 @@ module HexaPDF
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# comparison of Integer with HexaPDF::PDFArray failed
|
||||||
|
class CIDFont < Font
|
||||||
|
private
|
||||||
|
|
||||||
|
def widths
|
||||||
|
cache(:widths) do
|
||||||
|
result = {}
|
||||||
|
index = 0
|
||||||
|
array = self[:W] || []
|
||||||
|
|
||||||
|
while index < array.size
|
||||||
|
entry = array[index]
|
||||||
|
value = array[index + 1]
|
||||||
|
|
||||||
|
if value.is_a?(Array) || value.is_a?(HexaPDF::PDFArray)
|
||||||
|
value.each_with_index { |width, i| result[entry + i] = width }
|
||||||
|
index += 2
|
||||||
|
else
|
||||||
|
width = array[index + 2]
|
||||||
|
entry.upto(value) { |cid| result[cid] = width }
|
||||||
|
index += 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -55,6 +55,7 @@ Rails.application.routes.draw do
|
|||||||
resource :user_signature, only: %i[edit update destroy]
|
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]
|
||||||
|
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
||||||
resource :testing_account, only: %i[show destroy]
|
resource :testing_account, only: %i[show destroy]
|
||||||
resources :testing_api_settings, only: %i[index]
|
resources :testing_api_settings, only: %i[index]
|
||||||
resource :templates_upload, only: %i[create]
|
resource :templates_upload, only: %i[create]
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ module Accounts
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_testing_user(account)
|
def find_or_create_testing_user(account)
|
||||||
user = User.find_by(account: account.testing_accounts)
|
user = User.where(role: :admin).order(:id).find_by(account: account.testing_accounts)
|
||||||
|
|
||||||
return user if user
|
return user if user
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module RateLimit
|
||||||
|
LimitApproached = Class.new(StandardError)
|
||||||
|
|
||||||
|
STORE = ActiveSupport::Cache::MemoryStore.new
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(key, limit:, ttl:, enabled: Docuseal.multitenant?)
|
||||||
|
return true unless enabled
|
||||||
|
|
||||||
|
value = STORE.increment(key, 1, expires_in: ttl)
|
||||||
|
|
||||||
|
raise LimitApproached if value > limit
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,25 +20,25 @@ module ReplaceEmailVariables
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(text, submitter:, tracking_event_type: 'click_email', sig: nil)
|
def call(text, submitter:, tracking_event_type: 'click_email', html_escape: false, sig: nil)
|
||||||
text = text.gsub(TEMPLATE_NAME) { submitter.template.name }
|
text = replace(text, TEMPLATE_NAME, html_escape:) { submitter.template.name }
|
||||||
text = text.gsub(TEMPLATE_ID) { submitter.template.id }
|
text = replace(text, TEMPLATE_ID, html_escape:) { submitter.template.id }
|
||||||
text = text.gsub(SUBMITTER_ID) { submitter.id }
|
text = replace(text, SUBMITTER_ID, html_escape:) { submitter.id }
|
||||||
text = text.gsub(SUBMITTER_SLUG) { submitter.slug }
|
text = replace(text, SUBMITTER_SLUG, html_escape:) { submitter.slug }
|
||||||
text = text.gsub(SUBMISSION_ID) { submitter.submission.id }
|
text = replace(text, SUBMISSION_ID, html_escape:) { submitter.submission.id }
|
||||||
text = text.gsub(SUBMITTER_EMAIL) { submitter.email }
|
text = replace(text, SUBMITTER_EMAIL, html_escape:) { submitter.email }
|
||||||
text = text.gsub(SUBMITTER_NAME) { submitter.name || submitter.email || submitter.phone }
|
text = replace(text, SUBMITTER_NAME, html_escape:) { submitter.name || submitter.email || submitter.phone }
|
||||||
text = text.gsub(SUBMITTER_LINK) { build_submitter_link(submitter, tracking_event_type) }
|
text = replace(text, SUBMITTER_LINK, html_escape:) { build_submitter_link(submitter, tracking_event_type) }
|
||||||
text = text.gsub(SUBMISSION_LINK) do
|
text = replace(text, SUBMISSION_LINK, html_escape:) do
|
||||||
submitter.submission ? build_submission_link(submitter.submission) : ''
|
submitter.submission ? build_submission_link(submitter.submission) : ''
|
||||||
end
|
end
|
||||||
text = text.gsub(SUBMISSION_SUBMITTERS) { build_submission_submitters(submitter.submission) }
|
text = replace(text, SUBMISSION_SUBMITTERS, html_escape:) { build_submission_submitters(submitter.submission) }
|
||||||
text = text.gsub(DOCUMENTS_LINKS) { build_documents_links_text(submitter, sig) }
|
text = replace(text, DOCUMENTS_LINKS, html_escape:) { build_documents_links_text(submitter, sig) }
|
||||||
text = text.gsub(DOCUMENTS_LINK) { build_documents_links_text(submitter, sig) }
|
text = replace(text, DOCUMENTS_LINK, html_escape:) { build_documents_links_text(submitter, sig) }
|
||||||
text = text.gsub(ACCOUNT_NAME) { submitter.account.name }
|
text = replace(text, ACCOUNT_NAME, html_escape:) { submitter.submission.account.name }
|
||||||
text = text.gsub(SENDER_NAME) { submitter.submission.created_by_user&.full_name }
|
text = replace(text, SENDER_NAME, html_escape:) { submitter.submission.created_by_user&.full_name }
|
||||||
|
|
||||||
text.gsub(SENDER_EMAIL) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
|
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
@@ -48,6 +48,16 @@ module ReplaceEmailVariables
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replace(text, var, html_escape: false)
|
||||||
|
text.gsub(var) do
|
||||||
|
if html_escape
|
||||||
|
ERB::Util.html_escape(yield)
|
||||||
|
else
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def build_submitter_link(submitter, tracking_event_type)
|
def build_submitter_link(submitter, tracking_event_type)
|
||||||
if tracking_event_type == 'click_email'
|
if tracking_event_type == 'click_email'
|
||||||
Rails.application.routes.url_helpers.submit_form_url(
|
Rails.application.routes.url_helpers.submit_form_url(
|
||||||
|
|||||||
@@ -125,9 +125,14 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_submitter_uuid(template, attrs, index)
|
def find_submitter_uuid(template, attrs, index)
|
||||||
attrs[:uuid].presence ||
|
uuid = attrs[:uuid].presence
|
||||||
template.submitters.find { |e| e['name'] == attrs[:role] }&.dig('uuid') ||
|
uuid ||= template.submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
|
||||||
template.submitters[index]&.dig('uuid')
|
|
||||||
|
if attrs[:role].present? && uuid.blank? && defined?(Rollbar)
|
||||||
|
Rollbar.error("Role doesn't existng: #{attrs[:role]}, #{template.id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
uuid || template.submitters[index]&.dig('uuid')
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:, user:, preferences:)
|
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:, user:, preferences:)
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ module Submissions
|
|||||||
|
|
||||||
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
||||||
composer.document.fonts.add(FONT_BOLD_NAME, variant: :bold)
|
composer.document.fonts.add(FONT_BOLD_NAME, variant: :bold)
|
||||||
|
composer.document.config['font.on_missing_glyph'] =
|
||||||
|
Submissions::GenerateResultAttachments.method(:on_missing_glyph).to_proc
|
||||||
|
|
||||||
divider = HexaPDF::Layout::Box.create(
|
divider = HexaPDF::Layout::Box.create(
|
||||||
margin: [0, 0, 15, 0],
|
margin: [0, 0, 15, 0],
|
||||||
@@ -245,7 +247,6 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
value = value.join(', ') if value.is_a?(Array)
|
value = value.join(', ') if value.is_a?(Array)
|
||||||
value = Submissions::GenerateResultAttachments::REPLACE_EMOJI[value] || value
|
|
||||||
|
|
||||||
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
||||||
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
|
|||||||
@@ -21,9 +21,18 @@ module Submissions
|
|||||||
A4_SIZE = [595, 842].freeze
|
A4_SIZE = [595, 842].freeze
|
||||||
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
||||||
|
|
||||||
REPLACE_EMOJI = {
|
MISSING_GLYPH_REPLACE = {
|
||||||
|
'▪' => '-',
|
||||||
'✔️' => 'V',
|
'✔️' => 'V',
|
||||||
'✔' => 'V'
|
'✔' => 'V',
|
||||||
|
'✅' => 'V'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
MISSING_GLYPH_REPLACE_TYPE1 = {
|
||||||
|
'▪' => :bullet,
|
||||||
|
'✔️' => :V,
|
||||||
|
'✔' => :V,
|
||||||
|
'✅' => :V
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
@@ -62,12 +71,14 @@ module Submissions
|
|||||||
|
|
||||||
width = page.box.width
|
width = page.box.width
|
||||||
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, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center,
|
text_align = field.dig('preferences', 'align').to_s.to_sym.presence ||
|
||||||
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
(value.to_s.match?(RTL_REGEXP) ? :right : :left)
|
||||||
|
|
||||||
|
layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align:,
|
||||||
font: pdf.fonts.add(FONT_NAME), font_size:)
|
font: pdf.fonts.add(FONT_NAME), font_size:)
|
||||||
|
|
||||||
next if Array.wrap(value).compact_blank.blank?
|
next if Array.wrap(value).compact_blank.blank?
|
||||||
@@ -181,7 +192,6 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
||||||
value = REPLACE_EMOJI[value] || value
|
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size:)
|
font_size:)
|
||||||
@@ -194,15 +204,23 @@ module Submissions
|
|||||||
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)
|
||||||
|
|
||||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||||
|
|
||||||
box_height = lines.sum(&:height)
|
box_height = lines.sum(&:height)
|
||||||
end
|
end
|
||||||
|
|
||||||
height_diff = [0, box_height - (area['h'] * height)].max
|
height_diff = [0, box_height - (area['h'] * height)].max
|
||||||
|
|
||||||
layouter.fit([text], area['w'] * width, height_diff.positive? ? box_height : area['h'] * height)
|
right_align_x_adjustment =
|
||||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
if field['type'].in?(%w[date number]) && text_align != :left
|
||||||
|
(width - (area['w'] * width)) / (text_align == :center ? 2.0 : 1)
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
|
|
||||||
|
layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width,
|
||||||
|
height_diff.positive? ? box_height : area['h'] * height)
|
||||||
|
.draw(canvas, (area['x'] * width) - right_align_x_adjustment + TEXT_LEFT_MARGIN,
|
||||||
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN)
|
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -211,28 +229,28 @@ module Submissions
|
|||||||
image_pdfs = []
|
image_pdfs = []
|
||||||
original_documents = template.documents.preload(:blob)
|
original_documents = template.documents.preload(:blob)
|
||||||
|
|
||||||
results =
|
result_attachments =
|
||||||
submitter.submission.template_schema.map do |item|
|
submitter.submission.template_schema.map do |item|
|
||||||
pdf = pdfs_index[item['attachment_uuid']]
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
attachment = save_pdf(pdf:, submitter:, pkcs:, tsa_url:,
|
attachment = build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:,
|
||||||
uuid: item['attachment_uuid'],
|
uuid: item['attachment_uuid'],
|
||||||
name: item['name'])
|
name: item['name'])
|
||||||
|
|
||||||
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
||||||
|
|
||||||
attachment
|
attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
return results if image_pdfs.size < 2
|
return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2
|
||||||
|
|
||||||
images_pdf =
|
images_pdf =
|
||||||
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
||||||
pdf.pages.each { |page| doc.pages << doc.import(page) }
|
pdf.pages.each { |page| doc.pages << doc.import(page) }
|
||||||
end
|
end
|
||||||
|
|
||||||
images_pdf_result =
|
images_pdf_attachment =
|
||||||
save_pdf(
|
build_pdf_attachment(
|
||||||
pdf: images_pdf,
|
pdf: images_pdf,
|
||||||
submitter:,
|
submitter:,
|
||||||
tsa_url:,
|
tsa_url:,
|
||||||
@@ -241,11 +259,10 @@ module Submissions
|
|||||||
name: template.name
|
name: template.name
|
||||||
)
|
)
|
||||||
|
|
||||||
results + [images_pdf_result]
|
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
|
||||||
|
|
||||||
def save_pdf(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
|
def build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
pdf.trailer.info[:Creator] = info_creator
|
pdf.trailer.info[:Creator] = info_creator
|
||||||
@@ -265,12 +282,25 @@ module Submissions
|
|||||||
sign_params[:signature_size] = 10_000
|
sign_params[:signature_size] = 10_000
|
||||||
end
|
end
|
||||||
|
|
||||||
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
begin
|
||||||
else
|
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
||||||
pdf.write(io, incremental: true, validate: false)
|
rescue HexaPDF::MalformedPDFError => e
|
||||||
end
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
pdf.write(io, incremental: true, validate: false)
|
||||||
|
rescue HexaPDF::MalformedPDFError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
pdf.write(io, incremental: false, validate: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.new(
|
||||||
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(io.string), filename: "#{name}.pdf"),
|
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(io.string), filename: "#{name}.pdf"),
|
||||||
metadata: { original_uuid: uuid,
|
metadata: { original_uuid: uuid,
|
||||||
analyzed: true,
|
analyzed: true,
|
||||||
@@ -306,10 +336,25 @@ module Submissions
|
|||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pdf.config['font.on_missing_glyph'] = method(:on_missing_glyph).to_proc
|
||||||
|
|
||||||
[attachment.metadata['original_uuid'] || attachment.uuid, pdf]
|
[attachment.metadata['original_uuid'] || attachment.uuid, pdf]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_missing_glyph(character, font_wrapper)
|
||||||
|
Rollbar.error("Missing glyph: #{character}") if character.present? && defined?(Rollbar)
|
||||||
|
|
||||||
|
replace_with =
|
||||||
|
if font_wrapper.font_type == :Type1
|
||||||
|
MISSING_GLYPH_REPLACE_TYPE1[character] || :space
|
||||||
|
else
|
||||||
|
(MISSING_GLYPH_REPLACE[character] || ' ').bytes.first - 29
|
||||||
|
end
|
||||||
|
|
||||||
|
font_wrapper.custom_glyph(replace_with, character)
|
||||||
|
end
|
||||||
|
|
||||||
def find_last_submitter(submitter)
|
def find_last_submitter(submitter)
|
||||||
submitter.submission.submitters
|
submitter.submission.submitters
|
||||||
.select(&:completed_at?)
|
.select(&:completed_at?)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ module Submitters
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter)
|
def call(submitter, with_logo: true)
|
||||||
image = generate_stamp_image(submitter)
|
image = generate_stamp_image(submitter, with_logo:)
|
||||||
|
|
||||||
image_data = image.write_to_buffer('.png')
|
image_data = image.write_to_buffer('.png')
|
||||||
|
|
||||||
@@ -30,8 +30,13 @@ module Submitters
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_stamp_image(submitter)
|
def generate_stamp_image(submitter, with_logo: true)
|
||||||
logo = Vips::Image.new_from_buffer(load_logo(submitter).read, '')
|
logo =
|
||||||
|
if with_logo
|
||||||
|
Vips::Image.new_from_buffer(load_logo(submitter).read, '')
|
||||||
|
else
|
||||||
|
Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
|
||||||
|
end
|
||||||
|
|
||||||
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
|
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ module Submitters
|
|||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
if field['type'] == 'stamp'
|
if field['type'] == 'stamp'
|
||||||
acc[field['uuid']] ||= Submitters::CreateStampAttachment.call(submitter).uuid
|
acc[field['uuid']] ||=
|
||||||
|
Submitters::CreateStampAttachment.call(submitter,
|
||||||
|
with_logo: field.dig('preferences', 'with_logo') != false).uuid
|
||||||
|
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ module Templates
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(data)
|
def call(data)
|
||||||
pdf = PDF::Reader.new(StringIO.new(data))
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
pdf.pages.flat_map.with_index do |page, index|
|
pdf.pages.flat_map.with_index do |page, index|
|
||||||
annotations = page.objects.deref!(page.attributes[:Annots]) || []
|
(page[:Annots] || []).filter_map do |annot|
|
||||||
annotations.filter_map do |annot|
|
|
||||||
next if annot.nil?
|
|
||||||
next if annot[:A].blank? || annot[:A][:URI].blank?
|
next if annot[:A].blank? || annot[:A][:URI].blank?
|
||||||
next unless annot[:Subtype] == :Link
|
next unless annot[:Subtype] == :Link
|
||||||
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
||||||
@@ -30,10 +28,10 @@ module Templates
|
|||||||
{
|
{
|
||||||
'type' => 'external_link',
|
'type' => 'external_link',
|
||||||
'value' => annot[:A][:URI],
|
'value' => annot[:A][:URI],
|
||||||
'x' => left / page.width.to_f,
|
'x' => left / page.box.width.to_f,
|
||||||
'y' => (page.height - top) / page.height.to_f,
|
'y' => (page.box.height - top) / page.box.height.to_f,
|
||||||
'w' => (right - left) / page.width.to_f,
|
'w' => (right - left) / page.box.width.to_f,
|
||||||
'h' => (top - bottom) / page.height.to_f
|
'h' => (top - bottom) / page.box.height.to_f
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ module TextUtils
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
def rtl?(text)
|
||||||
|
return false if text.blank?
|
||||||
|
|
||||||
|
text.match?(TextUtils::RTL_REGEXP)
|
||||||
|
rescue Encoding::CompatibilityError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_rtl_reverse(text)
|
def maybe_rtl_reverse(text)
|
||||||
if text.match?(RTL_REGEXP)
|
if text.match?(RTL_REGEXP)
|
||||||
TwitterCldr::Shared::Bidi
|
TwitterCldr::Shared::Bidi
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module.exports = {
|
|||||||
'./app/views/start_form/**/*.erb',
|
'./app/views/start_form/**/*.erb',
|
||||||
'./app/views/shared/_button_title.html.erb',
|
'./app/views/shared/_button_title.html.erb',
|
||||||
'./app/views/shared/_attribution.html.erb',
|
'./app/views/shared/_attribution.html.erb',
|
||||||
|
'./app/views/scripts/_autosize_field.html.erb',
|
||||||
'./app/views/send_submission_email/**/*.erb'
|
'./app/views/send_submission_email/**/*.erb'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user