mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab68cc36b | |||
| ee2400a5c5 | |||
| 80be5d2cf5 | |||
| 76cf44a31b | |||
| eb63113227 | |||
| b7045fb3d9 | |||
| 11a5b5842d | |||
| 362994afd5 | |||
| d7682f4ebd | |||
| 56d4fe7fa9 | |||
| 93a852ac14 | |||
| 964db0ab08 | |||
| 8547295da7 | |||
| c75050fa46 | |||
| 65372c52aa | |||
| 12c767b7d3 | |||
| 2fc5e9cfb3 | |||
| bccc78ee65 | |||
| c0793f3ce7 | |||
| a8dd8699a3 | |||
| 91bb25e8b6 | |||
| c3f4480cdc | |||
| 33e226861e | |||
| 4f5acbec9d | |||
| d2c9bbf6ab | |||
| b5e57e1f2f | |||
| c82c8b2272 | |||
| 43f1b598a7 | |||
| 5081e387d0 | |||
| 1e5f55c1d6 |
+2
-2
@@ -51,7 +51,7 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
@@ -69,7 +69,7 @@ activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
RUN apk add --no-cache build-base git && bundle install && apk del --no-cache build-base git && 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||
COPY --chown=docuseal:docuseal ./app ./app
|
||||
|
||||
@@ -23,7 +23,6 @@ gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt', require: false
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'numo-narray-alt', require: false
|
||||
gem 'oj'
|
||||
gem 'onnxruntime', require: false
|
||||
@@ -45,6 +44,7 @@ gem 'shakapacker'
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
|
||||
+9
-3
@@ -1,3 +1,11 @@
|
||||
GIT
|
||||
remote: https://github.com/trilogy-libraries/trilogy.git
|
||||
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
||||
glob: contrib/ruby/*.gemspec
|
||||
specs:
|
||||
trilogy (2.10.0)
|
||||
bigdecimal
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
@@ -318,8 +326,6 @@ GEM
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.19.1)
|
||||
mysql2 (0.5.7)
|
||||
bigdecimal
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.2)
|
||||
@@ -644,7 +650,6 @@ DEPENDENCIES
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
numo-narray-alt
|
||||
oj
|
||||
onnxruntime
|
||||
@@ -673,6 +678,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
sqlite3
|
||||
strip_attributes
|
||||
trilogy!
|
||||
turbo-rails
|
||||
twitter_cldr
|
||||
tzinfo-data
|
||||
|
||||
@@ -188,7 +188,7 @@ module Api
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :invite_by,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :required, :validation_pattern, :invalid_message,
|
||||
|
||||
@@ -89,7 +89,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty()) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
|
||||
@@ -64,6 +64,22 @@ export default class extends HTMLElement {
|
||||
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||
|
||||
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
|
||||
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
|
||||
|
||||
const actualNumber = parseFloat(actual)
|
||||
const expectedNumber = parseFloat(expected)
|
||||
|
||||
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
|
||||
|
||||
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
|
||||
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
|
||||
if (action === 'greater_than') return actualNumber > expectedNumber
|
||||
if (action === 'less_than') return actualNumber < expectedNumber
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (action === 'equal') {
|
||||
const list = Array.isArray(actual) ? actual : [actual]
|
||||
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||
|
||||
@@ -80,7 +80,7 @@ export default targetable(class extends HTMLElement {
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty()) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
|
||||
@@ -182,8 +182,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.type === 'cells'"
|
||||
class="w-full flex items-center"
|
||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
||||
class="w-full flex"
|
||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||
>
|
||||
<div
|
||||
v-for="(char, index) in modelValue"
|
||||
|
||||
@@ -395,7 +395,9 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:reason="values[currentField.preferences?.reason_field_uuid]"
|
||||
:field="currentField"
|
||||
:values="values"
|
||||
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
||||
:touch-attachment-uuid="previousSignatureValue"
|
||||
:with-typed-signature="withTypedSignature"
|
||||
:remember-signature="rememberSignature"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@@ -407,6 +409,7 @@
|
||||
:submitter="submitter"
|
||||
:show-field-names="showFieldNames"
|
||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
|
||||
@attached="attachments.push($event)"
|
||||
@start="scrollIntoField(currentField)"
|
||||
@minimize="minimizeForm"
|
||||
@@ -926,10 +929,12 @@ export default {
|
||||
}, {})
|
||||
},
|
||||
attachmentConditionsIndex () {
|
||||
const cache = {}
|
||||
|
||||
return this.schema.reduce((acc, item) => {
|
||||
if (item.conditions?.length) {
|
||||
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
||||
acc[item.attachment_uuid] = this.checkFieldConditions(item)
|
||||
acc[item.attachment_uuid] = this.checkFieldConditions(item, cache)
|
||||
} else {
|
||||
acc[item.attachment_uuid] = true
|
||||
}
|
||||
@@ -996,7 +1001,9 @@ export default {
|
||||
},
|
||||
previousInitialsValue () {
|
||||
if (this.reuseSignature !== false) {
|
||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
const initialsField = this.fields.findLast
|
||||
? this.fields.findLast((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
: [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
|
||||
return this.values[initialsField?.uuid]
|
||||
} else {
|
||||
@@ -1023,7 +1030,9 @@ export default {
|
||||
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||
},
|
||||
readonlyFields () {
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
const cache = {}
|
||||
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
stepFields () {
|
||||
const verificationFields = []
|
||||
@@ -1078,10 +1087,12 @@ export default {
|
||||
sortedFields.push(verificationFields.pop())
|
||||
}
|
||||
|
||||
const cache = {}
|
||||
|
||||
return sortedFields.reduce((acc, f) => {
|
||||
const prevStep = acc[acc.length - 1]
|
||||
|
||||
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
|
||||
if (this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f)) {
|
||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||
prevStep.push(f)
|
||||
} else {
|
||||
@@ -1093,7 +1104,9 @@ export default {
|
||||
}, [])
|
||||
},
|
||||
formulaFields () {
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
const cache = {}
|
||||
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
attachmentsIndex () {
|
||||
return this.attachments.reduce((acc, a) => {
|
||||
@@ -1155,7 +1168,9 @@ export default {
|
||||
this.currentStep = Math.max(stepIndex, 0)
|
||||
} else if (this.goToLast) {
|
||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||
const lastFilledStepIndex = this.stepFields.indexOf(this.stepFields.findLast
|
||||
? this.stepFields.findLast((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))
|
||||
: [...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||
|
||||
const indexesList = [this.stepFields.length - 1]
|
||||
|
||||
@@ -1223,27 +1238,33 @@ export default {
|
||||
return true
|
||||
}
|
||||
},
|
||||
checkFieldConditions (field) {
|
||||
checkFieldConditions (field, cache = {}) {
|
||||
if (cache[field.uuid] !== undefined) {
|
||||
return cache[field.uuid]
|
||||
}
|
||||
|
||||
cache[field.uuid] = true
|
||||
|
||||
if (field.conditions?.length) {
|
||||
const result = field.conditions.reduce((acc, cond) => {
|
||||
if (cond.operation === 'or') {
|
||||
acc.push(acc.pop() || this.checkFieldCondition(cond))
|
||||
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||
} else {
|
||||
acc.push(this.checkFieldCondition(cond))
|
||||
acc.push(this.checkFieldCondition(cond, cache))
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return !result.includes(false)
|
||||
} else {
|
||||
return true
|
||||
cache[field.uuid] = !result.includes(false)
|
||||
}
|
||||
|
||||
return cache[field.uuid]
|
||||
},
|
||||
checkFieldCondition (condition) {
|
||||
checkFieldCondition (condition, cache = {}) {
|
||||
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||
|
||||
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
|
||||
if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1253,6 +1274,22 @@ export default {
|
||||
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
|
||||
const value = this.values[condition.field_uuid] ?? defaultValue
|
||||
|
||||
if (isEmpty(value) || isEmpty(condition.value)) return false
|
||||
|
||||
const actual = parseFloat(value)
|
||||
const expected = parseFloat(condition.value)
|
||||
|
||||
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
|
||||
|
||||
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
|
||||
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
|
||||
if (condition.action === 'greater_than') return actual > expected
|
||||
if (condition.action === 'less_than') return actual < expected
|
||||
|
||||
return false
|
||||
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||
if (field.options) {
|
||||
const option = field.options.find((o) => o.uuid === condition.value)
|
||||
@@ -1353,9 +1390,9 @@ export default {
|
||||
},
|
||||
previousSignatureValueFor (field) {
|
||||
if (this.reuseSignature !== false) {
|
||||
const signatureField = [...this.fields].reverse().find((f) =>
|
||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||
)
|
||||
const signatureField = this.fields.findLast
|
||||
? this.fields.findLast((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||
: [...this.fields].reverse().find((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||
|
||||
return this.values[signatureField?.uuid]
|
||||
} else {
|
||||
|
||||
@@ -497,10 +497,10 @@ export default {
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to start KBA')
|
||||
|
||||
const data = await resp.json()
|
||||
|
||||
if (!resp.ok) throw new Error(data.error || 'Failed to start KBA')
|
||||
|
||||
if (data.result && data.result.action === 'FAIL') {
|
||||
if (data.result.detail === 'NO MATCH') {
|
||||
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
||||
@@ -555,7 +555,11 @@ export default {
|
||||
const data = await resp.json()
|
||||
|
||||
if (data.result?.action !== 'PASS') {
|
||||
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||
if (data.result?.issues?.length) {
|
||||
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
|
||||
} else {
|
||||
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||
}
|
||||
|
||||
throw new Error('Knowledge Based Authentication Failed')
|
||||
}
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
>
|
||||
<input
|
||||
v-if="isTouchAttachment"
|
||||
:value="touchAttachmentUuid"
|
||||
type="hidden"
|
||||
name="touch_attachment_uuid"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||
@@ -334,6 +340,10 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
requireSigningReason: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -388,6 +398,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
touchAttachmentUuid: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
reason: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -399,13 +414,14 @@ export default {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason', 'touch-attachment'],
|
||||
data () {
|
||||
return {
|
||||
isSignatureStarted: false,
|
||||
isShowQr: false,
|
||||
isOtherReason: false,
|
||||
isUsePreviousValue: true,
|
||||
isTouchAttachment: false,
|
||||
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
||||
uploadImageInputKey: Math.random().toString()
|
||||
}
|
||||
@@ -514,7 +530,7 @@ export default {
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
const canvas = this.$refs.canvas
|
||||
|
||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty()) {
|
||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = canvas.width / oldWidth
|
||||
const sy = canvas.height / oldHeight
|
||||
|
||||
@@ -731,6 +747,13 @@ export default {
|
||||
},
|
||||
async submit () {
|
||||
if (this.modelValue || this.computedPreviousValue) {
|
||||
if (this.touchAttachmentUuid && this.computedPreviousValue === this.touchAttachmentUuid && !Object.values(this.values).includes(this.touchAttachmentUuid)) {
|
||||
this.isTouchAttachment = true
|
||||
this.$emit('touch-attachment', this.touchAttachmentUuid)
|
||||
} else {
|
||||
this.isTouchAttachment = false
|
||||
}
|
||||
|
||||
if (this.computedPreviousValue) {
|
||||
this.$emit('update:model-value', this.computedPreviousValue)
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
docId: this.eidEasyData.doc_id,
|
||||
language: this.locale,
|
||||
countryCode: this.countryCode,
|
||||
sandbox: ['demo.docuseal.tech'].includes(location.host),
|
||||
enabledMethods: {
|
||||
signature: this.eidEasyData.available_methods
|
||||
},
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
@focusout="maybeBlurSettings"
|
||||
>
|
||||
<FieldSettings
|
||||
v-if="isMobile"
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:editable="editable"
|
||||
@@ -150,6 +151,12 @@
|
||||
@save="save"
|
||||
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="whitespace-normal"
|
||||
>
|
||||
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
|
||||
</div>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
@@ -252,7 +259,8 @@
|
||||
</span>
|
||||
<div
|
||||
v-else-if="field.type === 'cells' && field.default_value"
|
||||
class="w-full flex items-center"
|
||||
class="w-full flex"
|
||||
:class="fontClasses"
|
||||
>
|
||||
<div
|
||||
v-for="(char, index) in field.default_value"
|
||||
@@ -334,6 +342,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -346,6 +355,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -357,6 +367,7 @@
|
||||
:item="field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:default-field="defaultField"
|
||||
@save="save"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -369,6 +380,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -401,7 +413,7 @@ export default {
|
||||
FieldSubmitter,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef'],
|
||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
area: {
|
||||
type: Object,
|
||||
@@ -467,6 +479,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isSelectMode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -579,7 +596,7 @@ export default {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
defaultName () {
|
||||
return this.buildDefaultName(this.field, this.template.fields)
|
||||
return this.buildDefaultName(this.field)
|
||||
},
|
||||
fontClasses () {
|
||||
if (!this.field.preferences) {
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
:draw-field-type="drawFieldType"
|
||||
:draw-custom-field="drawCustomField"
|
||||
:editable="editable"
|
||||
:is-mobile="isMobile"
|
||||
:base-url="baseUrl"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
|
||||
@@ -382,9 +383,9 @@
|
||||
@paste-field="pasteField"
|
||||
@copy-field="copyField"
|
||||
@add-custom-field="addCustomField"
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@copy-selected-areas="copySelectedAreas"
|
||||
@delete-selected-areas="deleteSelectedAreas"
|
||||
@align-selected-areas="alignSelectedAreas"
|
||||
@autodetect-fields="detectFieldsForPage"
|
||||
/>
|
||||
<DocumentControls
|
||||
@@ -645,7 +646,8 @@ export default {
|
||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
||||
customDragFieldRef: computed(() => this.customDragFieldRef),
|
||||
isSelectModeRef: computed(() => this.isSelectModeRef),
|
||||
isCmdKeyRef: computed(() => this.isCmdKeyRef)
|
||||
isCmdKeyRef: computed(() => this.isCmdKeyRef),
|
||||
getFieldTypeIndex: this.getFieldTypeIndex
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@@ -989,6 +991,18 @@ export default {
|
||||
|
||||
return areas
|
||||
},
|
||||
fieldTypeIndexMap () {
|
||||
const map = {}
|
||||
const typeCounters = {}
|
||||
|
||||
this.template.fields.forEach((f) => {
|
||||
typeCounters[f.type] ||= 0
|
||||
map[f.uuid] = typeCounters[f.type]
|
||||
typeCounters[f.type]++
|
||||
})
|
||||
|
||||
return map
|
||||
},
|
||||
isAllRequiredFieldsAdded () {
|
||||
return !this.defaultRequiredFields?.some((f) => {
|
||||
return !this.template.fields?.some((field) => field.name === f.name)
|
||||
@@ -1085,6 +1099,9 @@ export default {
|
||||
addCustomField (field) {
|
||||
return this.$refs.fields.addCustomField(field)
|
||||
},
|
||||
getFieldTypeIndex (field) {
|
||||
return this.fieldTypeIndexMap[field.uuid]
|
||||
},
|
||||
addCustomFieldWithoutDraw () {
|
||||
const customField = this.drawCustomField
|
||||
|
||||
@@ -1160,27 +1177,6 @@ export default {
|
||||
|
||||
this.debouncedSave()
|
||||
},
|
||||
alignSelectedAreas (direction) {
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
let targetValue
|
||||
|
||||
if (direction === 'left') {
|
||||
targetValue = Math.min(...areas.map(a => a.x))
|
||||
areas.forEach((area) => { area.x = targetValue })
|
||||
} else if (direction === 'right') {
|
||||
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||
} else if (direction === 'top') {
|
||||
targetValue = Math.min(...areas.map(a => a.y))
|
||||
areas.forEach((area) => { area.y = targetValue })
|
||||
} else if (direction === 'bottom') {
|
||||
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
@@ -2065,7 +2061,9 @@ export default {
|
||||
}
|
||||
|
||||
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
||||
const previousField = this.template.fields.findLast
|
||||
? this.template.fields.findLast((f) => f.type === type)
|
||||
: [...this.template.fields].reverse().find((f) => f.type === type)
|
||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||
|
||||
if (previousArea || area.w) {
|
||||
@@ -2329,7 +2327,9 @@ export default {
|
||||
assignDropAreaSize (fieldArea, field, area) {
|
||||
const fieldType = field.type || 'text'
|
||||
|
||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||
const previousField = this.template.fields.findLast
|
||||
? this.template.fields.findLast((f) => f.type === fieldType)
|
||||
: [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||
|
||||
let baseArea
|
||||
|
||||
|
||||
@@ -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') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
|
||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -83,7 +83,7 @@
|
||||
class="text-base-content"
|
||||
:selected="condition.field_uuid === f.uuid"
|
||||
>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
{{ f.name || buildDefaultName(f) }}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
@@ -124,6 +124,16 @@
|
||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-else-if="conditionField(condition)?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)"
|
||||
v-model="condition.value"
|
||||
type="number"
|
||||
step="any"
|
||||
class="input input-bordered input-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||
:class="{ 'text-gray-300': !condition.value }"
|
||||
:placeholder="t('type_value')"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
@@ -154,7 +164,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ConditionModal',
|
||||
inject: ['t', 'save', 'template', 'withConditions'],
|
||||
inject: ['t', 'template', 'withConditions'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@@ -169,18 +179,13 @@ export default {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
withClickSaveEvent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
excludeFieldUuids: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['close', 'click-save'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
||||
@@ -227,6 +232,8 @@ export default {
|
||||
actions.push('equal', 'not_equal')
|
||||
} else if (['multiple'].includes(field.type)) {
|
||||
actions.push('contains', 'does_not_contain')
|
||||
} else if (field.type === 'number') {
|
||||
actions.push('not_empty', 'empty', 'equal', 'not_equal', 'greater_than', 'less_than')
|
||||
} else {
|
||||
actions.push('not_empty', 'empty')
|
||||
}
|
||||
@@ -244,12 +251,7 @@ export default {
|
||||
delete this.item.conditions
|
||||
}
|
||||
|
||||
if (this.withClickSaveEvent) {
|
||||
this.$emit('click-save')
|
||||
} else {
|
||||
this.save()
|
||||
}
|
||||
|
||||
this.$emit('save')
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +179,11 @@ export default {
|
||||
},
|
||||
onBlur (e) {
|
||||
setTimeout(() => {
|
||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||
this.$emit('update:model-value', this.value)
|
||||
if (this.$refs.contenteditable) {
|
||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||
this.$emit('update:model-value', this.value)
|
||||
}
|
||||
|
||||
this.$emit('blur', e)
|
||||
|
||||
this.isEditable = false
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!isShowFormulaModal && !isShowFontModal && !isShowConditionsModal && !isShowDescriptionModal"
|
||||
ref="menu"
|
||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-base-300 min-w-[170px] cursor-default"
|
||||
:style="menuStyle"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<label
|
||||
v-if="showRequired"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
:checked="isRequired"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
@change="handleToggleRequired($event.target.checked)"
|
||||
@click.stop
|
||||
>
|
||||
<span>{{ t('required') }}</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="showReadOnly"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
:checked="isReadOnly"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
@change="handleToggleReadOnly($event.target.checked)"
|
||||
@click.stop
|
||||
>
|
||||
<span>{{ t('read_only') }}</span>
|
||||
</label>
|
||||
<hr
|
||||
v-if="(showRequired || showReadOnly) && (showFont || showDescription || showCondition || showFormula)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showFont && !isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFontModal"
|
||||
>
|
||||
<IconTypography class="w-4 h-4" />
|
||||
<span>{{ t('font') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showDescription"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openDescriptionModal"
|
||||
>
|
||||
<IconInfoCircle class="w-4 h-4" />
|
||||
<span>{{ t('description') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition && !isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showFormula"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFormulaModal"
|
||||
>
|
||||
<IconMathFunction class="w-4 h-4" />
|
||||
<span>{{ t('formula') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="((showFont && !isMultiSelection) || showDescription || (showCondition && !isMultiSelection) || showFormula) && (showCopy || showDelete || showPaste)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'left')"
|
||||
>
|
||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||
<span>{{ t('align_left') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'right')"
|
||||
>
|
||||
<IconLayoutAlignRight class="w-4 h-4" />
|
||||
<span>{{ t('align_right') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'top')"
|
||||
>
|
||||
<IconLayoutAlignTop class="w-4 h-4" />
|
||||
<span>{{ t('align_top') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'bottom')"
|
||||
>
|
||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||
<span>{{ t('align_bottom') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="isMultiSelection && (showFont || showCondition)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showFont && isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFontModal"
|
||||
>
|
||||
<IconTypography class="w-4 h-4" />
|
||||
<span>{{ t('font') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition && isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="isMultiSelection"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showCopy"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||
@click.stop="$emit('copy')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconCopy class="w-4 h-4" />
|
||||
<span>{{ t('copy') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showDelete"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm text-red-600"
|
||||
@click.stop="$emit('delete')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconTrashX class="w-4 h-4" />
|
||||
<span>{{ t('remove') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showPaste"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-base-100'"
|
||||
:disabled="!hasClipboardData"
|
||||
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClipboard class="w-4 h-4" />
|
||||
<span>{{ t('paste') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showSelectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||
@click.stop="handleToggleSelectMode"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClick
|
||||
v-if="!isSelectModeRef.value"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<IconNewSection
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="showAutodetectFields"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showAutodetectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('autodetect-fields')"
|
||||
>
|
||||
<IconSparkles class="w-4 h-4" />
|
||||
<span>{{ t('autodetect_fields') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="isShowFormulaModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FormulaModal
|
||||
:field="field"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowFontModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FontModal
|
||||
:field="multiSelectField || field"
|
||||
:area="contextMenu.area"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
:with-click-save-event="isMultiSelection"
|
||||
@click-save="handleSaveMultiSelectFontModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowConditionsModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<ConditionsModal
|
||||
:item="multiSelectField || field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:exclude-field-uuids="isMultiSelection ? selectedFields.map(f => f.uuid) : []"
|
||||
:with-click-save-event="isMultiSelection"
|
||||
@click-save="handleSaveMultiSelectConditionsModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowDescriptionModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<DescriptionModal
|
||||
:field="field"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconClick, IconNewSection, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconSparkles } from '@tabler/icons-vue'
|
||||
import FormulaModal from './formula_modal'
|
||||
import FontModal from './font_modal'
|
||||
import ConditionsModal from './conditions_modal'
|
||||
import DescriptionModal from './description_modal'
|
||||
import Field from './field'
|
||||
import FieldType from './field_type.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContextMenu',
|
||||
components: {
|
||||
IconCopy,
|
||||
IconClipboard,
|
||||
IconTrashX,
|
||||
IconTypography,
|
||||
IconInfoCircle,
|
||||
IconRouteAltLeft,
|
||||
IconMathFunction,
|
||||
IconClick,
|
||||
IconNewSection,
|
||||
IconLayoutAlignLeft,
|
||||
IconLayoutAlignRight,
|
||||
IconLayoutAlignTop,
|
||||
IconLayoutAlignBottom,
|
||||
IconSparkles,
|
||||
FormulaModal,
|
||||
FontModal,
|
||||
ConditionsModal,
|
||||
DescriptionModal
|
||||
},
|
||||
inject: ['t', 'save', 'selectedAreasRef', 'isSelectModeRef'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isMultiSelection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedAreas: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['copy', 'paste', 'delete', 'close', 'align', 'autodetect-fields'],
|
||||
data () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
isShowDescriptionModal: false,
|
||||
multiSelectField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
modalContainerEl () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
selectedFields () {
|
||||
if (!this.isMultiSelection) return []
|
||||
|
||||
return this.selectedAreasRef.value.map((area) => {
|
||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||
}).filter(Boolean)
|
||||
},
|
||||
isMac () {
|
||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||
},
|
||||
hasClipboardData () {
|
||||
try {
|
||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||
|
||||
if (clipboard) {
|
||||
const data = JSON.parse(clipboard)
|
||||
|
||||
return Date.now() - data.timestamp < 3600000
|
||||
}
|
||||
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
menuStyle () {
|
||||
return {
|
||||
left: this.contextMenu.x + 'px',
|
||||
top: this.contextMenu.y + 'px'
|
||||
}
|
||||
},
|
||||
showCopy () {
|
||||
return !!this.contextMenu.area || this.isMultiSelection
|
||||
},
|
||||
showPaste () {
|
||||
return !this.contextMenu.area && !this.isMultiSelection
|
||||
},
|
||||
showDelete () {
|
||||
return !!this.contextMenu.area || this.isMultiSelection
|
||||
},
|
||||
showFont () {
|
||||
if (this.isMultiSelection) return true
|
||||
if (!this.field) return false
|
||||
|
||||
return ['text', 'number', 'date', 'select', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showDescription () {
|
||||
if (!this.field) return false
|
||||
|
||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
||||
},
|
||||
showCondition () {
|
||||
if (this.isMultiSelection) return true
|
||||
if (!this.field) return false
|
||||
|
||||
return !['stamp', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showFormula () {
|
||||
if (!this.field) return false
|
||||
|
||||
return this.field.type === 'number'
|
||||
},
|
||||
showRequired () {
|
||||
if (!this.field) return false
|
||||
|
||||
return !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showReadOnly () {
|
||||
if (!this.field) return false
|
||||
|
||||
return ['text', 'number'].includes(this.field.type)
|
||||
},
|
||||
isRequired () {
|
||||
return this.field?.required || false
|
||||
},
|
||||
isReadOnly () {
|
||||
return this.field?.readonly || false
|
||||
},
|
||||
showSelectFields () {
|
||||
return !this.contextMenu.area && !this.isMultiSelection
|
||||
},
|
||||
showAutodetectFields () {
|
||||
return this.withFieldsDetection && this.editable && !this.contextMenu.area && !this.isMultiSelection
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('keydown', this.onKeyDown)
|
||||
document.addEventListener('mousedown', this.handleClickOutside)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.checkMenuPosition()
|
||||
})
|
||||
},
|
||||
beforeUnmount () {
|
||||
document.removeEventListener('keydown', this.onKeyDown)
|
||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||
},
|
||||
methods: {
|
||||
buildDefaultName: Field.methods.buildDefaultName,
|
||||
checkMenuPosition () {
|
||||
if (this.$refs.menu) {
|
||||
const rect = this.$refs.menu.getBoundingClientRect()
|
||||
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
this.contextMenu.y = this.contextMenu.y - rect.height
|
||||
}
|
||||
|
||||
if (rect.right > window.innerWidth) {
|
||||
this.contextMenu.x = this.contextMenu.x - rect.width
|
||||
}
|
||||
}
|
||||
},
|
||||
handleToggleRequired (value) {
|
||||
if (this.field) {
|
||||
this.field.required = value
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
handleToggleReadOnly (value) {
|
||||
if (this.field) {
|
||||
this.field.readonly = value
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
onKeyDown (event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('close')
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('paste')
|
||||
}
|
||||
},
|
||||
handleClickOutside (event) {
|
||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
openFontModal () {
|
||||
if (this.isMultiSelection) {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
preferences: {}
|
||||
}
|
||||
|
||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||
|
||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||
}
|
||||
}
|
||||
|
||||
this.isShowFontModal = true
|
||||
},
|
||||
openDescriptionModal () {
|
||||
this.isShowDescriptionModal = true
|
||||
},
|
||||
openConditionModal () {
|
||||
if (this.isMultiSelection) {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
conditions: []
|
||||
}
|
||||
|
||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||
|
||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||
}
|
||||
}
|
||||
|
||||
this.isShowConditionsModal = true
|
||||
},
|
||||
openFormulaModal () {
|
||||
this.isShowFormulaModal = true
|
||||
},
|
||||
closeModal () {
|
||||
this.isShowFormulaModal = false
|
||||
this.isShowFontModal = false
|
||||
this.isShowConditionsModal = false
|
||||
this.isShowDescriptionModal = false
|
||||
this.multiSelectField = null
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
handleSaveMultiSelectFontModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
},
|
||||
handleSaveMultiSelectConditionsModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
},
|
||||
handleToggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -125,6 +125,7 @@
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -136,6 +137,7 @@
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -147,6 +149,7 @@
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
@@ -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">
|
||||
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -67,7 +67,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'DescriptionModal',
|
||||
inject: ['t', 'save', 'template'],
|
||||
inject: ['t', 'template'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
description: this.field.description,
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
this.field.description = this.description
|
||||
this.field.title = this.title
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
this.$emit('close')
|
||||
},
|
||||
resizeTextarea () {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:is-drag="isDrag"
|
||||
:is-mobile="isMobile"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:default-fields="defaultFields"
|
||||
:drag-field-placeholder="dragFieldPlaceholder"
|
||||
@@ -30,9 +31,9 @@
|
||||
@copy-field="$emit('copy-field', $event)"
|
||||
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
@copy-selected-areas="$emit('copy-selected-areas')"
|
||||
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||
@align-selected-areas="$emit('align-selected-areas', $event)"
|
||||
@autodetect-fields="$emit('autodetect-fields', $event)"
|
||||
@scroll-to="scrollToArea"
|
||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||
@@ -98,6 +99,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
allowDraw: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -138,7 +144,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields', 'add-custom-field'],
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||
data () {
|
||||
return {
|
||||
pageRefs: []
|
||||
@@ -180,7 +186,15 @@ export default {
|
||||
methods: {
|
||||
scrollToArea (area) {
|
||||
this.$nextTick(() => {
|
||||
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
const pageRef = this.pageRefs[area.page]
|
||||
|
||||
if (pageRef && pageRef.areaRefs) {
|
||||
const areaRef = pageRef.areaRefs.find((e) => e.area === area)
|
||||
|
||||
if (areaRef && areaRef.$el) {
|
||||
areaRef.$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
setPageRefs (el) {
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -281,6 +282,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -292,6 +294,7 @@
|
||||
:item="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -304,6 +307,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -340,7 +344,7 @@ export default {
|
||||
IconMathFunction,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
|
||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -412,7 +416,7 @@ export default {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
defaultName () {
|
||||
return this.buildDefaultName(this.field, this.template.fields)
|
||||
return this.buildDefaultName(this.field)
|
||||
},
|
||||
areas () {
|
||||
return this.field.areas || []
|
||||
@@ -432,7 +436,7 @@ export default {
|
||||
|
||||
this.$emit('save')
|
||||
},
|
||||
buildDefaultName (field, fields) {
|
||||
buildDefaultName (field) {
|
||||
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
|
||||
const { price, currency } = field.preferences || {}
|
||||
|
||||
@@ -443,7 +447,7 @@ export default {
|
||||
|
||||
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
||||
} else {
|
||||
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
||||
const typeIndex = this.getFieldTypeIndex(field)
|
||||
|
||||
if (field.type === 'heading' || field.type === 'strikethrough') {
|
||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<Teleport :to="modalContainerEl">
|
||||
<div class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
@click.prevent="$emit('close')"
|
||||
/>
|
||||
<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">
|
||||
{{ title }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl modal-close-button"
|
||||
@click.prevent="$emit('close')"
|
||||
>×</a>
|
||||
</div>
|
||||
<form @submit.prevent="$emit('save')">
|
||||
<slot />
|
||||
<button
|
||||
class="base-button w-full mt-4 modal-save-button"
|
||||
type="submit"
|
||||
>
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContextModal',
|
||||
inject: ['t'],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modalContainerEl: {
|
||||
type: Element,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close', 'save']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
@click.stop="isOpen ? close() : open()"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<component
|
||||
:is="icon || 'span'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ label }}</span>
|
||||
</span>
|
||||
<IconChevronRight class="w-4 h-4" />
|
||||
</button>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
ref="submenu"
|
||||
class="absolute p-1 z-50 left-full bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||
style="min-width: 170px"
|
||||
:style="submenuStyle"
|
||||
:class="menuClass"
|
||||
@click.stop
|
||||
>
|
||||
<slot>
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between space-x-2 text-sm cursor-pointer"
|
||||
@click="handleSelect(option.value)"
|
||||
>
|
||||
<span class="whitespace-nowrap">{{ option.label }}</span>
|
||||
<IconCheck
|
||||
v-if="modelValue === option.value"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconChevronRight, IconCheck } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'ContextSubmenu',
|
||||
components: {
|
||||
IconChevronRight,
|
||||
IconCheck
|
||||
},
|
||||
props: {
|
||||
icon: {
|
||||
type: [Function],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
menuClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['select', 'update:modelValue'],
|
||||
data () {
|
||||
return {
|
||||
isOpen: false,
|
||||
topOffset: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submenuStyle () {
|
||||
return {
|
||||
top: this.topOffset + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.clearTimeout()
|
||||
},
|
||||
methods: {
|
||||
handleMouseEnter () {
|
||||
clearTimeout(this.closeTimeout)
|
||||
|
||||
this.openTimeout = setTimeout(() => this.open(), 200)
|
||||
},
|
||||
handleMouseLeave () {
|
||||
clearTimeout(this.openTimeout)
|
||||
|
||||
this.closeTimeout = setTimeout(() => this.close(), 200)
|
||||
},
|
||||
open () {
|
||||
this.clearTimeout()
|
||||
|
||||
this.isOpen = true
|
||||
this.topOffset = 0
|
||||
|
||||
this.$nextTick(() => setTimeout(() => this.adjustPosition(), 0))
|
||||
},
|
||||
clearTimeout () {
|
||||
if (this.openTimeout) {
|
||||
clearTimeout(this.openTimeout)
|
||||
}
|
||||
|
||||
if (this.closeTimeout) {
|
||||
clearTimeout(this.closeTimeout)
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.clearTimeout()
|
||||
|
||||
this.isOpen = false
|
||||
},
|
||||
handleSelect (value) {
|
||||
this.$emit('select', value)
|
||||
this.$emit('update:modelValue', value)
|
||||
},
|
||||
adjustPosition () {
|
||||
if (!this.$refs.submenu) return
|
||||
|
||||
const rect = this.$refs.submenu.getBoundingClientRect()
|
||||
const overflow = rect.bottom - window.innerHeight
|
||||
|
||||
if (overflow > 0) {
|
||||
this.topOffset = -overflow - 4
|
||||
} else {
|
||||
this.topOffset = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, $emit('save')]"
|
||||
>
|
||||
<option
|
||||
v-for="method in ['QeS', 'AeS']"
|
||||
v-for="method in verificationMethods"
|
||||
:key="method"
|
||||
:value="method.toLowerCase()"
|
||||
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
|
||||
@@ -26,32 +26,6 @@
|
||||
{{ t('method') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['cells'].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 bg-transparent"
|
||||
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, $emit('save')]"
|
||||
>
|
||||
<option
|
||||
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
|
||||
:key="value"
|
||||
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
||||
:value="value"
|
||||
>
|
||||
{{ t(value) }}
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('align') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
||||
class="py-1.5 px-1 relative"
|
||||
@@ -348,7 +322,7 @@
|
||||
{{ t('any') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
|
||||
v-for="type in signatureFormats"
|
||||
:key="type"
|
||||
:value="type"
|
||||
:selected="field.preferences?.format === type"
|
||||
@@ -437,7 +411,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="['text', 'number'].includes(field.type)"
|
||||
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
@@ -452,7 +426,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
|
||||
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
@@ -470,7 +444,7 @@
|
||||
v-if="field.type != 'stamp'"
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)">
|
||||
<li v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)">
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-font')"
|
||||
@@ -718,7 +692,7 @@ export default {
|
||||
},
|
||||
lengthValidation () {
|
||||
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
||||
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||
return this.parseLengthPattern(this.field.validation.pattern)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -735,6 +709,15 @@ export default {
|
||||
'^[a-zA-Z]+$': 'letters_only'
|
||||
}
|
||||
},
|
||||
signatureFormats () {
|
||||
return ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']
|
||||
},
|
||||
verificationMethods () {
|
||||
return ['QeS', 'AeS']
|
||||
},
|
||||
prefillableFieldTypes () {
|
||||
return ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone']
|
||||
},
|
||||
sortedAreas () {
|
||||
return (this.field.areas || []).sort((a, b) => {
|
||||
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||
@@ -798,6 +781,9 @@ export default {
|
||||
return number
|
||||
}
|
||||
},
|
||||
parseLengthPattern (pattern) {
|
||||
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
||||
},
|
||||
formatDate (date, format) {
|
||||
const monthFormats = {
|
||||
M: 'numeric',
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
:key="field.uuid"
|
||||
:data-uuid="field.uuid"
|
||||
:field="field"
|
||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||
:type-index="getFieldTypeIndex(field)"
|
||||
:editable="editable"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
@@ -127,7 +127,7 @@
|
||||
>{{ t('custom') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="showCustomTab && editable"
|
||||
v-if="showCustomTab && editable && (customFields.length || newCustomField)"
|
||||
ref="customFields"
|
||||
class="custom-fields"
|
||||
@dragover.prevent="onCustomFieldDragover"
|
||||
@@ -376,7 +376,7 @@ export default {
|
||||
IconDrag,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef'],
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
|
||||
@@ -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') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -171,7 +171,7 @@
|
||||
contenteditable="true"
|
||||
class="outline-none whitespace-nowrap truncate"
|
||||
>
|
||||
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
|
||||
{{ field.default_value || field.name || buildDefaultName(field) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,7 +196,7 @@ export default {
|
||||
components: {
|
||||
IconChevronDown
|
||||
},
|
||||
inject: ['t', 'save', 'template'],
|
||||
inject: ['t', 'template'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -212,17 +212,12 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withClickSaveEvent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close', 'click-save'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
preferences: {}
|
||||
@@ -262,6 +257,7 @@ export default {
|
||||
colors () {
|
||||
return [
|
||||
{ label: '⬛', value: 'black' },
|
||||
{ label: '⬜', value: 'white' },
|
||||
{ label: '🟦', value: 'blue' },
|
||||
{ label: '🟥', value: 'red' }
|
||||
]
|
||||
@@ -276,6 +272,7 @@ export default {
|
||||
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
||||
'items-start': this.preferences.valign === 'top',
|
||||
'items-end': this.preferences.valign === 'bottom',
|
||||
'bg-black': this.preferences.color === 'white',
|
||||
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
||||
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
||||
}
|
||||
@@ -327,12 +324,7 @@ export default {
|
||||
|
||||
Object.assign(this.field.preferences, this.preferences)
|
||||
|
||||
if (this.withClickSaveEvent) {
|
||||
this.$emit('click-save')
|
||||
} else {
|
||||
this.save()
|
||||
}
|
||||
|
||||
this.$emit('save')
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -33,7 +33,7 @@
|
||||
v-for="f in fields"
|
||||
:key="f.uuid"
|
||||
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)}}}`)"
|
||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f)}}}`)"
|
||||
>
|
||||
<IconMathFunction
|
||||
v-if="f.preferences?.formula"
|
||||
@@ -47,7 +47,7 @@
|
||||
height="20"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
{{ f.name || buildDefaultName(f) }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
@@ -131,7 +131,7 @@ export default {
|
||||
IconCodePlus,
|
||||
IconMathFunction
|
||||
},
|
||||
inject: ['t', 'save', 'template', 'withFormula'],
|
||||
inject: ['t', 'template', 'withFormula'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
formula: ''
|
||||
@@ -181,7 +181,7 @@ export default {
|
||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||
|
||||
if (foundField) {
|
||||
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
|
||||
return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
|
||||
} else {
|
||||
return '{{FIELD NOT FOUND}}'
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export default {
|
||||
normalizeFormula (text) {
|
||||
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
||||
const foundField = this.fields.find((f) => {
|
||||
return (f.name || this.buildDefaultName(f, this.template.fields)).trim() === name.trim()
|
||||
return (f.name || this.buildDefaultName(f)).trim() === name.trim()
|
||||
})
|
||||
|
||||
if (foundField) {
|
||||
@@ -212,11 +212,14 @@ export default {
|
||||
} else {
|
||||
this.field.preferences.formula = normalizedFormula
|
||||
|
||||
if (this.field.type !== 'payment') {
|
||||
if (this.field.type === 'payment') {
|
||||
delete this.field.preferences.price
|
||||
delete this.field.preferences.payment_link_id
|
||||
} else {
|
||||
this.field.readonly = !!normalizedFormula
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const en = {
|
||||
fixed: 'Fixed',
|
||||
default: 'Default',
|
||||
save_as_custom_field: 'Save as Custom Field',
|
||||
save_as_custom_field: 'Save as custom field',
|
||||
kba: 'KBA',
|
||||
analyzing_: 'Analyzing...',
|
||||
download: 'Download',
|
||||
@@ -31,6 +32,9 @@ const en = {
|
||||
field_not_found: 'Field not found',
|
||||
clear: 'Clear',
|
||||
align: 'Align',
|
||||
resize: 'Resize',
|
||||
width: 'Width',
|
||||
height: 'Height',
|
||||
add_all_required_fields_to_continue: 'Add all required fields to continue',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?',
|
||||
keep: 'Keep',
|
||||
@@ -48,6 +52,8 @@ const en = {
|
||||
type_value: 'Type value',
|
||||
equal: 'Equal',
|
||||
not_equal: 'Not equal',
|
||||
greater_than: 'Greater than',
|
||||
less_than: 'Less than',
|
||||
contains: 'Contains',
|
||||
does_not_contain: 'Does not contain',
|
||||
not_empty: 'Not empty',
|
||||
@@ -91,8 +97,9 @@ const en = {
|
||||
format: 'Format',
|
||||
read_only: 'Read-only',
|
||||
page: 'Page',
|
||||
draw_new_area: 'Draw New Area',
|
||||
copy_to_all_pages: 'Copy to All Pages',
|
||||
draw_new_area: 'Draw new area',
|
||||
copy_to_all_pages: 'Copy to all pages',
|
||||
more: 'More',
|
||||
add_option: 'Add option',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
@@ -173,6 +180,9 @@ const en = {
|
||||
numbers_only: 'Numbers only',
|
||||
letters_only: 'Letters only',
|
||||
regexp_validation: 'Regexp validation',
|
||||
custom_validation: 'Custom Validation',
|
||||
length_validation: 'Length Validation',
|
||||
number_range: 'Number Range',
|
||||
enter_pdf_password: 'Enter PDF password',
|
||||
wrong_password: 'Wrong password',
|
||||
currency: 'Currency',
|
||||
@@ -202,6 +212,7 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
fixed: 'Fijo',
|
||||
default: 'Predeterminado',
|
||||
save_as_custom_field: 'Guardar como personalizado',
|
||||
kba: 'KBA',
|
||||
@@ -235,6 +246,9 @@ const es = {
|
||||
clear: 'Borrar',
|
||||
type_value: 'Escriba valor',
|
||||
align: 'Alinear',
|
||||
resize: 'Redimensionar',
|
||||
width: 'Ancho',
|
||||
height: 'Alto',
|
||||
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
|
||||
keep: 'Mantener',
|
||||
@@ -252,6 +266,8 @@ const es = {
|
||||
price: 'Precio',
|
||||
equal: 'Igual',
|
||||
not_equal: 'No es igual',
|
||||
greater_than: 'Mayor que',
|
||||
less_than: 'Menor que',
|
||||
contains: 'Contiene',
|
||||
does_not_contain: 'No contiene',
|
||||
not_empty: 'No vacío',
|
||||
@@ -292,6 +308,7 @@ const es = {
|
||||
page: 'Página',
|
||||
draw_new_area: 'Dibujar nueva área',
|
||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||
more: 'Más',
|
||||
add_option: 'Agregar opción',
|
||||
option: 'Opción',
|
||||
options: 'Opciones',
|
||||
@@ -376,6 +393,9 @@ const es = {
|
||||
numbers_only: 'Solo números',
|
||||
letters_only: 'Solo letras',
|
||||
regexp_validation: 'Validación de expresión regular',
|
||||
custom_validation: 'Validación Personalizada',
|
||||
length_validation: 'Validación de Longitud',
|
||||
number_range: 'Rango de Números',
|
||||
enter_pdf_password: 'Ingrese la contraseña del PDF',
|
||||
wrong_password: 'Contraseña incorrecta',
|
||||
currency: 'Moneda',
|
||||
@@ -405,6 +425,7 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
fixed: 'Fisso',
|
||||
default: 'Predefinito',
|
||||
save_as_custom_field: 'Salva come personalizzato',
|
||||
kba: 'KBA',
|
||||
@@ -437,6 +458,9 @@ const it = {
|
||||
field_not_found: 'Campo non trovato',
|
||||
clear: 'Cancella',
|
||||
align: 'Allinea',
|
||||
resize: 'Ridimensiona',
|
||||
width: 'Larghezza',
|
||||
height: 'Altezza',
|
||||
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
|
||||
keep: 'Mantieni',
|
||||
@@ -454,6 +478,8 @@ const it = {
|
||||
type_value: 'Inserisci valore',
|
||||
equal: 'Uguale',
|
||||
not_equal: 'Non uguale',
|
||||
greater_than: 'Maggiore di',
|
||||
less_than: 'Minore di',
|
||||
contains: 'Contiene',
|
||||
does_not_contain: 'Non contiene',
|
||||
not_empty: 'Non vuoto',
|
||||
@@ -499,6 +525,7 @@ const it = {
|
||||
page: 'Pagina',
|
||||
draw_new_area: 'Disegna nuova area',
|
||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||
more: 'Altro',
|
||||
add_option: 'Aggiungi opzione',
|
||||
option: 'Opzione',
|
||||
options: 'Opzioni',
|
||||
@@ -579,6 +606,9 @@ const it = {
|
||||
numbers_only: 'Solo numeri',
|
||||
letters_only: 'Solo lettere',
|
||||
regexp_validation: 'Validazione regexp',
|
||||
custom_validation: 'Validazione Personalizzata',
|
||||
length_validation: 'Validazione Lunghezza',
|
||||
number_range: 'Intervallo Numerico',
|
||||
enter_pdf_password: 'Inserisci password PDF',
|
||||
wrong_password: 'Password errata',
|
||||
currency: 'Valuta',
|
||||
@@ -608,6 +638,7 @@ const it = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
fixed: 'Fixo',
|
||||
default: 'Padrão',
|
||||
save_as_custom_field: 'Salvar como personalizado',
|
||||
kba: 'KBA',
|
||||
@@ -644,6 +675,9 @@ const pt = {
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
||||
keep: 'Manter',
|
||||
align: 'Alinhar',
|
||||
resize: 'Redimensionar',
|
||||
width: 'Largura',
|
||||
height: 'Altura',
|
||||
left: 'Esquerda',
|
||||
heading: 'Cabeçalho',
|
||||
validation: 'Validação',
|
||||
@@ -658,6 +692,8 @@ const pt = {
|
||||
price: 'Preço',
|
||||
equal: 'Igual',
|
||||
not_equal: 'Não é igual',
|
||||
greater_than: 'Maior que',
|
||||
less_than: 'Menor que',
|
||||
contains: 'Contém',
|
||||
does_not_contain: 'Não contém',
|
||||
not_empty: 'Não vazio',
|
||||
@@ -698,6 +734,7 @@ const pt = {
|
||||
page: 'Página',
|
||||
draw_new_area: 'Desenhar nova área',
|
||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||
more: 'Mais',
|
||||
add_option: 'Adicionar opção',
|
||||
option: 'Opção',
|
||||
options: 'Opções',
|
||||
@@ -782,6 +819,9 @@ const pt = {
|
||||
numbers_only: 'Somente números',
|
||||
letters_only: 'Somente letras',
|
||||
regexp_validation: 'Validação de expressão regular',
|
||||
custom_validation: 'Validação Personalizada',
|
||||
length_validation: 'Validação de Comprimento',
|
||||
number_range: 'Intervalo de Números',
|
||||
enter_pdf_password: 'Digite a senha do PDF',
|
||||
wrong_password: 'Senha incorreta',
|
||||
currency: 'Moeda',
|
||||
@@ -811,6 +851,7 @@ const pt = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
fixed: 'Fixe',
|
||||
default: 'Par défaut',
|
||||
save_as_custom_field: 'Enregistrer comme personnalisé',
|
||||
kba: 'KBA',
|
||||
@@ -843,6 +884,9 @@ const fr = {
|
||||
field_not_found: 'Champ introuvable',
|
||||
clear: 'Effacer',
|
||||
align: 'Aligner',
|
||||
resize: 'Redimensionner',
|
||||
width: 'Largeur',
|
||||
height: 'Hauteur',
|
||||
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléversé contient des champs de formulaire. Les conserver ou les supprimer ?',
|
||||
keep: 'Conserver',
|
||||
@@ -860,6 +904,8 @@ const fr = {
|
||||
type_value: 'Saisir une valeur',
|
||||
equal: 'Égal',
|
||||
not_equal: 'Différent',
|
||||
greater_than: 'Supérieur à',
|
||||
less_than: 'Inférieur à',
|
||||
contains: 'Contient',
|
||||
does_not_contain: 'Ne contient pas',
|
||||
not_empty: 'Non vide',
|
||||
@@ -905,6 +951,7 @@ const fr = {
|
||||
page: 'Page',
|
||||
draw_new_area: 'Dessiner une zone',
|
||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||
more: 'Plus',
|
||||
add_option: 'Ajouter une option',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
@@ -985,6 +1032,9 @@ const fr = {
|
||||
numbers_only: 'Chiffres uniquement',
|
||||
letters_only: 'Lettres uniquement',
|
||||
regexp_validation: 'Validation par expression régulière',
|
||||
custom_validation: 'Validation Personnalisée',
|
||||
length_validation: 'Validation de Longueur',
|
||||
number_range: 'Plage de Nombres',
|
||||
enter_pdf_password: 'Saisir le mot de passe du PDF',
|
||||
wrong_password: 'Mot de passe incorrect',
|
||||
currency: 'Devise',
|
||||
@@ -1014,6 +1064,7 @@ const fr = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
fixed: 'Fest',
|
||||
default: 'Standard',
|
||||
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
||||
kba: 'KBA',
|
||||
@@ -1046,6 +1097,9 @@ const de = {
|
||||
field_not_found: 'Feld nicht gefunden',
|
||||
clear: 'Leeren',
|
||||
align: 'Ausrichten',
|
||||
resize: 'Größe ändern',
|
||||
width: 'Breite',
|
||||
height: 'Höhe',
|
||||
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?',
|
||||
keep: 'Beibehalten',
|
||||
@@ -1063,6 +1117,8 @@ const de = {
|
||||
type_value: 'Wert eingeben',
|
||||
equal: 'Gleich',
|
||||
not_equal: 'Ungleich',
|
||||
greater_than: 'Größer als',
|
||||
less_than: 'Kleiner als',
|
||||
contains: 'Enthält',
|
||||
does_not_contain: 'Enthält nicht',
|
||||
not_empty: 'Nicht leer',
|
||||
@@ -1108,6 +1164,7 @@ const de = {
|
||||
page: 'Seite',
|
||||
draw_new_area: 'Bereich zeichnen',
|
||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||
more: 'Mehr',
|
||||
add_option: 'Option hinzufügen',
|
||||
option: 'Option',
|
||||
options: 'Optionen',
|
||||
@@ -1188,6 +1245,9 @@ const de = {
|
||||
numbers_only: 'Nur Zahlen',
|
||||
letters_only: 'Nur Buchstaben',
|
||||
regexp_validation: 'RegExp-Validierung',
|
||||
custom_validation: 'Benutzerdefinierte Validierung',
|
||||
length_validation: 'Längenvalidierung',
|
||||
number_range: 'Zahlenbereich',
|
||||
enter_pdf_password: 'PDF-Passwort eingeben',
|
||||
wrong_password: 'Falsches Passwort',
|
||||
currency: 'Währung',
|
||||
@@ -1217,6 +1277,7 @@ const de = {
|
||||
}
|
||||
|
||||
const nl = {
|
||||
fixed: 'Vast',
|
||||
default: 'Standaard',
|
||||
save_as_custom_field: 'Opslaan als aangepast',
|
||||
kba: 'KBA',
|
||||
@@ -1249,6 +1310,9 @@ const nl = {
|
||||
field_not_found: 'Veld niet gevonden',
|
||||
clear: 'Wissen',
|
||||
align: 'Uitlijnen',
|
||||
resize: 'Formaat wijzigen',
|
||||
width: 'Breedte',
|
||||
height: 'Hoogte',
|
||||
add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?',
|
||||
keep: 'Behouden',
|
||||
@@ -1266,6 +1330,8 @@ const nl = {
|
||||
type_value: 'Typ waarde',
|
||||
equal: 'Gelijk aan',
|
||||
not_equal: 'Niet gelijk aan',
|
||||
greater_than: 'Groter dan',
|
||||
less_than: 'Kleiner dan',
|
||||
contains: 'Bevat',
|
||||
does_not_contain: 'Bevat niet',
|
||||
not_empty: 'Niet leeg',
|
||||
@@ -1311,6 +1377,7 @@ const nl = {
|
||||
page: 'Pagina',
|
||||
draw_new_area: 'Nieuw gebied tekenen',
|
||||
copy_to_all_pages: 'Kopieer naar alle pag.',
|
||||
more: 'Meer',
|
||||
add_option: 'Optie toevoegen',
|
||||
option: 'Optie',
|
||||
options: 'Opties',
|
||||
@@ -1391,6 +1458,9 @@ const nl = {
|
||||
numbers_only: 'Alleen cijfers',
|
||||
letters_only: 'Alleen letters',
|
||||
regexp_validation: 'Regex validatie',
|
||||
custom_validation: 'Aangepaste Validatie',
|
||||
length_validation: 'Lengte Validatie',
|
||||
number_range: 'Getalbereik',
|
||||
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
||||
wrong_password: 'Onjuist wachtwoord',
|
||||
currency: 'Valuta',
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
:is-drag="isDrag"
|
||||
@move="onSelectionBoxMove"
|
||||
@contextmenu="openSelectionContextMenu"
|
||||
@close-context-menu="closeSelectionContextMenu"
|
||||
@close-context-menu="closeContextMenu"
|
||||
/>
|
||||
<FieldArea
|
||||
v-for="(item, i) in areas"
|
||||
@@ -48,6 +48,7 @@
|
||||
:default-submitters="defaultSubmitters"
|
||||
:max-page="totalPages - 1"
|
||||
:is-select-mode="isSelectMode"
|
||||
:is-mobile="isMobile"
|
||||
@start-resize="resizeDirection = $event"
|
||||
@stop-resize="resizeDirection = null"
|
||||
@remove="$emit('remove-area', item.area)"
|
||||
@@ -74,29 +75,39 @@
|
||||
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||
:style="selectionRectStyle"
|
||||
/>
|
||||
<ContextMenu
|
||||
v-if="contextMenu"
|
||||
<FieldContextMenu
|
||||
v-if="contextMenu && contextMenu.field"
|
||||
:context-menu="contextMenu"
|
||||
:field="contextMenu.field"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:editable="editable"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
:default-field="defaultFieldsIndex[contextMenu.field.name]"
|
||||
@copy="handleCopy"
|
||||
@delete="handleDelete"
|
||||
@paste="handlePaste"
|
||||
@autodetect-fields="handleAutodetectFields"
|
||||
@close="closeContextMenu"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
@save="save"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
/>
|
||||
<ContextMenu
|
||||
v-if="selectionContextMenu"
|
||||
:context-menu="selectionContextMenu"
|
||||
<SelectionContextMenu
|
||||
v-else-if="contextMenu && contextMenu.areas"
|
||||
:context-menu="contextMenu"
|
||||
:editable="editable"
|
||||
:is-multi-selection="true"
|
||||
:selected-areas="selectedAreasRef.value"
|
||||
:template="template"
|
||||
@copy="handleSelectionCopy"
|
||||
@delete="handleSelectionDelete"
|
||||
@align="handleSelectionAlign"
|
||||
@close="closeSelectionContextMenu"
|
||||
@close="closeContextMenu"
|
||||
/>
|
||||
<PageContextMenu
|
||||
v-else-if="contextMenu && !contextMenu.field && !contextMenu.areas"
|
||||
:context-menu="contextMenu"
|
||||
:editable="editable"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
@paste="handlePaste"
|
||||
@autodetect-fields="handleAutodetectFields"
|
||||
@close="closeContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -119,17 +130,21 @@
|
||||
|
||||
<script>
|
||||
import FieldArea from './area'
|
||||
import ContextMenu from './context_menu'
|
||||
import FieldContextMenu from './field_context_menu'
|
||||
import SelectionContextMenu from './selection_context_menu'
|
||||
import PageContextMenu from './page_context_menu'
|
||||
import SelectionBox from './selection_box'
|
||||
|
||||
export default {
|
||||
name: 'TemplatePage',
|
||||
components: {
|
||||
FieldArea,
|
||||
ContextMenu,
|
||||
FieldContextMenu,
|
||||
SelectionContextMenu,
|
||||
PageContextMenu,
|
||||
SelectionBox
|
||||
},
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef', 'save'],
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
@@ -140,6 +155,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -228,7 +248,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields', 'add-custom-field'],
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||
data () {
|
||||
return {
|
||||
areaRefs: [],
|
||||
@@ -236,8 +256,7 @@ export default {
|
||||
resizeDirection: null,
|
||||
newAreas: [],
|
||||
contextMenu: null,
|
||||
selectionRect: null,
|
||||
selectionContextMenu: null
|
||||
selectionRect: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -305,11 +324,6 @@ export default {
|
||||
return 'text'
|
||||
}
|
||||
},
|
||||
isMobile () {
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
|
||||
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
||||
},
|
||||
resizeDirectionClasses () {
|
||||
return {
|
||||
nwse: 'cursor-nwse-resize',
|
||||
@@ -399,29 +413,32 @@ export default {
|
||||
}
|
||||
},
|
||||
openSelectionContextMenu (event) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const rect = this.$el.getBoundingClientRect()
|
||||
|
||||
this.selectionContextMenu = {
|
||||
this.contextMenu = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
relativeX: (event.clientX - rect.left) / rect.width,
|
||||
relativeY: (event.clientY - rect.top) / rect.height
|
||||
relativeY: (event.clientY - rect.top) / rect.height,
|
||||
areas: this.selectedAreasRef.value
|
||||
}
|
||||
},
|
||||
closeSelectionContextMenu () {
|
||||
this.selectionContextMenu = null
|
||||
},
|
||||
handleSelectionCopy () {
|
||||
this.$emit('copy-selected-areas')
|
||||
this.closeSelectionContextMenu()
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handleSelectionDelete () {
|
||||
this.$emit('delete-selected-areas')
|
||||
this.closeSelectionContextMenu()
|
||||
},
|
||||
handleSelectionAlign (direction) {
|
||||
this.$emit('align-selected-areas', direction)
|
||||
this.closeSelectionContextMenu()
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
closeContextMenu () {
|
||||
this.contextMenu = null
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div
|
||||
ref="menu"
|
||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||
style="min-width: 170px"
|
||||
:style="menuStyle"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||
:disabled="!hasClipboardData"
|
||||
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClipboard class="w-4 h-4" />
|
||||
<span>{{ t('paste') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
@click.stop="handleToggleSelectMode"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClick
|
||||
v-if="!isSelectModeRef.value"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<IconNewSection
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="showAutodetectFields"
|
||||
class="my-1 border-neutral-200"
|
||||
>
|
||||
<button
|
||||
v-if="showAutodetectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('autodetect-fields')"
|
||||
>
|
||||
<IconSparkles class="w-4 h-4" />
|
||||
<span>{{ t('autodetect_fields') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconClipboard, IconClick, IconNewSection, IconSparkles } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'PageContextMenu',
|
||||
components: {
|
||||
IconClipboard,
|
||||
IconClick,
|
||||
IconNewSection,
|
||||
IconSparkles
|
||||
},
|
||||
inject: ['t', 'isSelectModeRef'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['paste', 'close', 'autodetect-fields'],
|
||||
computed: {
|
||||
isMac () {
|
||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||
},
|
||||
hasClipboardData () {
|
||||
try {
|
||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||
|
||||
if (clipboard) {
|
||||
const data = JSON.parse(clipboard)
|
||||
|
||||
return Date.now() - data.timestamp < 3600000
|
||||
}
|
||||
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
menuStyle () {
|
||||
return {
|
||||
left: this.contextMenu.x + 'px',
|
||||
top: this.contextMenu.y + 'px'
|
||||
}
|
||||
},
|
||||
showAutodetectFields () {
|
||||
return this.withFieldsDetection && this.editable
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('keydown', this.onKeyDown)
|
||||
document.addEventListener('mousedown', this.handleClickOutside)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.checkMenuPosition()
|
||||
})
|
||||
},
|
||||
beforeUnmount () {
|
||||
document.removeEventListener('keydown', this.onKeyDown)
|
||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||
},
|
||||
methods: {
|
||||
checkMenuPosition () {
|
||||
if (this.$refs.menu) {
|
||||
const rect = this.$refs.menu.getBoundingClientRect()
|
||||
const overflow = rect.bottom - window.innerHeight
|
||||
|
||||
if (overflow > 0) {
|
||||
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyDown (event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('close')
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('paste')
|
||||
}
|
||||
},
|
||||
handleClickOutside (event) {
|
||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
handleToggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -119,6 +119,7 @@
|
||||
<ConditionsModal
|
||||
:item="item"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('change')"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -145,7 +146,7 @@ export default {
|
||||
GoogleDriveDocumentSettings,
|
||||
IconSortDescending2
|
||||
},
|
||||
inject: ['t'],
|
||||
inject: ['t', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!isShowFontModal && !isShowConditionsModal"
|
||||
ref="menu"
|
||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||
style="min-width: 170px"
|
||||
:style="menuStyle"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<ContextSubmenu
|
||||
:icon="IconLayoutAlignMiddle"
|
||||
:label="t('align')"
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('left')"
|
||||
>
|
||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||
<span>{{ t('align_left') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('right')"
|
||||
>
|
||||
<IconLayoutAlignRight class="w-4 h-4" />
|
||||
<span>{{ t('align_right') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('top')"
|
||||
>
|
||||
<IconLayoutAlignTop class="w-4 h-4" />
|
||||
<span>{{ t('align_top') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('bottom')"
|
||||
>
|
||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||
<span>{{ t('align_bottom') }}</span>
|
||||
</button>
|
||||
</ContextSubmenu>
|
||||
<ContextSubmenu
|
||||
:icon="IconAspectRatio"
|
||||
:label="t('resize')"
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="resizeSelectedAreas('width')"
|
||||
>
|
||||
<IconArrowsHorizontal class="w-4 h-4" />
|
||||
<span>{{ t('width') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="resizeSelectedAreas('height')"
|
||||
>
|
||||
<IconArrowsVertical class="w-4 h-4" />
|
||||
<span>{{ t('height') }}</span>
|
||||
</button>
|
||||
</ContextSubmenu>
|
||||
<hr
|
||||
v-if="showFont || showCondition"
|
||||
class="my-1 border-neutral-200"
|
||||
>
|
||||
<button
|
||||
v-if="showFont"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFontModal"
|
||||
>
|
||||
<IconTypography class="w-4 h-4" />
|
||||
<span>{{ t('font') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<hr class="my-1 border-neutral-200">
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
@click.stop="$emit('copy')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconCopy class="w-4 h-4" />
|
||||
<span>{{ t('copy') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
||||
@click.stop="$emit('delete')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconTrashX class="w-4 h-4" />
|
||||
<span>{{ t('remove') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||
</button>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="isShowFontModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FontModal
|
||||
:field="multiSelectField"
|
||||
:area="contextMenu.area"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="handleSaveMultiSelectFontModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowConditionsModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<ConditionsModal
|
||||
:item="multiSelectField"
|
||||
:build-default-name="buildDefaultName"
|
||||
:exclude-field-uuids="selectedFields.map(f => f.uuid)"
|
||||
@save="handleSaveMultiSelectConditionsModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCopy, IconTrashX, IconTypography, IconRouteAltLeft, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconLayoutAlignMiddle, IconAspectRatio, IconArrowsHorizontal, IconArrowsVertical } from '@tabler/icons-vue'
|
||||
import FontModal from './font_modal'
|
||||
import ConditionsModal from './conditions_modal'
|
||||
import ContextSubmenu from './field_context_submenu'
|
||||
import Field from './field'
|
||||
import FieldType from './field_type'
|
||||
|
||||
export default {
|
||||
name: 'SelectionContextMenu',
|
||||
components: {
|
||||
IconCopy,
|
||||
IconTrashX,
|
||||
IconTypography,
|
||||
IconRouteAltLeft,
|
||||
IconLayoutAlignLeft,
|
||||
IconLayoutAlignRight,
|
||||
IconLayoutAlignTop,
|
||||
IconLayoutAlignBottom,
|
||||
FontModal,
|
||||
IconArrowsHorizontal,
|
||||
IconArrowsVertical,
|
||||
ConditionsModal,
|
||||
ContextSubmenu
|
||||
},
|
||||
inject: ['t', 'save', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
withCondition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['copy', 'delete', 'close'],
|
||||
data () {
|
||||
return {
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
multiSelectField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalContainerEl () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
selectedFields () {
|
||||
return this.selectedAreasRef.value.map((area) => {
|
||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||
}).filter(Boolean)
|
||||
},
|
||||
isMac () {
|
||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||
},
|
||||
menuStyle () {
|
||||
return {
|
||||
left: this.contextMenu.x + 'px',
|
||||
top: this.contextMenu.y + 'px'
|
||||
}
|
||||
},
|
||||
showFont () {
|
||||
return true
|
||||
},
|
||||
showCondition () {
|
||||
return this.withCondition
|
||||
},
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('keydown', this.onKeyDown)
|
||||
document.addEventListener('mousedown', this.handleClickOutside)
|
||||
|
||||
this.$nextTick(() => this.checkMenuPosition())
|
||||
},
|
||||
beforeUnmount () {
|
||||
document.removeEventListener('keydown', this.onKeyDown)
|
||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||
},
|
||||
methods: {
|
||||
IconLayoutAlignMiddle,
|
||||
IconAspectRatio,
|
||||
buildDefaultName: Field.methods.buildDefaultName,
|
||||
checkMenuPosition () {
|
||||
if (this.$refs.menu) {
|
||||
const rect = this.$refs.menu.getBoundingClientRect()
|
||||
const overflow = rect.bottom - window.innerHeight
|
||||
|
||||
if (overflow > 0) {
|
||||
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyDown (event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
handleClickOutside (event) {
|
||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
openFontModal () {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
preferences: {}
|
||||
}
|
||||
|
||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||
|
||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||
}
|
||||
|
||||
this.isShowFontModal = true
|
||||
},
|
||||
openConditionModal () {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
conditions: []
|
||||
}
|
||||
|
||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||
|
||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||
}
|
||||
|
||||
this.isShowConditionsModal = true
|
||||
},
|
||||
closeModal () {
|
||||
this.isShowFontModal = false
|
||||
this.isShowConditionsModal = false
|
||||
this.multiSelectField = null
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
alignSelectedAreas (direction) {
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
let targetValue
|
||||
|
||||
if (direction === 'left') {
|
||||
targetValue = Math.min(...areas.map(a => a.x))
|
||||
areas.forEach((area) => { area.x = targetValue })
|
||||
} else if (direction === 'right') {
|
||||
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||
} else if (direction === 'top') {
|
||||
targetValue = Math.min(...areas.map(a => a.y))
|
||||
areas.forEach((area) => { area.y = targetValue })
|
||||
} else if (direction === 'bottom') {
|
||||
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||
}
|
||||
|
||||
this.save()
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
resizeSelectedAreas (dimension) {
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
const values = areas.map(a => dimension === 'width' ? a.w : a.h).sort((a, b) => a - b)
|
||||
const medianValue = values[Math.floor(values.length / 2)]
|
||||
|
||||
if (dimension === 'width') {
|
||||
areas.forEach((area) => { area.w = medianValue })
|
||||
} else if (dimension === 'height') {
|
||||
areas.forEach((area) => {
|
||||
const diff = medianValue - area.h
|
||||
area.y = area.y - diff
|
||||
area.h = medianValue
|
||||
})
|
||||
}
|
||||
|
||||
this.save()
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
handleSaveMultiSelectFontModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
},
|
||||
handleSaveMultiSelectConditionsModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -290,7 +290,11 @@ export default {
|
||||
if (resp.ok) {
|
||||
resp.json().then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.value = ''
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
})
|
||||
} else if (resp.status === 422) {
|
||||
|
||||
@@ -164,11 +164,13 @@ class ProcessSubmitterCompletionJob
|
||||
next_submitter_items =
|
||||
if submission.template_submitters.any? { |s| s['order'] }
|
||||
submitter_groups =
|
||||
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
|
||||
submission.template_submitters
|
||||
.group_by.with_index { |s, index| s['order'] || index }
|
||||
.sort_by(&:first).pluck(1)
|
||||
|
||||
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
|
||||
current_group_index = submitter_groups.index { |group| group.any? { |s| s['uuid'] == submitter.uuid } }
|
||||
|
||||
if submitter_groups[current_group_index + 1] &&
|
||||
if current_group_index && submitter_groups[current_group_index + 1] &&
|
||||
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
|
||||
.compact.all?(&:completed_at?)
|
||||
submitter_groups[current_group_index + 1]
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="form-control">
|
||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||
<div class="flex items-center space-x-6">
|
||||
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
|
||||
<% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
|
||||
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||
<%= label %>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<% end %>
|
||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||
<div class="w-full flex items-center <%= 'justify-end' if align == 'right' %>">
|
||||
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
|
||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
||||
<% if value[index] %>
|
||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||
|
||||
+2
-3
@@ -30,9 +30,8 @@ production:
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
|
||||
<<: *default
|
||||
url: <%= ENV['DATABASE_URL'] %>
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Amysql/) %>
|
||||
adapter: mysql2
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Amysql|\Atrilogy/) %>
|
||||
encoding: utf8mb4
|
||||
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
|
||||
url: <%= ENV['DATABASE_URL'] %>
|
||||
url: <%= ENV['DATABASE_URL'].sub(/\Amysql2?/, 'trilogy') %>
|
||||
<% end %>
|
||||
|
||||
@@ -85,6 +85,7 @@ Rails.application.configure do
|
||||
domain: ENV.fetch('SMTP_DOMAIN', nil),
|
||||
user_name: ENV.fetch('SMTP_USERNAME', nil),
|
||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||
openssl_verify_mode: ENV['SMTP_SSL_VERIFY'] == 'false' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
|
||||
authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
|
||||
enable_starttls: ENV['SMTP_ENABLE_STARTTLS'] != 'false'
|
||||
}.compact
|
||||
|
||||
@@ -47,6 +47,9 @@ module ActionMailerConfigsInterceptor
|
||||
|
||||
is_tls = value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||
is_ssl = value['security'] == 'ssl'
|
||||
is_noverify = value['security'] == 'noverify'
|
||||
|
||||
enable_starttls = is_noverify ? :enable_starttls_auto : :enable_starttls
|
||||
|
||||
{
|
||||
user_name: value['username'],
|
||||
@@ -54,9 +57,9 @@ module ActionMailerConfigsInterceptor
|
||||
address: value['host'],
|
||||
port: value['port'],
|
||||
domain: value['domain'],
|
||||
openssl_verify_mode: value['security'] == 'noverify' ? OpenSSL::SSL::VERIFY_NONE : nil,
|
||||
openssl_verify_mode: is_noverify ? OpenSSL::SSL::VERIFY_NONE : nil,
|
||||
authentication: value['password'].present? ? value.fetch('authentication', 'plain') : nil,
|
||||
enable_starttls: !is_tls && !is_ssl,
|
||||
enable_starttls => !is_tls && !is_ssl,
|
||||
open_timeout: OPEN_TIMEOUT,
|
||||
read_timeout: READ_TIMEOUT,
|
||||
ssl: is_ssl,
|
||||
|
||||
@@ -6,9 +6,16 @@ class NormalizeClientIpMiddleware
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if env['HTTP_CLIENT_IP'].present? && env['HTTP_X_CLIENT_IP'].present? &&
|
||||
env['HTTP_CLIENT_IP'].starts_with?("#{env['HTTP_X_CLIENT_IP']}:")
|
||||
env['HTTP_CLIENT_IP'] = env['HTTP_X_CLIENT_IP']
|
||||
if env['HTTP_CLIENT_IP'].present?
|
||||
if env['HTTP_X_CLIENT_IP'].present? &&
|
||||
env['HTTP_CLIENT_IP'].starts_with?("#{env['HTTP_X_CLIENT_IP']}:")
|
||||
env['HTTP_CLIENT_IP'] = env['HTTP_X_CLIENT_IP']
|
||||
end
|
||||
|
||||
if env['HTTP_X_FORWARDED_FOR'].present? &&
|
||||
env['HTTP_X_FORWARDED_FOR'].sub(/:\d+\z/, '') == env['HTTP_CLIENT_IP']
|
||||
env['HTTP_X_FORWARDED_FOR'] = env['HTTP_CLIENT_IP']
|
||||
end
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
|
||||
@@ -61,7 +61,8 @@ module Submissions
|
||||
|
||||
submission.template_submitters << template_submitter
|
||||
|
||||
is_order_sent = submitters_order == 'random' || (template_submitter['order'] || index).zero?
|
||||
is_order_sent = submitters_order == 'random' ||
|
||||
(template_submitter['order'] || submitter_attrs[:index] || index).zero?
|
||||
|
||||
build_submitter(submission:, attrs: submitter_attrs,
|
||||
uuid:, is_order_sent:, user:, params:,
|
||||
@@ -308,7 +309,7 @@ module Submissions
|
||||
uuid = attrs[:uuid].presence
|
||||
uuid ||= submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
|
||||
|
||||
uuid || submitters[index]&.dig('uuid')
|
||||
uuid || submitters[attrs[:index] || index]&.dig('uuid')
|
||||
end
|
||||
|
||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
|
||||
|
||||
@@ -19,7 +19,7 @@ module Submissions
|
||||
total_wait_time ||= 0
|
||||
key = ['result_attachments', submitter.id].join(':')
|
||||
|
||||
return submitter.documents.reset if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||
return submitter.documents.reload if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||
|
||||
events = ApplicationRecord.uncached { LockEvent.where(key:).order(:id).to_a }
|
||||
|
||||
@@ -32,6 +32,8 @@ module Submissions
|
||||
|
||||
LockEvent.create!(key:, event_name: :complete)
|
||||
|
||||
submitter.documents.reset
|
||||
|
||||
documents
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
@@ -60,7 +62,7 @@ module Submissions
|
||||
LockEvent.where(key: ['result_attachments', submitter.id].join(':')).order(:id).last
|
||||
end
|
||||
|
||||
break submitter.documents.reset if last_event.event_name.in?(%w[complete fail])
|
||||
break submitter.documents.reload if last_event.event_name.in?(%w[complete fail])
|
||||
|
||||
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
|
||||
end
|
||||
|
||||
@@ -200,7 +200,9 @@ module Submissions
|
||||
|
||||
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
||||
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil)
|
||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
||||
cell_layouters = Hash.new do |hash, valign|
|
||||
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
||||
end
|
||||
|
||||
attachments_data_cache = {}
|
||||
|
||||
@@ -550,6 +552,8 @@ module Submissions
|
||||
)
|
||||
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
|
||||
cell_width = area['cell_w'] * width
|
||||
cell_valign = field.dig('preferences', 'valign').to_s.presence || 'center'
|
||||
cell_layouter = cell_layouters[cell_valign]
|
||||
|
||||
if (mask = field.dig('preferences', 'mask').presence)
|
||||
value = TextUtils.mask_value(value, mask)
|
||||
@@ -790,10 +794,10 @@ module Submissions
|
||||
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
||||
latest_submitter = find_last_submitter(submission, submitter:)
|
||||
|
||||
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||
documents = Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||
documents ||= submission.schema_documents
|
||||
|
||||
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
||||
documents ||= submission.schema_documents.preload(:blob)
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||
|
||||
attachment_uuids = Submissions.filtered_conditions_schema(submission).pluck('attachment_uuid')
|
||||
attachments_index = documents.index_by { |a| a.metadata['original_uuid'] || a.uuid }
|
||||
|
||||
@@ -10,7 +10,8 @@ module Submissions
|
||||
|
||||
Array.wrap(submissions_params).each do |submission|
|
||||
submission[:submitters].each_with_index do |submitter, index|
|
||||
_, new_attachments, new_fields = normalize_submitter_params!(submitter, template, index, add_fields:)
|
||||
_, new_attachments, new_fields =
|
||||
normalize_submitter_params!(submitter, template, submitter[:index] || index, add_fields:)
|
||||
|
||||
attachments.push(*new_attachments)
|
||||
fields.push(*new_fields)
|
||||
|
||||
@@ -14,6 +14,7 @@ module Submissions
|
||||
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
|
||||
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
|
||||
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||
@@ -32,6 +33,10 @@ module Submissions
|
||||
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
||||
end
|
||||
|
||||
if params[:include].to_s.include?('fields')
|
||||
json['fields'] = submission.template_fields || submission.template&.fields
|
||||
end
|
||||
|
||||
if submitters.all?(&:completed_at?)
|
||||
last_submitter = submitters.max_by(&:completed_at)
|
||||
|
||||
@@ -57,6 +62,7 @@ module Submissions
|
||||
|
||||
json
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def build_status(submission, submitters)
|
||||
if submitters.any?(&:declined_at?)
|
||||
|
||||
@@ -48,6 +48,10 @@ module Submitters
|
||||
maybe_set_signature_reason!(values, submitter, params)
|
||||
validate_values!(values, submitter, params, request)
|
||||
|
||||
if (touch_attachment_uuid = params[:touch_attachment_uuid].presence)
|
||||
ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at)
|
||||
end
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true'
|
||||
|
||||
submitter.save!
|
||||
@@ -317,17 +321,33 @@ module Submitters
|
||||
end.exclude?(false)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def check_field_condition(condition, submitter_values, fields_uuid_index)
|
||||
value = submitter_values[condition['field_uuid']]
|
||||
field = fields_uuid_index[condition['field_uuid']]
|
||||
|
||||
case condition['action']
|
||||
when 'empty', 'unchecked'
|
||||
value.blank?
|
||||
when 'not_empty', 'checked'
|
||||
value.present?
|
||||
when 'equal', 'contains'
|
||||
field = fields_uuid_index[condition['field_uuid']]
|
||||
when ->(action) { action == 'equal' && field&.dig('type') == 'number' }
|
||||
return false if value.blank? || condition['value'].blank?
|
||||
|
||||
(value.to_f - condition['value'].to_f).abs < Float::EPSILON
|
||||
when ->(action) { action == 'not_equal' && field&.dig('type') == 'number' }
|
||||
return false if value.blank? || condition['value'].blank?
|
||||
|
||||
(value.to_f - condition['value'].to_f).abs > Float::EPSILON
|
||||
when 'greater_than'
|
||||
return false if field.nil? || value.blank? || condition['value'].blank?
|
||||
|
||||
value.to_f > condition['value'].to_f
|
||||
when 'less_than'
|
||||
return false if field.nil? || value.blank? || condition['value'].blank?
|
||||
|
||||
value.to_f < condition['value'].to_f
|
||||
when 'equal', 'contains'
|
||||
return true unless field
|
||||
|
||||
values = Array.wrap(value)
|
||||
@@ -340,8 +360,6 @@ module Submitters
|
||||
|
||||
values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}")
|
||||
when 'not_equal', 'does_not_contain'
|
||||
field = fields_uuid_index[condition['field_uuid']]
|
||||
|
||||
return true unless field
|
||||
return false unless field['options']
|
||||
|
||||
@@ -356,6 +374,7 @@ module Submitters
|
||||
true
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
||||
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||
|
||||
Reference in New Issue
Block a user