diff --git a/app/controllers/account_custom_fields_controller.rb b/app/controllers/account_custom_fields_controller.rb
new file mode 100644
index 00000000..74e27ea0
--- /dev/null
+++ b/app/controllers/account_custom_fields_controller.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class AccountCustomFieldsController < ApplicationController
+ before_action :load_account_config, only: :create
+
+ def create
+ authorize!(:create, Template)
+
+ @account_config.update!(account_config_params)
+
+ render json: @account_config.value
+ end
+
+ private
+
+ def load_account_config
+ @account_config =
+ AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
+ end
+
+ def account_config_params
+ params.permit(
+ value: [[:uuid, :name, :type,
+ :required, :readonly, :default_value,
+ :title, :description,
+ { preferences: {},
+ default_value: [],
+ options: [%i[value uuid]],
+ validation: %i[message pattern min max step],
+ areas: [%i[x y w h cell_w option_uuid]] }]]
+ )
+ end
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 4b5988c9..49cd9516 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -155,6 +155,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),
+ customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
@@ -164,6 +165,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
+ withCustomFields: true,
withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true',
diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue
index 664acf96..7f87edbe 100644
--- a/app/javascript/template_builder/area.vue
+++ b/app/javascript/template_builder/area.vue
@@ -145,7 +145,9 @@
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
+ @add-custom-field="$emit('add-custom-field')"
@click-condition="isShowConditionsModal = true"
+ @save="save"
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
/>
@@ -471,7 +473,7 @@ export default {
default: false
}
},
- emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
+ emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
data () {
return {
isShowFormulaModal: false,
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue
index 33bc748b..f029cc57 100644
--- a/app/javascript/template_builder/builder.vue
+++ b/app/javascript/template_builder/builder.vue
@@ -20,8 +20,9 @@
/>
+
{{ t('draw_strikethrough_the_document') }}
@@ -458,10 +461,10 @@ {{ t('cancel') }} {{ t('or_add_field_without_drawing') }} @@ -477,6 +480,8 @@ :with-help="withHelp" :default-submitters="defaultSubmitters" :draw-field-type="drawFieldType" + :custom-fields="customFields" + :with-custom-fields="withCustomFields" :with-fields-search="withFieldsSearch" :default-fields="[...defaultRequiredFields, ...defaultFields]" :template="template" @@ -493,6 +498,7 @@ @set-draw="[drawField = $event.field, drawOption = $event.option]" @select-submitter="selectedSubmitter = $event" @set-draw-type="[drawFieldType = $event, showDrawField = true]" + @set-draw-custom-field="[drawCustomField = $event, showDrawField = true]" @set-drag="dragField = $event" @set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event" @change-submitter="selectedSubmitter = $event" @@ -632,10 +638,12 @@ export default { isPaymentConnected: this.isPaymentConnected, withFormula: this.withFormula, withConditions: this.withConditions, + withCustomFields: this.withCustomFields, isInlineSize: this.isInlineSize, defaultDrawFieldType: this.defaultDrawFieldType, selectedAreasRef: computed(() => this.selectedAreasRef), fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef), + customDragFieldRef: computed(() => this.customDragFieldRef), isSelectModeRef: computed(() => this.isSelectModeRef), isCmdKeyRef: computed(() => this.isCmdKeyRef) } @@ -705,6 +713,16 @@ export default { required: false, default: false }, + withCustomFields: { + type: Boolean, + required: false, + default: false + }, + customFields: { + type: Array, + required: false, + default: () => [] + }, withAddPageButton: { type: Boolean, required: false, @@ -902,6 +920,7 @@ export default { pendingFieldAttachmentUuids: [], drawField: null, drawFieldType: null, + drawCustomField: null, drawOption: null, dragField: null, isDragFile: false @@ -912,6 +931,7 @@ export default { isSelectModeRef: () => ref(false), isCmdKeyRef: () => ref(false), fieldsDragFieldRef: () => ref(), + customDragFieldRef: () => ref(), selectedAreasRef: () => ref([]), language () { return this.locale.split('-')[0].toLowerCase() @@ -1062,6 +1082,30 @@ export default { }, methods: { toRaw, + addCustomField (field) { + return this.$refs.fields.addCustomField(field) + }, + addCustomFieldWithoutDraw () { + const customField = this.drawCustomField + + const field = JSON.parse(JSON.stringify(customField)) + + field.uuid = v4() + field.submitter_uuid = this.selectedSubmitter.uuid + field.areas = [] + + if (field.options?.length) { + field.options = field.options.map(opt => ({ ...opt, uuid: v4() })) + } + + delete field.conditions + + this.insertField(field) + this.save() + + this.drawCustomField = null + this.showDrawField = false + }, toggleSelectMode () { this.isSelectModeRef.value = !this.isSelectModeRef.value @@ -1514,6 +1558,7 @@ export default { clearDrawField () { this.drawField = null this.drawOption = null + this.drawCustomField = null this.showDrawField = false if (!this.withSelectedFieldType) { @@ -1951,6 +1996,10 @@ export default { } }, onDraw ({ area, isTooSmall }) { + if (this.drawCustomField) { + return this.onDrawCustomField(area) + } + if (this.drawField) { if (this.drawOption) { const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid) @@ -2061,6 +2110,10 @@ export default { return } + if (this.customDragFieldRef.value) { + return this.dropCustomField(area) + } + const field = this.fieldsDragFieldRef.value || { name: '', uuid: v4(), @@ -2153,6 +2206,122 @@ export default { }) } }, + dropCustomField (area) { + const customField = this.customDragFieldRef.value + const customAreas = customField.areas || [] + + const field = JSON.parse(JSON.stringify(customField)) + + field.uuid = v4() + field.submitter_uuid = this.selectedSubmitter.uuid + field.areas = [] + + if (field.options?.length) { + field.options = field.options.map(opt => ({ ...opt, uuid: v4() })) + } + + delete field.conditions + + const dropX = (area.x - 6) / area.maskW + const dropY = area.y / area.maskH + + if (customAreas.length > 0) { + const refArea = customAreas[0] + + customAreas.forEach((customArea) => { + const fieldArea = { + x: dropX + (customArea.x - refArea.x), + y: dropY + (customArea.y - refArea.y) - (customArea.h / 2), + w: customArea.w, + h: customArea.h, + page: area.page, + attachment_uuid: area.attachment_uuid + } + + if (customArea.cell_w) { + fieldArea.cell_w = customArea.cell_w + } + + if (customArea.option_uuid && field.options?.length) { + const optionIndex = customField.options.findIndex(o => o.uuid === customArea.option_uuid) + if (optionIndex !== -1) { + fieldArea.option_uuid = field.options[optionIndex].uuid + } + } + + field.areas.push(fieldArea) + }) + } else { + const fieldArea = { + x: dropX, + y: dropY, + page: area.page, + attachment_uuid: area.attachment_uuid + } + + this.assignDropAreaSize(fieldArea, field, area) + + field.areas.push(fieldArea) + } + + this.selectedAreasRef.value = [field.areas[0]] + + this.insertField(field) + this.save() + + document.activeElement?.blur() + }, + onDrawCustomField (area) { + const customField = this.drawCustomField + const customAreas = customField.areas || [] + + const field = JSON.parse(JSON.stringify(customField)) + + field.uuid = v4() + field.submitter_uuid = this.selectedSubmitter.uuid + field.areas = [] + + if (field.options?.length) { + field.options = field.options.map(opt => ({ ...opt, uuid: v4() })) + } + + delete field.conditions + + const firstArea = { + x: area.x, + y: area.y, + w: area.w || customAreas[0]?.w, + h: area.h || customAreas[0]?.h, + page: area.page, + attachment_uuid: area.attachment_uuid + } + + if (!firstArea.w || !firstArea.h) { + if (customAreas[0]) { + firstArea.w = customAreas[0].w + firstArea.h = customAreas[0].h + } else { + this.setDefaultAreaSize(firstArea, field.type) + } + + firstArea.x -= firstArea.w / 2 + firstArea.y -= firstArea.h / 2 + } + + if (field.options?.length) { + firstArea.option_uuid = field.options[0].uuid + } + + field.areas.push(firstArea) + + this.selectedAreasRef.value = [field.areas[0]] + + this.insertField(field) + this.save() + + this.drawCustomField = null + this.showDrawField = false + }, assignDropAreaSize (fieldArea, field, area) { const fieldType = field.type || 'text' diff --git a/app/javascript/template_builder/contenteditable.vue b/app/javascript/template_builder/contenteditable.vue index b6dcd909..1ee20ddb 100644 --- a/app/javascript/template_builder/contenteditable.vue +++ b/app/javascript/template_builder/contenteditable.vue @@ -6,18 +6,24 @@ {{ value }} @@ -28,7 +34,7 @@ :class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }" :width="iconWidth + 4" :stroke-width="iconStrokeWidth" - @click="[focusContenteditable(), selectOnEditClick && selectContent()]" + @click="clickEdit" />