diff --git a/Dockerfile b/Dockerfile index 3d296004..f6412c78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Gemfile b/Gemfile index 650cde01..61c84807 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index e4419ff0..7ad54e84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 93c2190f..22af24d9 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -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, diff --git a/app/javascript/draw.js b/app/javascript/draw.js index 2a0a917c..3b95b5ea 100644 --- a/app/javascript/draw.js +++ b/app/javascript/draw.js @@ -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 diff --git a/app/javascript/elements/field_condition.js b/app/javascript/elements/field_condition.js index 609d9955..b99a8bb3 100644 --- a/app/javascript/elements/field_condition.js +++ b/app/javascript/elements/field_condition.js @@ -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)) diff --git a/app/javascript/elements/signature_form.js b/app/javascript/elements/signature_form.js index b7ce43e5..5fc9af48 100644 --- a/app/javascript/elements/signature_form.js +++ b/app/javascript/elements/signature_form.js @@ -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 diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 30a0255e..affd57a6 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -182,8 +182,8 @@
{ 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 { diff --git a/app/javascript/submission_form/kba_step.vue b/app/javascript/submission_form/kba_step.vue index 15958ad3..03c7431a 100644 --- a/app/javascript/submission_form/kba_step.vue +++ b/app/javascript/submission_form/kba_step.vue @@ -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') } diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index d7cb86fb..91937ec9 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -127,6 +127,12 @@ type="hidden" :name="`values[${field.uuid}]`" > + 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) } diff --git a/app/javascript/submission_form/verification_step.vue b/app/javascript/submission_form/verification_step.vue index 1f711c5e..29b3b590 100644 --- a/app/javascript/submission_form/verification_step.vue +++ b/app/javascript/submission_form/verification_step.vue @@ -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 }, diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index 7f87edbe..e071e9ee 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -134,6 +134,7 @@ @focusout="maybeBlurSettings" > +
+ 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. +
@@ -252,7 +259,8 @@
@@ -346,6 +355,7 @@ :editable="editable && !defaultField" :default-field="defaultField" :build-default-name="buildDefaultName" + @save="save" @close="isShowFontModal = false" /> @@ -357,6 +367,7 @@ :item="field" :build-default-name="buildDefaultName" :default-field="defaultField" + @save="save" @close="isShowConditionsModal = false" /> @@ -369,6 +380,7 @@ :editable="editable && !defaultField" :default-field="defaultField" :build-default-name="buildDefaultName" + @save="save" @close="isShowDescriptionModal = false" /> @@ -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) { diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index b0f9e9cd..34953da1 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -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" /> 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 diff --git a/app/javascript/template_builder/conditions_modal.vue b/app/javascript/template_builder/conditions_modal.vue index 81e54407..d8219ff6 100644 --- a/app/javascript/template_builder/conditions_modal.vue +++ b/app/javascript/template_builder/conditions_modal.vue @@ -9,7 +9,7 @@ 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') } } diff --git a/app/javascript/template_builder/contenteditable.vue b/app/javascript/template_builder/contenteditable.vue index 1ee20ddb..c17724fb 100644 --- a/app/javascript/template_builder/contenteditable.vue +++ b/app/javascript/template_builder/contenteditable.vue @@ -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 diff --git a/app/javascript/template_builder/context_menu.vue b/app/javascript/template_builder/context_menu.vue deleted file mode 100644 index 4ad6972b..00000000 --- a/app/javascript/template_builder/context_menu.vue +++ /dev/null @@ -1,546 +0,0 @@ - - - diff --git a/app/javascript/template_builder/custom_field.vue b/app/javascript/template_builder/custom_field.vue index ee60ca3e..21317261 100644 --- a/app/javascript/template_builder/custom_field.vue +++ b/app/javascript/template_builder/custom_field.vue @@ -125,6 +125,7 @@ :field="field" :default-field="defaultField" :build-default-name="buildDefaultName" + @save="$emit('save')" @close="isShowFormulaModal = false" /> @@ -136,6 +137,7 @@ :field="field" :default-field="defaultField" :build-default-name="buildDefaultName" + @save="$emit('save')" @close="isShowFontModal = false" /> @@ -147,6 +149,7 @@ :field="field" :default-field="defaultField" :build-default-name="buildDefaultName" + @save="$emit('save')" @close="isShowDescriptionModal = false" /> diff --git a/app/javascript/template_builder/description_modal.vue b/app/javascript/template_builder/description_modal.vue index 946d961e..b387733f 100644 --- a/app/javascript/template_builder/description_modal.vue +++ b/app/javascript/template_builder/description_modal.vue @@ -9,7 +9,7 @@