mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 23:32:07 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21299dc65f | |||
| 0df0946c2a | |||
| 1bb38d50a1 | |||
| 242987cb41 | |||
| 5456abde54 | |||
| 4a07427440 | |||
| cf60dae2ff | |||
| 659997c6c1 | |||
| 389a47a152 | |||
| b6b81ca487 | |||
| 719c1786f1 | |||
| 4685bac4b3 | |||
| a05275cc08 | |||
| 1abf1b1658 | |||
| 8893dc5698 | |||
| f8467b2da1 | |||
| f39b8b000e | |||
| 961036fa31 | |||
| 8b9056894e | |||
| 7f979e9396 | |||
| 8db1192d0e | |||
| 3bd515951a | |||
| 2bd5b9676b |
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv-safe'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
|
||||
+9
-4
@@ -150,6 +150,8 @@ GEM
|
||||
css_parser (1.21.0)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.15.1)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.15.0)
|
||||
@@ -271,10 +273,11 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.0.3)
|
||||
hexapdf (1.4.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
strscan (>= 3.1.2)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.7)
|
||||
@@ -288,7 +291,7 @@ GEM
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
json (2.13.2)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
@@ -353,13 +356,13 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.8)
|
||||
oj (3.16.11)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
openssl (3.3.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
ostruct (0.6.3)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.26.3)
|
||||
@@ -548,6 +551,7 @@ GEM
|
||||
stringio (3.1.7)
|
||||
strip_attributes (1.14.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
strscan (3.1.5)
|
||||
thor (1.4.0)
|
||||
timeout (0.4.3)
|
||||
trailblazer-option (0.1.2)
|
||||
@@ -610,6 +614,7 @@ DEPENDENCIES
|
||||
cancancan
|
||||
capybara
|
||||
csv
|
||||
csv-safe
|
||||
cuprite
|
||||
debug
|
||||
devise
|
||||
|
||||
@@ -189,7 +189,7 @@ module Api
|
||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :required, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }] }]]
|
||||
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -111,9 +111,10 @@ module Api
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -122,9 +122,10 @@ class TemplatesController < ApplicationController
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
)
|
||||
end
|
||||
|
||||
@@ -29,8 +29,15 @@
|
||||
{{ optionValue(option) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ field.title || field.name || fieldNames[field.type] }}
|
||||
<template v-if="field.type === 'checkbox' && !field.name">
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:text-only="true"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ field.name || fieldNames[field.type] }}
|
||||
</template>
|
||||
<template v-if="field.type === 'checkbox' && !field.name && !field.title">
|
||||
{{ fieldIndex + 1 }}
|
||||
</template>
|
||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||
@@ -222,12 +229,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
components: {
|
||||
IconPaperclip,
|
||||
MarkdownContent,
|
||||
IconCheck
|
||||
},
|
||||
inject: ['t'],
|
||||
@@ -422,10 +431,14 @@ export default {
|
||||
},
|
||||
formattedDate () {
|
||||
if (this.field.type === 'date' && this.modelValue) {
|
||||
return this.formatDate(
|
||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
)
|
||||
try {
|
||||
return this.formatDate(
|
||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
)
|
||||
} catch {
|
||||
return this.modelValue
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="space-y-3 mt-5">
|
||||
<a
|
||||
v-if="completedButton.url"
|
||||
:href="sanitizeHref(completedButton.url)"
|
||||
:href="sanitizeUrl(completedButton.url)"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="white-button flex items-center w-full completed-form-completed-button"
|
||||
>
|
||||
@@ -102,6 +102,7 @@
|
||||
<script>
|
||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
export default {
|
||||
name: 'FormCompleted',
|
||||
@@ -198,6 +199,7 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
sanitizeUrl,
|
||||
sendCopyToEmail () {
|
||||
this.isSendingCopy = true
|
||||
|
||||
@@ -252,11 +254,6 @@ export default {
|
||||
this.isDownloading = false
|
||||
})
|
||||
},
|
||||
sanitizeHref (href) {
|
||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||
return href.replace(/javascript:/g, '')
|
||||
}
|
||||
},
|
||||
downloadSafariIos (urls) {
|
||||
const fileRequests = urls.map((url) => {
|
||||
return fetch(url).then(async (resp) => {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</template>
|
||||
</label>
|
||||
<button
|
||||
v-if="withToday"
|
||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||
>
|
||||
@@ -46,6 +47,8 @@
|
||||
:id="field.uuid"
|
||||
ref="input"
|
||||
v-model="value"
|
||||
:min="validationMin"
|
||||
:max="validationMax"
|
||||
class="base-input !text-2xl text-center w-full"
|
||||
:required="field.required"
|
||||
type="date"
|
||||
@@ -89,6 +92,44 @@ export default {
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'submit'],
|
||||
computed: {
|
||||
dateNowString () {
|
||||
const today = new Date()
|
||||
|
||||
const yyyy = today.getFullYear()
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(today.getDate()).padStart(2, '0')
|
||||
|
||||
return `${yyyy}-${mm}-${dd}`
|
||||
},
|
||||
validationMin () {
|
||||
if (this.field.validation?.min) {
|
||||
return ['{{date}}', '{date}'].includes(this.field.validation.min) ? this.dateNowString : this.field.validation.min
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
validationMax () {
|
||||
if (this.field.validation?.max) {
|
||||
return ['{{date}}', '{date}'].includes(this.field.validation.max) ? this.dateNowString : this.field.validation.max
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
withToday () {
|
||||
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.validationMin) {
|
||||
if (new Date(this.validationMin).setHours(0, 0, 0, 0) <= todayDate) {
|
||||
return this.validationMax ? (new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate) : true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if (this.validationMax) {
|
||||
return new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
value: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
|
||||
@@ -572,6 +572,7 @@ import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import AppearsOn from './appears_on'
|
||||
import i18n from './i18n'
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
const isEmpty = (obj) => {
|
||||
if (obj == null) return true
|
||||
@@ -1476,7 +1477,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.completedRedirectUrl) {
|
||||
window.location.href = this.completedRedirectUrl
|
||||
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,13 @@ export default {
|
||||
computed: {
|
||||
isInlineSize () {
|
||||
return CSS.supports('container-type: size')
|
||||
},
|
||||
fieldsUuidIndex () {
|
||||
return this.fields.reduce((acc, field) => {
|
||||
acc[field.uuid] = field
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
@@ -96,8 +103,19 @@ export default {
|
||||
findPageElementForArea (area) {
|
||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||
},
|
||||
normalizeFormula (formula, depth = 0) {
|
||||
if (depth > 10) return formula
|
||||
|
||||
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
if (this.fieldsUuidIndex[uuid]) {
|
||||
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||
} else {
|
||||
return match
|
||||
}
|
||||
})
|
||||
},
|
||||
calculateFormula (field) {
|
||||
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||
})
|
||||
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
<template>
|
||||
<span>
|
||||
<template
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<a
|
||||
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:class="extractAttr(item, 'class') || 'link'"
|
||||
target="_blank"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</a>
|
||||
<b
|
||||
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</b>
|
||||
<i
|
||||
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</i>
|
||||
<br
|
||||
v-else-if="item === '<br>' || item === '\n'"
|
||||
>
|
||||
<span v-if="textOnly">
|
||||
{{ dom.body.textContent }}
|
||||
</span>
|
||||
<template v-else>
|
||||
<template
|
||||
v-else
|
||||
v-for="(item, index) in nodes || dom.body.childNodes"
|
||||
:key="index"
|
||||
>
|
||||
{{ item }}
|
||||
<a
|
||||
v-if="item.tagName === 'A' && item.getAttribute('href') !== 'undefined'"
|
||||
:href="sanitizeUrl(item.getAttribute('href'))"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:class="item.getAttribute('class') || 'link'"
|
||||
target="_blank"
|
||||
>
|
||||
<MarkdownContent :nodes="item.childNodes" />
|
||||
</a>
|
||||
<component
|
||||
:is="item.tagName"
|
||||
v-else-if="safeTags.includes(item.tagName)"
|
||||
>
|
||||
<MarkdownContent :nodes="item.childNodes" />
|
||||
</component>
|
||||
<br v-else-if="item.tagName === 'BR' || item.nodeValue === '\n'">
|
||||
<template v-else>
|
||||
{{ item.textContent }}
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</span>
|
||||
@@ -37,8 +34,7 @@
|
||||
|
||||
<script>
|
||||
import snarkdown from 'snarkdown'
|
||||
|
||||
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
export default {
|
||||
name: 'MarkdownContent',
|
||||
@@ -47,39 +43,30 @@ export default {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
nodes: {
|
||||
type: [Array, Object],
|
||||
require: false,
|
||||
default: null
|
||||
},
|
||||
textOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items () {
|
||||
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
||||
safeTags () {
|
||||
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
|
||||
},
|
||||
dom () {
|
||||
const text = this.string.replace(/(?<!\(\s*)(https?:\/\/[^\s)]+)(?!\s*\))/g, (url) => `[${url}](${url})`)
|
||||
|
||||
const text = linkParts.map((part, index) => {
|
||||
if (part.match(/^https?:\/\//) && !linkParts[index - 1]?.match(/\(\s*$/) && !linkParts[index + 1]?.match(/^\s*\)/)) {
|
||||
return `[${part}](${part})`
|
||||
} else {
|
||||
return part
|
||||
}
|
||||
}).join('')
|
||||
|
||||
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
||||
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sanitizeHref (href) {
|
||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||
return href.replace(/javascript:/g, '')
|
||||
}
|
||||
},
|
||||
extractAttr (text, attr) {
|
||||
if (text.includes(attr)) {
|
||||
return text.split(attr).pop().split('"')[1]
|
||||
}
|
||||
},
|
||||
extractText (text) {
|
||||
if (text) {
|
||||
return text.match(/>(.+?)</)?.[1]
|
||||
}
|
||||
}
|
||||
sanitizeUrl
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,8 +39,10 @@
|
||||
:id="field.uuid"
|
||||
v-model="number"
|
||||
type="number"
|
||||
:step="field.validation?.step || 'any'"
|
||||
:min="field.validation?.min"
|
||||
:max="field.validation?.max"
|
||||
class="base-input !text-2xl w-full"
|
||||
step="any"
|
||||
:required="field.required"
|
||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:name="`values[${field.uuid}]`"
|
||||
|
||||
@@ -72,11 +72,11 @@
|
||||
@blur="onNameBlur"
|
||||
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
||||
<div
|
||||
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||
class="flex items-center ml-1.5"
|
||||
>
|
||||
<input
|
||||
v-if="!isValueInput"
|
||||
v-if="!isValueInput && !isSelectInput"
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
@@ -84,14 +84,14 @@
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
v-if="!isValueInput"
|
||||
v-if="!isValueInput && !isSelectInput"
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>{{ t('required') }}</label>
|
||||
<input
|
||||
v-if="isValueInput"
|
||||
v-if="isValueInput || isSelectInput"
|
||||
:id="`readonly-checkbox-${field.uuid}`"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@@ -100,7 +100,7 @@
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
v-if="isValueInput"
|
||||
v-if="isValueInput || isSelectInput"
|
||||
:for="`readonly-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
||||
@@ -164,30 +164,39 @@
|
||||
ref="touchValueTarget"
|
||||
class="flex h-full w-full field-area"
|
||||
dir="auto"
|
||||
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||
@click="focusValueInput"
|
||||
>
|
||||
<span
|
||||
v-if="field"
|
||||
class="flex justify-center items-center space-x-1"
|
||||
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
|
||||
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && !isDefaultValuePresent }"
|
||||
>
|
||||
<div
|
||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||
v-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||
:class="{ 'w-full h-full': isWFullType }"
|
||||
:style="fontStyle"
|
||||
>
|
||||
<div
|
||||
ref="textContainer"
|
||||
class="flex items-center px-0.5"
|
||||
:style="{ color: field.preferences?.color }"
|
||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||
:class="{ 'w-full h-full': isWFullType }"
|
||||
>
|
||||
<IconCheck
|
||||
v-if="field.type == 'checkbox'"
|
||||
class="aspect-square mx-auto"
|
||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||
/>
|
||||
<template
|
||||
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
|
||||
>
|
||||
<IconCheck
|
||||
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
|
||||
class="aspect-square mx-auto"
|
||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
||||
class="whitespace-pre-wrap"
|
||||
@@ -210,14 +219,39 @@
|
||||
{{ char }}
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
v-else-if="isSelectInput"
|
||||
ref="defaultValueSelect"
|
||||
class="bg-transparent outline-none focus:outline-none w-full"
|
||||
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>
|
||||
<option
|
||||
:disabled="!field.default_value?.length"
|
||||
:selected="!field.default_value?.length"
|
||||
:value="''"
|
||||
>
|
||||
{{ t(field.default_value?.length ? 'none' : 'select') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(option, index) in field.options"
|
||||
:key="index"
|
||||
:selected="field.default_value === option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.value }}
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
v-else
|
||||
ref="defaultValue"
|
||||
:contenteditable="isValueInput"
|
||||
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
|
||||
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
||||
:class="{ 'cursor-text': isValueInput }"
|
||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
|
||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
||||
@blur="onDefaultValueBlur"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@paste.prevent="onPaste"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>{{ field.default_value }}</span>
|
||||
@@ -225,7 +259,7 @@
|
||||
</div>
|
||||
<component
|
||||
:is="fieldIcons[field.type]"
|
||||
v-else
|
||||
v-else-if="!isCheckboxInput"
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="max-h-10 opacity-50"
|
||||
@@ -233,12 +267,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isValueInput"
|
||||
v-if="!isValueInput && !isSelectInput"
|
||||
ref="touchTarget"
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
||||
@dblclick="maybeToggleDefaultValue"
|
||||
@click="maybeToggleCheckboxValue"
|
||||
@click="inputMode && maybeToggleCheckboxValue()"
|
||||
/>
|
||||
<span
|
||||
v-if="field?.type && editable"
|
||||
@@ -253,6 +287,7 @@
|
||||
<FormulaModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
@@ -264,6 +299,7 @@
|
||||
<FontModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
@@ -275,6 +311,7 @@
|
||||
<ConditionsModal
|
||||
:item="field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:default-field="defaultField"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -285,6 +322,7 @@
|
||||
<DescriptionModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
@@ -398,6 +436,9 @@ export default {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
isWFullType () {
|
||||
return ['cells', 'checkbox', 'radio', 'multiple', 'select'].includes(this.field.type)
|
||||
},
|
||||
fontStyle () {
|
||||
let fontSize = ''
|
||||
|
||||
@@ -417,6 +458,13 @@ export default {
|
||||
|
||||
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||
},
|
||||
optionsUuidIndex () {
|
||||
return this.field.options.reduce((acc, option) => {
|
||||
acc[option.uuid] = option
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
fontSizePx () {
|
||||
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||
},
|
||||
@@ -427,14 +475,17 @@ export default {
|
||||
return 1040 / 612.0
|
||||
},
|
||||
isDefaultValuePresent () {
|
||||
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
||||
return false
|
||||
} else {
|
||||
return this.field?.default_value || this.field?.default_value === 0
|
||||
}
|
||||
return this.field?.default_value || this.field?.default_value === 0
|
||||
},
|
||||
isSelectInput () {
|
||||
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
||||
},
|
||||
isCheckboxInput () {
|
||||
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
||||
},
|
||||
isValueInput () {
|
||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
|
||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
||||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
||||
},
|
||||
modalContainerEl () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
@@ -549,6 +600,11 @@ export default {
|
||||
closeDropdown () {
|
||||
this.$el.getRootNode().activeElement.blur()
|
||||
},
|
||||
buildAreaOptionValue (area) {
|
||||
const option = this.optionsUuidIndex[area.option_uuid]
|
||||
|
||||
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||
},
|
||||
maybeToggleDefaultValue () {
|
||||
if (!this.editable) {
|
||||
return
|
||||
@@ -558,22 +614,41 @@ export default {
|
||||
this.isContenteditable = true
|
||||
|
||||
this.focusValueInput()
|
||||
} else if (this.field.type === 'checkbox') {
|
||||
this.field.readonly = !this.field.readonly
|
||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||
|
||||
this.save()
|
||||
} else if (this.field.type === 'date') {
|
||||
this.field.readonly = !this.field.readonly
|
||||
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
||||
|
||||
this.save()
|
||||
} else {
|
||||
this.maybeToggleCheckboxValue()
|
||||
}
|
||||
},
|
||||
maybeToggleCheckboxValue () {
|
||||
if (this.inputMode && this.field.type === 'checkbox') {
|
||||
this.field.readonly = !this.field.readonly
|
||||
if (this.field.type === 'checkbox') {
|
||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||
this.field.readonly = this.field.default_value === true
|
||||
|
||||
this.save()
|
||||
} else if (this.field.type === 'radio' && this.area.option_uuid) {
|
||||
const value = this.buildAreaOptionValue(this.area)
|
||||
|
||||
this.field.default_value === value ? delete this.field.default_value : this.field.default_value = value
|
||||
|
||||
this.field.readonly = !!this.field.default_value?.length
|
||||
|
||||
this.save()
|
||||
} else if (this.field.type === 'multiple' && this.area.option_uuid) {
|
||||
const value = this.buildAreaOptionValue(this.area)
|
||||
|
||||
if (this.field.default_value?.includes(value)) {
|
||||
this.field.default_value.splice(this.field.default_value.indexOf(value), 1)
|
||||
|
||||
if (!this.field.default_value?.length) delete this.field.default_value
|
||||
} else {
|
||||
Array.isArray(this.field.default_value) ? this.field.default_value.push(value) : this.field.default_value = [value]
|
||||
}
|
||||
|
||||
this.field.readonly = !!this.field.default_value?.length
|
||||
|
||||
this.save()
|
||||
}
|
||||
@@ -749,7 +824,7 @@ export default {
|
||||
}
|
||||
},
|
||||
drag (e) {
|
||||
if (e.target.id === 'mask') {
|
||||
if (e.target.id === 'mask' && this.editable) {
|
||||
this.isDragged = true
|
||||
|
||||
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||
@@ -765,7 +840,9 @@ export default {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
this.isDragged = true
|
||||
if (this.editable) {
|
||||
this.isDragged = true
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
@@ -818,7 +895,9 @@ export default {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
this.isDragged = true
|
||||
if (this.editable) {
|
||||
this.isDragged = true
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
|
||||
@@ -1065,7 +1065,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
if (type === 'stamp') {
|
||||
@@ -1109,7 +1109,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
if (type === 'stamp') {
|
||||
@@ -1301,7 +1301,13 @@ export default {
|
||||
this.copiedArea.option_uuid ||= field.options[0].uuid
|
||||
area.option_uuid = v4()
|
||||
|
||||
field.options.push({ uuid: area.option_uuid })
|
||||
const lastOption = field.options[field.options.length - 1]
|
||||
|
||||
if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
|
||||
area.option_uuid = lastOption.uuid
|
||||
} else {
|
||||
field.options.push({ uuid: area.option_uuid })
|
||||
}
|
||||
|
||||
field.areas.push(area)
|
||||
} else {
|
||||
@@ -1477,7 +1483,7 @@ export default {
|
||||
if (this.dragField?.options?.length) {
|
||||
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
|
||||
} else {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span class="modal-title">
|
||||
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
|
||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -160,6 +160,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
defaultField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span class="modal-title">
|
||||
{{ field.name || buildDefaultName(field, template.fields) }}
|
||||
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -73,6 +73,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
defaultField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="field.options && withOptions"
|
||||
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
||||
ref="options"
|
||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||
draggable="true"
|
||||
@@ -216,12 +216,30 @@
|
||||
/>
|
||||
<button
|
||||
v-else-if="field.options && editable && !defaultField"
|
||||
class="text-center text-sm w-full pb-1"
|
||||
class="field-add-option text-center text-sm w-full pb-1"
|
||||
@click="addOption"
|
||||
>
|
||||
+ {{ t('add_option') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.options && withOptions && !isExpandOptions && field.options.length > 4"
|
||||
class="border-t border-base-300 mx-2 space-y-1.5"
|
||||
>
|
||||
<button
|
||||
class="field-expand-options text-center text-sm w-full py-1 flex space-x-0.5 justify-center items-center"
|
||||
@click="isExpandOptions = true"
|
||||
>
|
||||
<span class="lowercase">
|
||||
{{ field.options.length }} {{ t('options') }}
|
||||
</span>
|
||||
<IconChevronDown
|
||||
class="ml-2 mr-2 mt-0.5"
|
||||
width="15"
|
||||
height="15"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="isShowFormulaModal"
|
||||
@@ -230,6 +248,7 @@
|
||||
<FormulaModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
@@ -241,6 +260,7 @@
|
||||
<FontModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
@@ -251,6 +271,7 @@
|
||||
>
|
||||
<ConditionsModal
|
||||
:item="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
@@ -262,6 +283,7 @@
|
||||
<DescriptionModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
@@ -278,7 +300,7 @@ import FormulaModal from './formula_modal'
|
||||
import FontModal from './font_modal'
|
||||
import ConditionsModal from './conditions_modal'
|
||||
import DescriptionModal from './description_modal'
|
||||
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
|
||||
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
@@ -288,6 +310,7 @@ export default {
|
||||
IconSettings,
|
||||
FieldSettings,
|
||||
PaymentSettings,
|
||||
IconChevronDown,
|
||||
IconNewSection,
|
||||
FormulaModal,
|
||||
FontModal,
|
||||
@@ -333,6 +356,7 @@ export default {
|
||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||
data () {
|
||||
return {
|
||||
isExpandOptions: false,
|
||||
isNameFocus: false,
|
||||
showPaymentModal: false,
|
||||
isShowFormulaModal: false,
|
||||
@@ -427,6 +451,8 @@ export default {
|
||||
this.$el.getRootNode().activeElement.blur()
|
||||
},
|
||||
addOption () {
|
||||
this.isExpandOptions = true
|
||||
|
||||
this.field.options.push({ value: '', uuid: v4() })
|
||||
|
||||
this.$nextTick(() => {
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="field.type === 'number'"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
<select
|
||||
:placeholder="t('format')"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
||||
>
|
||||
<option
|
||||
v-for="format in numberFormats"
|
||||
:key="format"
|
||||
:value="format"
|
||||
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
||||
>
|
||||
{{ formatNumber(123456789.567, format) }}
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('format') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="field.type === 'verification'"
|
||||
class="py-1.5 px-1 relative"
|
||||
@@ -217,6 +190,77 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="field.type === 'number'"
|
||||
class="py-1.5 px-1 relative flex space-x-1"
|
||||
@click.stop
|
||||
>
|
||||
<div class="w-1/2 relative">
|
||||
<input
|
||||
:placeholder="t('min')"
|
||||
type="number"
|
||||
min="0"
|
||||
:value="field.validation?.min"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation?.min"
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('min') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-1/2 relative">
|
||||
<input
|
||||
:placeholder="t('max')"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
:value="field.validation?.max"
|
||||
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation?.max"
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('max') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="field.type === 'number'"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
<select
|
||||
:placeholder="t('format')"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
||||
>
|
||||
<option
|
||||
v-for="format in numberFormats"
|
||||
:key="format"
|
||||
:value="format"
|
||||
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
||||
>
|
||||
{{ formatNumber(123456789.567, format) }}
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('format') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||
class="py-1.5 px-1 relative"
|
||||
@@ -393,7 +437,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||
v-if="['text', 'number'].includes(field.type)"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
@@ -401,6 +445,7 @@
|
||||
v-model="field.readonly"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
|
||||
@update:model-value="save"
|
||||
>
|
||||
<span class="label-text">{{ t('read_only') }}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span class="modal-title">
|
||||
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
||||
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -202,6 +202,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
defaultField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span class="modal-title">
|
||||
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
||||
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -28,14 +28,21 @@
|
||||
class="link"
|
||||
>{{ t('available_in_pro') }}</a>
|
||||
</div>
|
||||
<div class="flex-inline mb-2 gap-2 space-y-1">
|
||||
<div class="flex flex-wrap mb-2 gap-y-1 pt-1">
|
||||
<button
|
||||
v-for="f in fields"
|
||||
:key="f.uuid"
|
||||
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
||||
>
|
||||
<IconMathFunction
|
||||
v-if="f.preferences?.formula"
|
||||
width="17"
|
||||
height="17"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<IconCodePlus
|
||||
v-else
|
||||
width="20"
|
||||
height="20"
|
||||
stroke-width="1.5"
|
||||
@@ -57,7 +64,7 @@
|
||||
<div class="mb-3 mt-1">
|
||||
<div
|
||||
target="blank"
|
||||
class="text-sm mb-2 inline space-x-2"
|
||||
class="text-sm mb-2 inline space-x-2 font-mono"
|
||||
>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@@ -83,12 +90,6 @@
|
||||
>
|
||||
/
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor(' % ')"
|
||||
>
|
||||
%
|
||||
</button>
|
||||
<button
|
||||
class="bg-base-200 px-2 rounded-xl"
|
||||
@click="insertTextUnderCursor('^')"
|
||||
@@ -122,12 +123,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCodePlus } from '@tabler/icons-vue'
|
||||
import { IconCodePlus, IconMathFunction } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FormulaModal',
|
||||
components: {
|
||||
IconCodePlus
|
||||
IconCodePlus,
|
||||
IconMathFunction
|
||||
},
|
||||
inject: ['t', 'save', 'template', 'withFormula'],
|
||||
props: {
|
||||
@@ -135,6 +137,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
defaultField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -154,7 +161,7 @@ export default {
|
||||
computed: {
|
||||
fields () {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
|
||||
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
@@ -224,9 +231,11 @@ export default {
|
||||
|
||||
this.formula = newText
|
||||
|
||||
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
||||
this.$nextTick(() => {
|
||||
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
||||
|
||||
textarea.focus()
|
||||
textarea.focus()
|
||||
})
|
||||
},
|
||||
resizeTextarea () {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
@@ -83,6 +83,7 @@ const en = {
|
||||
copy_to_all_pages: 'Copy to All Pages',
|
||||
add_option: 'Add option',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
condition: 'Condition',
|
||||
first_party: 'First Party',
|
||||
second_party: 'Second Party',
|
||||
@@ -255,6 +256,7 @@ const es = {
|
||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||
add_option: 'Agregar opción',
|
||||
option: 'Opción',
|
||||
options: 'Opciones',
|
||||
first_party: 'Primera Parte',
|
||||
second_party: 'Segunda Parte',
|
||||
third_party: 'Tercera Parte',
|
||||
@@ -435,6 +437,7 @@ const it = {
|
||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||
add_option: 'Aggiungi opzione',
|
||||
option: 'Opzione',
|
||||
options: 'Opzioni',
|
||||
condition: 'Condizione',
|
||||
first_party: 'Prima parte',
|
||||
second_party: 'Seconda parte',
|
||||
@@ -607,6 +610,7 @@ const pt = {
|
||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||
add_option: 'Adicionar opção',
|
||||
option: 'Opção',
|
||||
options: 'Opções',
|
||||
first_party: 'Primeira Parte',
|
||||
second_party: 'Segunda Parte',
|
||||
third_party: 'Terceira Parte',
|
||||
@@ -782,6 +786,7 @@ const fr = {
|
||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||
add_option: 'Ajouter une option',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
first_party: 'Première partie',
|
||||
second_party: 'Deuxième partie',
|
||||
third_party: 'Troisième partie',
|
||||
@@ -958,6 +963,7 @@ const de = {
|
||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||
add_option: 'Option hinzufügen',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
first_party: 'Erste Partei',
|
||||
second_party: 'Zweite Partei',
|
||||
third_party: 'Dritte Partei',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||
:class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
|
||||
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
|
||||
style="container-type: size"
|
||||
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"orientation": "natural",
|
||||
"orientation": "any",
|
||||
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"theme_color": "#FAF7F4",
|
||||
|
||||
@@ -275,6 +275,7 @@ module Submissions
|
||||
end
|
||||
|
||||
field['preferences'] = (field['preferences'] || {}).merge(attrs['preferences']) if attrs['preferences'].present?
|
||||
field['validation'] = (field['validation'] || {}).merge(attrs['validation']) if attrs['validation'].present?
|
||||
|
||||
return field if attrs['validation_pattern'].blank?
|
||||
|
||||
@@ -331,8 +332,8 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
def assign_completed_attributes(submitter)
|
||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||
def assign_completed_attributes(submitter, with_verification: true)
|
||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter, with_verification:)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
|
||||
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
||||
|
||||
@@ -40,7 +40,7 @@ module Submissions
|
||||
def rows_to_csv(rows)
|
||||
headers = build_headers(rows)
|
||||
|
||||
CSV.generate do |csv|
|
||||
CSVSafe.generate do |csv|
|
||||
csv << headers
|
||||
|
||||
rows.each do |row|
|
||||
|
||||
@@ -40,7 +40,7 @@ module Submissions
|
||||
end
|
||||
|
||||
json['status'] = 'completed'
|
||||
json['completed_at'] = last_submitter.completed_at
|
||||
json['completed_at'] = last_submitter.completed_at.as_json
|
||||
else
|
||||
json['documents'] = [] if with_documents
|
||||
json['status'] = build_status(submission, submitters)
|
||||
|
||||
@@ -67,25 +67,16 @@ module Submitters
|
||||
end
|
||||
|
||||
def build_text_image(submitter)
|
||||
time = I18n.l(submitter.completed_at.in_time_zone(submitter.submission.account.timezone),
|
||||
format: :long,
|
||||
locale: submitter.submission.account.locale)
|
||||
if submitter.completed_at
|
||||
time = I18n.l(submitter.completed_at.in_time_zone(submitter.submission.account.timezone),
|
||||
format: :long,
|
||||
locale: submitter.submission.account.locale)
|
||||
|
||||
timezone = TimeUtils.timezone_abbr(submitter.submission.account.timezone, submitter.completed_at)
|
||||
timezone = TimeUtils.timezone_abbr(submitter.submission.account.timezone, submitter.completed_at)
|
||||
end
|
||||
|
||||
name = if submitter.name.present? && submitter.email.present?
|
||||
"#{submitter.name} #{submitter.email}"
|
||||
else
|
||||
submitter.name || submitter.email || submitter.phone
|
||||
end
|
||||
|
||||
role = if submitter.submission.template_submitters.size > 1
|
||||
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
|
||||
|
||||
"#{I18n.t(:role, locale: submitter.account.locale)}: #{item['name']}\n"
|
||||
else
|
||||
''
|
||||
end
|
||||
name = build_name(submitter)
|
||||
role = build_role(submitter)
|
||||
|
||||
digitally_signed_by = I18n.t(:digitally_signed_by, locale: submitter.submission.account.locale)
|
||||
|
||||
@@ -97,6 +88,24 @@ module Submitters
|
||||
Vips::Image.text(text, width: WIDTH, height: HEIGHT, wrap: :'word-char')
|
||||
end
|
||||
|
||||
def build_name(submitter)
|
||||
if submitter.name.present? && submitter.email.present?
|
||||
"#{submitter.name} #{submitter.email}"
|
||||
else
|
||||
submitter.name || submitter.email || submitter.phone
|
||||
end
|
||||
end
|
||||
|
||||
def build_role(submitter)
|
||||
if submitter.submission.template_submitters.size > 1
|
||||
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
|
||||
|
||||
"#{I18n.t(:role, locale: submitter.account.locale)}: #{item['name']}\n"
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def load_logo(_submitter)
|
||||
PdfIcons.stamp_logo_io
|
||||
end
|
||||
|
||||
@@ -12,15 +12,16 @@ module Submitters
|
||||
|
||||
def call(submitter)
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [submitter],
|
||||
associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
||||
records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
||||
).call
|
||||
|
||||
values = build_values_array(submitter)
|
||||
documents = build_documents_array(submitter)
|
||||
|
||||
submitter_name = (submitter.submission.template_submitters ||
|
||||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
||||
submission = submitter.submission
|
||||
|
||||
submitter_name = (submission.template_submitters ||
|
||||
submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
||||
|
||||
decline_reason =
|
||||
submitter.declined_at? ? submitter.submission_events.find_by(event_type: :decline_form).data['reason'] : nil
|
||||
@@ -32,16 +33,18 @@ module Submitters
|
||||
'values' => values,
|
||||
'documents' => documents,
|
||||
'audit_log_url' => submitter.submission.audit_log_url,
|
||||
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
|
||||
**Docuseal.default_url_options),
|
||||
'template' => submitter.submission.template.as_json(
|
||||
'submission_url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
|
||||
'template' => submission.template.as_json(
|
||||
only: %i[id name external_id created_at updated_at],
|
||||
methods: %i[folder_name]
|
||||
),
|
||||
'submission' => {
|
||||
**submitter.submission.slice(:id, :audit_log_url, :combined_document_url, :created_at),
|
||||
status: build_submission_status(submitter.submission),
|
||||
url: r.submissions_preview_url(submitter.submission.slug, **Docuseal.default_url_options)
|
||||
'id' => submission.id,
|
||||
'audit_log_url' => submission.audit_log_url,
|
||||
'combined_document_url' => submission.combined_document_url,
|
||||
'status' => build_submission_status(submission),
|
||||
'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
|
||||
'created_at' => submission.created_at.as_json
|
||||
})
|
||||
end
|
||||
|
||||
@@ -63,7 +66,7 @@ module Submitters
|
||||
|
||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
|
||||
|
||||
{ field: field_name, value: }
|
||||
{ 'field' => field_name, 'value' => value }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -85,7 +88,7 @@ module Submitters
|
||||
|
||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
|
||||
|
||||
{ name: field_name, uuid: field['uuid'], value:, readonly: field['readonly'] == true }
|
||||
{ 'name' => field_name, 'uuid' => field['uuid'], 'value' => value, 'readonly' => field['readonly'] == true }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -103,7 +106,7 @@ module Submitters
|
||||
|
||||
def build_documents_array(submitter)
|
||||
submitter.documents.map do |attachment|
|
||||
{ name: attachment.filename.base, url: rails_storage_proxy_url(attachment) }
|
||||
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ module Submitters
|
||||
end
|
||||
end
|
||||
|
||||
def merge_default_values(submitter)
|
||||
def merge_default_values(submitter, with_verification: true)
|
||||
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
||||
next if field['submitter_uuid'] != submitter.uuid
|
||||
|
||||
@@ -157,7 +157,7 @@ module Submitters
|
||||
next
|
||||
end
|
||||
|
||||
if field['type'] == 'verification'
|
||||
if field['type'] == 'verification' && with_verification
|
||||
acc[field['uuid']] =
|
||||
if submitter.submission_events.exists?(event_type: :complete_verification)
|
||||
I18n.t(:verified, locale: :en)
|
||||
@@ -189,6 +189,8 @@ module Submitters
|
||||
|
||||
next if formula.blank?
|
||||
|
||||
formula = normalize_formula(formula, submitter.submission)
|
||||
|
||||
submission_values ||=
|
||||
if submitter.submission.template_submitters.size > 1
|
||||
merge_submitters_values(submitter)
|
||||
@@ -202,6 +204,20 @@ module Submitters
|
||||
computed_values.compact_blank
|
||||
end
|
||||
|
||||
def normalize_formula(formula, submission, depth = 0)
|
||||
raise ValidationError, 'Formula infinite loop' if depth > 10
|
||||
|
||||
formula.gsub(/{{(.*?)}}/) do |match|
|
||||
uuid = Regexp.last_match(1)
|
||||
|
||||
if (nested_formula = submission.fields_uuid_index.dig(uuid, 'preferences', 'formula').presence)
|
||||
"(#{normalize_formula(nested_formula, submission, depth + 1)})"
|
||||
else
|
||||
match
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_formula_value(_formula, _values)
|
||||
0
|
||||
end
|
||||
@@ -321,7 +337,7 @@ module Submitters
|
||||
end
|
||||
|
||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
||||
return value if value.in?([true, false]) || value.is_a?(Numeric)
|
||||
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||
return if value.blank?
|
||||
|
||||
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
||||
|
||||
@@ -25,7 +25,7 @@ module Templates
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
json[:documents] = template.schema.filter_map do |item|
|
||||
json['documents'] = template.schema.filter_map do |item|
|
||||
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||
|
||||
unless attachment
|
||||
@@ -38,11 +38,11 @@ module Templates
|
||||
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
||||
|
||||
{
|
||||
id: attachment.id,
|
||||
uuid: attachment.uuid,
|
||||
url: ActiveStorage::Blob.proxy_url(attachment.blob),
|
||||
preview_image_url: first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob),
|
||||
filename: attachment.filename
|
||||
'id' => attachment.id,
|
||||
'uuid' => attachment.uuid,
|
||||
'url' => ActiveStorage::Blob.proxy_url(attachment.blob),
|
||||
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob),
|
||||
'filename' => attachment.filename
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
+2
-1
@@ -6,8 +6,9 @@
|
||||
"@babel/plugin-transform-runtime": "7.21.4",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/runtime": "7.21.5",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@eid-easy/eideasy-widget": "^2.163.4",
|
||||
"@eid-easy/eideasy-widget": "^2.171.0",
|
||||
"@github/catalyst": "^2.0.0-beta",
|
||||
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
|
||||
"@hotwired/turbo-rails": "^7.3.0",
|
||||
|
||||
@@ -1030,6 +1030,11 @@
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@braintree/sanitize-url@^7.1.1":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e"
|
||||
integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==
|
||||
|
||||
"@codemirror/autocomplete@^6.0.0":
|
||||
version "6.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb"
|
||||
@@ -1141,22 +1146,22 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@eid-easy/eideasy-browser-client@2.127.0":
|
||||
version "2.127.0"
|
||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.127.0.tgz#d1b09e3634e94d7e32f593209a63f962fd326731"
|
||||
integrity sha512-2iosVqkF1C0hQWc6TVBe1SK7b/z8ZnvHnulGXfQf6VrrpEJMgnzK95h6LFDqDQyetfIwEGGoeOiUij2hYA1ZPA==
|
||||
"@eid-easy/eideasy-browser-client@2.135.0":
|
||||
version "2.135.0"
|
||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.135.0.tgz#ade3cd72210aba2bdef99ec8ffdabbefdd5e6480"
|
||||
integrity sha512-QaFMxdZaEzN/MdQ/ZhJBDN2v+6XJL0l9vK3zDgJFDyaUYvMvWcjFpHvtMiQEOivTD3sB4cM7/sJdwzA01APvsw==
|
||||
dependencies:
|
||||
axios "1.8.2"
|
||||
jsencrypt "3.2.1"
|
||||
lodash "^4.17.21"
|
||||
serialize-error "^9.1.1"
|
||||
|
||||
"@eid-easy/eideasy-widget@^2.163.4":
|
||||
version "2.163.4"
|
||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.163.4.tgz#4a8ada2c61f032527a08f9f8d9d1adf3fa7b5334"
|
||||
integrity sha512-7OQ1bm+KSG99wUI6szXT25Icq67CEQRK38VGj8fynW0ctqJTiFXRop7dqgR9JXLlJ1s1Z++El7igYxph7Dq5Aw==
|
||||
"@eid-easy/eideasy-widget@^2.171.0":
|
||||
version "2.171.0"
|
||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.171.0.tgz#2072e50a187ef36dd89d0bc5102d5996657ea180"
|
||||
integrity sha512-zDQgq2JhGR+omomU+4PU9vyWXJbMutklWCzZ74YVXzP1LmV0SD/X9Vy/bqofSZ1iRwgT+A+lCqLSmkoztaPC3A==
|
||||
dependencies:
|
||||
"@eid-easy/eideasy-browser-client" "2.127.0"
|
||||
"@eid-easy/eideasy-browser-client" "2.135.0"
|
||||
core-js "^3.8.3"
|
||||
i18n-iso-countries "^6.7.0"
|
||||
lodash.defaultsdeep "^4.6.1"
|
||||
|
||||
Reference in New Issue
Block a user