Work in progress
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
|
||||
<template>
|
||||
<div v-if="isMounted" class="flex flex-wrap">
|
||||
<div class="w-full font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ property.name }}
|
||||
</div>
|
||||
<SelectInput v-model="content.operator" class="w-full" :options="operators"
|
||||
:name="'operator_'+property.id" placeholder="Comparison operator"
|
||||
@update:modelValue="operatorChanged()"
|
||||
/>
|
||||
|
||||
<template v-if="hasInput">
|
||||
<component v-bind="inputComponentData" :is="inputComponentData.component" v-model="content.value" class="w-full"
|
||||
:name="'value_'+property.id" placeholder="Filter Value"
|
||||
@update:modelValue="emitInput()"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OpenFilters from '../../../../../../data/open_filters.json'
|
||||
|
||||
export default {
|
||||
components: { },
|
||||
props: {
|
||||
value: { required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
content: { ...this.value },
|
||||
available_filters: OpenFilters,
|
||||
isMounted: false,
|
||||
hasInput: false,
|
||||
inputComponent: {
|
||||
text: 'TextInput',
|
||||
number: 'TextInput',
|
||||
select: 'SelectInput',
|
||||
multi_select: 'SelectInput',
|
||||
date: 'DateInput',
|
||||
files: 'FileInput',
|
||||
checkbox: 'CheckboxInput',
|
||||
url: 'TextInput',
|
||||
email: 'TextInput',
|
||||
phone_number: 'TextInput'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Return type of input, and props for that input
|
||||
inputComponentData () {
|
||||
const componentData = {
|
||||
component: this.inputComponent[this.property.type],
|
||||
name: this.property.id,
|
||||
required: true
|
||||
}
|
||||
|
||||
if (this.property.type === 'phone_number' && !this.property.use_simple_text_input) {
|
||||
componentData.component = 'PhoneInput'
|
||||
}
|
||||
|
||||
if (['select', 'multi_select'].includes(this.property.type)) {
|
||||
componentData.multiple = false
|
||||
componentData.options = this.property[this.property.type].options.map(option => {
|
||||
return {
|
||||
name: option.name,
|
||||
value: option.name
|
||||
}
|
||||
})
|
||||
} else if (this.property.type === 'date') {
|
||||
// componentData.withTime = true
|
||||
} else if (this.property.type === 'checkbox') {
|
||||
componentData.label = this.property.name
|
||||
}
|
||||
|
||||
return componentData
|
||||
},
|
||||
operators () {
|
||||
return Object.keys(this.available_filters[this.property.type].comparators).map(key => {
|
||||
return {
|
||||
value: key,
|
||||
name: this.optionFilterNames(key, this.property.type)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (!this.content.operator) {
|
||||
this.content.operator = this.operators[0].value
|
||||
this.operatorChanged()
|
||||
} else {
|
||||
this.hasInput = this.needsInput()
|
||||
}
|
||||
|
||||
this.content.property_meta = {
|
||||
id: this.property.id,
|
||||
type: this.property.type
|
||||
}
|
||||
this.isMounted = true
|
||||
},
|
||||
|
||||
methods: {
|
||||
castContent (content) {
|
||||
if (this.property.type === 'number' && content.value) {
|
||||
content.value = Number(content.value)
|
||||
}
|
||||
|
||||
const operator = this.selectedOperator()
|
||||
if (operator.expected_type === 'boolean') {
|
||||
content.value = Boolean(content.value)
|
||||
}
|
||||
|
||||
return content
|
||||
},
|
||||
operatorChanged () {
|
||||
if (!this.content.operator) {
|
||||
return
|
||||
}
|
||||
|
||||
const operator = this.selectedOperator()
|
||||
const operatorFormat = operator.format
|
||||
this.hasInput = this.needsInput()
|
||||
|
||||
if (operator.expected_type === 'boolean' && operatorFormat.type === 'enum' && operatorFormat.values.length === 1) {
|
||||
this.content.value = operator.format.values[0]
|
||||
} else if (operator.expected_type === 'object' && operatorFormat.type === 'empty' && operatorFormat.values === '{}') {
|
||||
this.content.value = {}
|
||||
} else if (typeof this.content.value === 'boolean' || typeof this.content.value === 'object') {
|
||||
this.content.value = null
|
||||
}
|
||||
this.emitInput()
|
||||
},
|
||||
needsInput () {
|
||||
const operator = this.selectedOperator()
|
||||
if (!operator) {
|
||||
return false
|
||||
}
|
||||
const operatorFormat = operator.format
|
||||
if (!operatorFormat) return true
|
||||
|
||||
if (operator.expected_type === 'boolean' && operatorFormat.type === 'enum' && operatorFormat.values.length === 1) {
|
||||
return false
|
||||
} else if (operator.expected_type === 'object' && operatorFormat.type === 'empty' && operatorFormat.values === '{}') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
selectedOperator () {
|
||||
if (!this.content.operator) {
|
||||
return null
|
||||
}
|
||||
return this.available_filters[this.property.type].comparators[this.content.operator]
|
||||
},
|
||||
optionFilterNames (key, propertyType) {
|
||||
if (propertyType === 'checkbox') {
|
||||
return {
|
||||
equals: 'Is checked',
|
||||
does_not_equal: 'Is not checked'
|
||||
}[key]
|
||||
}
|
||||
return key.split('_').map(function (item) {
|
||||
return item.charAt(0).toUpperCase() + item.substring(1)
|
||||
}).join(' ')
|
||||
},
|
||||
emitInput () {
|
||||
this.$emit('update:modelValue', this.castContent(this.content))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<query-builder v-model="query" :rules="rules" :config="config" @update:modelValue="onChange">
|
||||
<template #groupOperator="props">
|
||||
<div class="query-builder-group-slot__group-selection flex items-center px-5 border-b py-1 mb-1 flex">
|
||||
<p class="mr-2 font-semibold">
|
||||
Operator
|
||||
</p>
|
||||
<select-input
|
||||
wrapper-class="relative"
|
||||
:model-value="props.currentOperator"
|
||||
:options="props.operators"
|
||||
emit-key="identifier"
|
||||
option-key="identifier"
|
||||
name="operator-input"
|
||||
margin-bottom=""
|
||||
@update:modelValue="props.updateCurrentOperator($event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #groupControl="props">
|
||||
<group-control-slot :group-ctrl="props" />
|
||||
</template>
|
||||
<template #rule="ruleCtrl">
|
||||
<component
|
||||
:is="ruleCtrl.ruleComponent"
|
||||
:model-value="ruleCtrl.ruleData"
|
||||
@update:modelValue="ruleCtrl.updateRuleData"
|
||||
/>
|
||||
</template>
|
||||
</query-builder>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import QueryBuilder from 'query-builder-vue-3'
|
||||
import ColumnCondition from './ColumnCondition.vue'
|
||||
import GroupControlSlot from './GroupControlSlot.vue'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
GroupControlSlot,
|
||||
QueryBuilder,
|
||||
ColumnCondition
|
||||
},
|
||||
|
||||
props: {
|
||||
form: { type: Object, required: true },
|
||||
value: { required: false }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
query: this.value
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
rules () {
|
||||
return this.form.properties.filter((property) => {
|
||||
return !property.type.startsWith('nf-')
|
||||
}).map((property) => {
|
||||
const workspaceId = this.form.workspace_id
|
||||
const formSlug = this.form.slug
|
||||
return {
|
||||
identifier: property.id,
|
||||
name: property.name,
|
||||
component: (function () {
|
||||
return defineComponent({
|
||||
extends: ColumnCondition,
|
||||
computed: {
|
||||
property () {
|
||||
return property
|
||||
},
|
||||
viewContext () {
|
||||
return {
|
||||
form_slug: formSlug,
|
||||
workspace_id: workspaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
config () {
|
||||
return {
|
||||
operators: [
|
||||
{
|
||||
name: 'And',
|
||||
identifier: 'and'
|
||||
},
|
||||
{
|
||||
name: 'Or',
|
||||
identifier: 'or'
|
||||
}
|
||||
],
|
||||
rules: this.rules,
|
||||
colors: ['#ef4444', '#22c55e', '#f97316', '#0ea5e9', '#8b5cf6', '#ec4899']
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
value () {
|
||||
this.query = this.value
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChange () {
|
||||
this.$emit('update:modelValue', this.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div v-if="logic" :key="resetKey">
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Logic
|
||||
</h3>
|
||||
<p class="text-gray-400 text-xs mb-3">
|
||||
Add some logic to this block. Start by adding some conditions, and then add some actions.
|
||||
</p>
|
||||
<div class="relative flex">
|
||||
<div>
|
||||
<v-button color="light-gray" size="small" @click="showCopyFormModal=true">
|
||||
<svg class="h-4 w-4 text-blue-600 inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5M11 9H20C21.1046 9 22 9.89543 22 11V20C22 21.1046 21.1046 22 20 22H11C9.89543 22 9 21.1046 9 20V11C9 9.89543 9.89543 9 11 9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Copy from
|
||||
</v-button>
|
||||
</div>
|
||||
<div>
|
||||
<v-button color="light-gray" shade="light" size="small" class="ml-1" @click="clearAll">
|
||||
<svg class="text-red-600 h-4 w-4 inline -mt-1 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 9L12 15M12 9L18 15M21 4H8L1 12L8 20H21C21.5304 20 22.0391 19.7893 22.4142 19.4142C22.7893 19.0391 23 18.5304 23 18V6C23 5.46957 22.7893 4.96086 22.4142 4.58579C22.0391 4.21071 21.5304 4 21 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
Clear All
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="font-semibold mt-3">
|
||||
1. Conditions
|
||||
</h5>
|
||||
<condition-editor ref="filter-editor" v-model="logic.conditions" class="mt-1 border-t border rounded-md" :form="form" />
|
||||
|
||||
<h5 class="font-semibold mt-3">
|
||||
2. Actions
|
||||
</h5>
|
||||
<select-input :key="resetKey" v-model="logic.actions" name="actions"
|
||||
:multiple="true" class="mt-1" placeholder="Actions..."
|
||||
help="Action(s) triggerred when above conditions are true"
|
||||
:options="actionOptions"
|
||||
@update:model-value="onActionInput"
|
||||
/>
|
||||
|
||||
<modal :show="showCopyFormModal" @close="showCopyFormModal = false">
|
||||
<div class="min-h-[450px]">
|
||||
<h3 class="font-semibold block text-lg">
|
||||
Copy logic from another field
|
||||
</h3>
|
||||
<p class="text-gray-400 text-xs mb-5">
|
||||
Select another field/block to copy its logic and apply it to "{{ field.name }}".
|
||||
</p>
|
||||
<select-input v-model="copyFrom" name="copy_from" emit-key="value"
|
||||
label="Copy logic from" placeholder="Choose a field/block..."
|
||||
:options="copyFromOptions" :searchable="copyFromOptions && copyFromOptions.options > 5"
|
||||
/>
|
||||
<div class="flex justify-between mb-6">
|
||||
<v-button color="blue" shade="light" @click="copyLogic">
|
||||
Confirm & Copy
|
||||
</v-button>
|
||||
<v-button color="gray" shade="light" class="ml-1" @click="showCopyFormModal=false">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConditionEditor from './ConditionEditor.vue'
|
||||
import Modal from '../../../../global/Modal.vue'
|
||||
import SelectInput from '../../../../forms/SelectInput.vue'
|
||||
import clonedeep from 'clone-deep'
|
||||
|
||||
export default {
|
||||
name: 'FormBlockLogicEditor',
|
||||
components: { SelectInput, Modal, ConditionEditor },
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
resetKey: 0,
|
||||
logic: this.field.logic || {
|
||||
conditions: null,
|
||||
actions: []
|
||||
},
|
||||
showCopyFormModal: false,
|
||||
copyFrom: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
copyFromOptions () {
|
||||
return this.form.properties.filter((field) => {
|
||||
return field.id !== this.field.id && field.hasOwnProperty('logic') && field.logic !== null && field.logic !== {}
|
||||
}).map((field) => {
|
||||
return { name: field.name, value: field.id }
|
||||
})
|
||||
},
|
||||
actionOptions () {
|
||||
if (['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image'].includes(this.field.type)) {
|
||||
if (this.field.hidden) {
|
||||
return [{ name: 'Show Block', value: 'show-block' }]
|
||||
} else {
|
||||
return [{ name: 'Hide Block', value: 'hide-block' }]
|
||||
}
|
||||
}
|
||||
|
||||
if (this.field.hidden) {
|
||||
return [
|
||||
{ name: 'Show Block', value: 'show-block' },
|
||||
{ name: 'Require answer', value: 'require-answer' }
|
||||
]
|
||||
} else if (this.field.disabled) {
|
||||
return [
|
||||
{ name: 'Enable Block', value: 'enable-block' },
|
||||
(this.field.required
|
||||
? { name: 'Make it optional', value: 'make-it-optional' }
|
||||
: {
|
||||
name: 'Require answer',
|
||||
value: 'require-answer'
|
||||
})
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ name: 'Hide Block', value: 'hide-block' },
|
||||
{ name: 'Disable Block', value: 'disable-block' },
|
||||
(this.field.required
|
||||
? { name: 'Make it optional', value: 'make-it-optional' }
|
||||
: {
|
||||
name: 'Require answer',
|
||||
value: 'require-answer'
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
logic: {
|
||||
handler () {
|
||||
this.field.logic = this.logic
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'field.id': {
|
||||
handler (field, oldField) {
|
||||
// On field change, reset logic
|
||||
this.logic = this.field.logic || {
|
||||
conditions: null,
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
},
|
||||
'field.required': 'cleanConditions',
|
||||
'field.disabled': 'cleanConditions',
|
||||
'field.hidden': 'cleanConditions'
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (!this.field.hasOwnProperty('logic')) {
|
||||
this.field.logic = this.logic
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clearAll () {
|
||||
this.logic.conditions = null
|
||||
this.logic.actions = []
|
||||
this.refreshActions()
|
||||
},
|
||||
onActionInput () {
|
||||
if (this.logic.actions.length >= 2) {
|
||||
if (this.logic.actions[1] === 'require-answer' && this.logic.actions[0] === 'hide-block') {
|
||||
this.logic.actions = ['require-answer']
|
||||
} else if (this.logic.actions[1] === 'hide-block' && this.logic.actions[0] === 'require-answer') {
|
||||
this.logic.actions = ['hide-block']
|
||||
}
|
||||
this.refreshActions()
|
||||
}
|
||||
},
|
||||
cleanConditions () {
|
||||
const availableActions = this.actionOptions.map(function (op) { return op.value })
|
||||
this.logic.actions = availableActions.filter(value => this.logic.actions.includes(value))
|
||||
this.refreshActions()
|
||||
},
|
||||
refreshActions () {
|
||||
this.resetKey++
|
||||
},
|
||||
copyLogic () {
|
||||
if (this.copyFrom) {
|
||||
const property = this.form.properties.find((property) => {
|
||||
return property.id === this.copyFrom
|
||||
})
|
||||
if (property && property.logic) {
|
||||
this.logic = clonedeep(property.logic)
|
||||
this.cleanConditions()
|
||||
}
|
||||
}
|
||||
this.showCopyFormModal = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap px-4 py-1 -ml-1 -mt-1">
|
||||
<select-input ref="ruleSelect" v-model="selectedRule" class="flex-grow ml-1 mr-1 mt-1"
|
||||
wrapper-class="relative" placeholder="Add condition on input field"
|
||||
:options="groupCtrl.rules" margin-bottom="" :searchable="groupCtrl.rules.length > 5"
|
||||
emit-key="identifier"
|
||||
option-key="identifier"
|
||||
name="group-control-slot-rule"
|
||||
/>
|
||||
<v-button class="ml-1 mt-1" color="blue" size="small" :disabled="(selectedRule === '')?true:null" @click="addRule">
|
||||
Add Condition
|
||||
</v-button>
|
||||
<v-button class="ml-1 mt-1" color="outline-blue" size="small" @click="groupCtrl.newGroup">
|
||||
Add Group
|
||||
</v-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: { groupCtrl: { type: Object, required: true } },
|
||||
data () {
|
||||
return {
|
||||
selectedRule: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRule () {
|
||||
if (this.selectedRule) {
|
||||
this.groupCtrl.addRule(this.selectedRule)
|
||||
this.$refs.ruleSelect.content = null
|
||||
this.selectedRule = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user