Work in progress

This commit is contained in:
Julien Nahum
2023-12-09 15:47:03 +01:00
parent f970557b76
commit 1f853e8178
315 changed files with 34058 additions and 25 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>