diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index b8be0e3a..824fa6c0 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -89,6 +89,7 @@ module Api submitters: [%i[name uuid]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, { preferences: {}, + conditions: [%i[field_uuid value action]], options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } ] diff --git a/app/javascript/application.js b/app/javascript/application.js index a23ce174..85c7fa2a 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -91,6 +91,7 @@ window.customElements.define('template-builder', class extends HTMLElement { editable: this.dataset.editable !== 'false', withPayment: this.dataset.withPayment === 'true', withFormula: this.dataset.withFormula === 'true', + withConditions: this.dataset.withConditions === 'true', currencies: (this.dataset.currencies || '').split(',').filter(Boolean), acceptFileTypes: this.dataset.acceptFileTypes, isDirectUpload: this.dataset.isDirectUpload === 'true' diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 991fc9cd..f0d6429a 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -125,3 +125,7 @@ button[disabled] .enabled { outline-offset: 3px; outline-color: hsl(var(--bc) / 0.2); } + +select:required:invalid { + color: gray !important; +} diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 58502fec..f8d7f2af 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -621,6 +621,13 @@ export default { submitterSlug () { return this.submitter.slug }, + fieldsUuidIndex () { + return this.fields.reduce((acc, f) => { + acc[f.uuid] = f + + return acc + }, {}) + }, previousInitialsValue () { const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid]) @@ -646,10 +653,12 @@ export default { return this.fields.filter((f) => !f.readonly).reduce((acc, f) => { const prevStep = acc[acc.length - 1] - if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') { - prevStep.push(f) - } else { - acc.push([f]) + if (this.checkFieldConditions(f)) { + if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') { + prevStep.push(f) + } else { + acc.push([f]) + } } return acc @@ -739,6 +748,40 @@ export default { t (key) { return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key }, + checkFieldConditions (field) { + if (field.conditions?.length) { + return field.conditions.reduce((acc, c) => { + if (['empty', 'unchecked'].includes(c.action)) { + return acc && isEmpty(this.values[c.field_uuid]) + } else if (['not_empty', 'checked'].includes(c.action)) { + return acc && !isEmpty(this.values[c.field_uuid]) + } else if (['equal', 'contains'].includes(c.action)) { + const field = this.fieldsUuidIndex[c.field_uuid] + const option = field.options.find((o) => o.uuid === c.value) + const values = [this.values[c.field_uuid]].flat() + + return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) + } else if (['not_equal', 'does_not_contain'].includes(c.action)) { + const field = this.fieldsUuidIndex[c.field_uuid] + const option = field.options.find((o) => o.uuid === c.value) + const values = [this.values[c.field_uuid]].flat() + + return acc && !values.includes(this.optionValue(option, field.options.indexOf(option))) + } else { + return acc + } + }, true) + } else { + return true + } + }, + optionValue (option, index) { + if (option.value) { + return option.value + } else { + return `${this.t('option')} ${index + 1}` + } + }, maybeTrackEmailClick () { const { queryParams } = this diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 90b19df9..7b3e4b70 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -323,6 +323,7 @@ export default { withPhone: this.withPhone, withPayment: this.withPayment, withFormula: this.withFormula, + withConditions: this.withConditions, defaultDrawFieldType: this.defaultDrawFieldType, selectedAreaRef: computed(() => this.selectedAreaRef), fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef) @@ -457,6 +458,11 @@ export default { required: false, default: false }, + withConditions: { + type: Boolean, + required: false, + default: false + }, onlyDefinedFields: { type: Boolean, required: false, diff --git a/app/javascript/template_builder/conditions_modal.vue b/app/javascript/template_builder/conditions_modal.vue new file mode 100644 index 00000000..bedaabab --- /dev/null +++ b/app/javascript/template_builder/conditions_modal.vue @@ -0,0 +1,195 @@ + + + diff --git a/app/javascript/template_builder/field.vue b/app/javascript/template_builder/field.vue index 2e426b57..9101e9f3 100644 --- a/app/javascript/template_builder/field.vue +++ b/app/javascript/template_builder/field.vue @@ -68,7 +68,7 @@ + {{ t('required') }} -
  • - -

  • +
  • + +
  • +
  • + +
  • +
  • + + + @@ -414,7 +442,8 @@ import Contenteditable from './contenteditable' import FieldType from './field_type' import PaymentSettings from './payment_settings' import FormulaModal from './formula_modal' -import { IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue' +import ConditionsModal from './conditions_modal' +import { IconRouteAltLeft, IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue' import { v4 } from 'uuid' export default { @@ -426,12 +455,14 @@ export default { PaymentSettings, IconNewSection, FormulaModal, + ConditionsModal, + IconRouteAltLeft, IconTrashX, IconMathFunction, IconCopy, FieldType }, - inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'withFormula'], + inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'], props: { field: { type: Object, @@ -454,6 +485,7 @@ export default { isNameFocus: false, showPaymentModal: false, isShowFormulaModal: false, + isShowConditionsModal: false, renderDropdown: false } }, diff --git a/app/javascript/template_builder/fields.vue b/app/javascript/template_builder/fields.vue index 8abe3de6..f4662408 100644 --- a/app/javascript/template_builder/fields.vue +++ b/app/javascript/template_builder/fields.vue @@ -278,6 +278,14 @@ export default { removeField (field) { this.fields.splice(this.fields.indexOf(field), 1) + this.fields.forEach((f) => { + (f.conditions || []).forEach((c) => { + if (c.field_uuid === field.uuid) { + f.conditions.splice(f.conditions.indexOf(c), 1) + } + }) + }) + this.save() }, addField (type, area = null) { diff --git a/app/javascript/template_builder/formula_modal.vue b/app/javascript/template_builder/formula_modal.vue index 144750e2..5c2b0e13 100644 --- a/app/javascript/template_builder/formula_modal.vue +++ b/app/javascript/template_builder/formula_modal.vue @@ -9,7 +9,7 @@