Form editor v2 (#579)

* Form editor v2

* fix template test

* setFormDefaults when save

* fix form cleaning dark mode

* improvements on open sidebar

* UI polish

* Fix change type button

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2024-09-23 23:32:38 +05:30
committed by GitHub
parent 47ae11bc58
commit d6181cd249
61 changed files with 2576 additions and 2661 deletions

View File

@@ -1,25 +1,15 @@
<template>
<div>
<div class="p-4 border-b sticky top-0 z-10 bg-white">
<div class="px-4 py-2 border-b sticky top-0 z-10 bg-white">
<button
v-if="!field"
class="text-gray-500 hover:text-gray-900 cursor-pointer"
@click.prevent="closeSidebar"
>
<svg
<Icon
name="heroicons:x-mark-solid"
class="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6L6 18M6 6L18 18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
/>
</button>
<template v-else>
<div class="flex">
@@ -27,216 +17,201 @@
class="text-gray-500 hover:text-gray-900 cursor-pointer"
@click.prevent="closeSidebar"
>
<svg
<Icon
name="heroicons:x-mark-solid"
class="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6L6 18M6 6L18 18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
/>
</button>
<div class="font-semibold inline ml-2 truncate flex-grow truncate">
Configure "<span class="truncate">{{ field.name }}</span>"
<div class="ml-2 flex flex-grow items-center space-between min-w-0 gap-x-3">
<div class="flex-grow" />
<BlockTypeIcon
:type="field.type"
/>
<p class="text-sm text-gray-500">
{{ blocksTypes[field.type].title }}
</p>
<UDropdown
:items="dropdownItems"
:popper="{ placement: 'bottom-start' }"
>
<UButton
color="white"
icon="i-heroicons-ellipsis-vertical"
/>
<template
v-if="typeCanBeChanged"
#changetype
>
<ChangeFieldType
v-if="!isBlockField"
:field="field"
@change-type="onChangeType"
/>
</template>
</UDropdown>
</div>
</div>
<div class="flex mt-2">
<v-button
color="light-gray"
class="border-r-0 rounded-r-none text-xs hover:bg-red-50"
size="small"
@click="removeBlock"
>
<svg
class="h-4 w-4 text-red-600 inline mr-1 -mt-1"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 6H5M5 6H21M5 6V20C5 20.5304 5.21071 21.0391 5.58579 21.4142C5.96086 21.7893 6.46957 22 7 22H17C17.5304 22 18.0391 21.7893 18.4142 21.4142C18.7893 21.0391 19 20.5304 19 20V6H5ZM8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M10 11V17M14 11V17"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Remove
</v-button>
<v-button
size="small"
class="text-xs"
:class="{
'rounded-none border-r-0': !isBlockField && typeCanBeChanged,
'rounded-l-none': isBlockField || !typeCanBeChanged,
}"
color="light-gray"
@click="duplicateBlock"
>
<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>
Duplicate
</v-button>
<change-field-type
v-if="!isBlockField"
btn-classes="rounded-l-none text-xs"
:field="field"
@change-type="onChangeType"
/>
</div>
</template>
</div>
<template v-if="field">
<field-options
v-if="!isBlockField"
:form="form"
:field="field"
/>
<block-options
v-if="isBlockField"
:form="form"
:field="field"
/>
<div class="bg-gray-100 border-b">
<UTabs
v-model="activeTab"
:items="tabItems"
class="w-full"
/>
</div>
<div v-if="activeTab ===0">
<FieldOptions
v-if="!isBlockField"
:form="form"
:field="field"
/>
<BlockOptions
v-else
:form="form"
:field="field"
/>
</div>
<div v-else-if="activeTab === 1">
<FormBlockLogicEditor
class="py-2 px-4"
:form="form"
:field="field"
/>
</div>
<div v-else-if="activeTab === 2">
<custom-field-validation
class="py-2 px-4"
:form="form"
:field="field"
/>
</div>
</template>
<div
v-else
class="text-center p-10"
class="text-center p-10 text-sm text-gray-500"
>
Click on field's setting icon in your form to modify it
Click on field to edit it.
</div>
</div>
</template>
<script>
import { computed } from "vue"
import clonedeep from "clone-deep"
import { useWorkingFormStore } from "../../../../stores/working_form"
<script setup>
import { storeToRefs } from 'pinia'
import clonedeep from 'clone-deep'
import FieldOptions from './components/FieldOptions.vue'
import BlockOptions from './components/BlockOptions.vue'
import BlockTypeIcon from '../components/BlockTypeIcon.vue'
import ChangeFieldType from "./components/ChangeFieldType.vue"
import FieldOptions from "./components/FieldOptions.vue"
import BlockOptions from "./components/BlockOptions.vue"
import blocksTypes from '~/data/blocks_types.json'
import FormBlockLogicEditor from '../components/form-logic-components/FormBlockLogicEditor.vue'
import CustomFieldValidation from '../components/CustomFieldValidation.vue'
import { generateUUID } from '~/lib/utils'
export default {
name: "FormFieldEdit",
components: { ChangeFieldType, FieldOptions, BlockOptions },
props: {},
setup() {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
}
},
data() {
return {}
},
const workingFormStore = useWorkingFormStore()
const { content: form } = storeToRefs(workingFormStore)
computed: {
form: {
get() {
return this.workingFormStore.content
},
/* We add a setter */
set(value) {
this.workingFormStore.set(value)
},
},
field() {
return this.form && this.selectedFieldIndex !== null
? this.form.properties[this.selectedFieldIndex]
: null
},
isBlockField() {
return this.field && this.field.type.startsWith("nf")
},
typeCanBeChanged() {
return [
"text",
"email",
"phone_number",
"number",
"select",
"multi_select",
"rating",
"scale",
"slider",
].includes(this.field.type)
},
},
const selectedFieldIndex = computed(() => workingFormStore.selectedFieldIndex)
watch: {},
const field = computed(() => {
return form.value && selectedFieldIndex.value !== null
? form.value.properties[selectedFieldIndex.value]
: null
})
created() {},
const isBlockField = computed(() => {
return field.value && field.value.type.startsWith('nf')
})
mounted() {},
const typeCanBeChanged = computed(() => {
return [
"text",
"email",
"phone_number",
"number",
"select",
"multi_select",
"rating",
"scale",
"slider",
].includes(field.value.type)
})
methods: {
onChangeType(newType) {
if (["select", "multi_select"].includes(this.field.type)) {
this.field[newType] = this.field[this.field.type] // Set new options with new type
delete this.field[this.field.type] // remove old type options
}
this.field.type = newType
},
removeBlock() {
const newFields = clonedeep(this.form.properties)
newFields.splice(this.selectedFieldIndex, 1)
this.form.properties = newFields
this.closeSidebar()
},
duplicateBlock() {
const newFields = clonedeep(this.form.properties)
const newField = clonedeep(this.form.properties[this.selectedFieldIndex])
newField.id = this.generateUUID()
newFields.push(newField)
this.form.properties = newFields
this.closeSidebar()
},
closeSidebar() {
this.workingFormStore.closeEditFieldSidebar()
},
generateUUID() {
let d = new Date().getTime() // Timestamp
let d2 =
(typeof performance !== "undefined" &&
performance.now &&
performance.now() * 1000) ||
0 // Time in microseconds since page-load or 0 if unsupported
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
let r = Math.random() * 16 // random number between 0 and 16
if (d > 0) {
// Use timestamp until depleted
r = (d + r) % 16 | 0
d = Math.floor(d / 16)
} else {
// Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0
d2 = Math.floor(d2 / 16)
}
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16)
},
)
},
},
function removeBlock() {
workingFormStore.removeField(field.value)
}
function closeSidebar() {
workingFormStore.closeEditFieldSidebar()
}
function onChangeType(newType) {
if (["select", "multi_select"].includes(field.value.type)) {
field.value[newType] = field.value[field.value.type] // Set new options with new type
delete field.value[field.value.type] // remove old type options
}
field.value.type = newType
}
const dropdownItems = computed(() => {
return [
[{
label: 'Copy field ID',
icon: 'i-heroicons-clipboard-20-solid',
click: () => {
navigator.clipboard.writeText(field.value.id)
useAlert().success('Field ID copied to clipboard')
}
}],
[{
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid',
click: () => {
const newField = clonedeep(field.value)
newField.id = generateUUID()
newField.name = 'Copy of ' + newField.name
const newFields = [...form.value.properties]
newFields.splice(selectedFieldIndex.value + 1, 0, newField)
form.value.properties = newFields
}
}],
... (typeCanBeChanged.value ? [[{
label: 'Change type',
icon: 'i-heroicons-document-duplicate-20-solid',
slot: 'changetype',
}]] : []),
[{
label: 'Remove',
icon: 'i-heroicons-trash-20-solid',
class: 'group/remove hover:text-red-800',
iconClass: 'group-hover/remove:text-red-900',
click: removeBlock
}]
]
})
const activeTab = ref(0)
const tabItems = computed(() => {
const commonTabs = [
{ label: 'Options'},
{ label: 'Logic' },
]
if (isBlockField.value) {
return commonTabs
} else {
return [
...commonTabs,
{ label: 'Validation'},
]
}
})
</script>

View File

@@ -1,53 +1,62 @@
<template>
<div v-if="field">
<!-- General -->
<div class="border-b py-2 px-4">
<h3 class="font-semibold block text-lg">
General
</h3>
<p class="text-gray-400 mb-3 text-xs">
Exclude this field or make it required.
</p>
<toggle-switch-input
<div
v-if="field"
class="py-2"
>
<div class="px-4">
<text-input
name="name"
:form="field"
name="hidden"
label="Hidden"
@update:model-value="onFieldHiddenChange"
wrapper-class="mb-2"
:required="true"
label="Block Name"
/>
<select-input
name="width"
class="mt-3"
:options="[
{ name: 'Full', value: 'full' },
{ name: '1/2 (half width)', value: '1/2' },
{ name: '1/3 (a third of the width)', value: '1/3' },
{ name: '2/3 (two thirds of the width)', value: '2/3' },
{ name: '1/4 (a quarter of the width)', value: '1/4' },
{ name: '3/4 (three quarters of the width)', value: '3/4' },
]"
<HiddenRequiredDisabled
:form="field"
label="Field Width"
/>
<select-input
v-if="['nf-text', 'nf-image'].includes(field.type)"
name="align"
class="mt-3"
:options="[
{ name: 'Left', value: 'left' },
{ name: 'Center', value: 'center' },
{ name: 'Right', value: 'right' },
{ name: 'Justify', value: 'justify' },
]"
:form="field"
label="Field Alignment"
:field="field"
:can-be-disabled="false"
:can-be-hidden="true"
:can-be-required="false"
/>
<div class="grid grid-cols-2 gap-2 mt-2">
<select-input
name="width"
class="flex-grow"
:options="[
{ name: 'Full', value: 'full' },
{ name: '1/2', value: '1/2' },
{ name: '1/3', value: '1/3' },
{ name: '2/3', value: '2/3' },
{ name: '1/4', value: '1/4' },
{ name: '3/4', value: '3/4' },
]"
:form="field"
label="Width"
/>
<select-input
v-if="['nf-text', 'nf-image'].includes(field.type)"
name="align"
class="flex-grow"
:options="[
{ name: 'Left', value: 'left' },
{ name: 'Center', value: 'center' },
{ name: 'Right', value: 'right' },
{ name: 'Justify', value: 'justify' },
]"
:form="field"
label="Alignment"
/>
</div>
</div>
<div
v-if="field.type == 'nf-text'"
class="border-b py-2 px-4"
class="border-t py-2"
>
<rich-text-area-input
class="mx-4"
name="content"
:form="field"
label="Content"
@@ -62,43 +71,25 @@
<text-input
name="next_btn_text"
:form="field"
label="Text of next button"
label="Next button label"
:required="true"
/>
<text-input
name="previous_btn_text"
:form="field"
label="Text of previous button"
help="Shown on the next page"
label="Previous button label"
help="Displayed on the next page"
:required="true"
/>
</div>
<div
v-else-if="field.type == 'nf-divider'"
class="border-b py-2 px-4"
>
<text-input
name="name"
:form="field"
:required="true"
label="Field Name"
/>
</div>
<div
v-else-if="field.type == 'nf-image'"
class="border-b py-2 px-4"
class="border-t py-2"
>
<text-input
name="name"
:form="field"
:required="true"
label="Field Name"
/>
<image-input
name="image_block"
class="mt-3"
class="mx-4"
:form="field"
label="Upload Image"
:required="false"
@@ -107,95 +98,48 @@
<div
v-else-if="field.type == 'nf-code'"
class="border-b py-2 px-4"
class="border-t"
>
<code-input
name="content"
class="mt-4 mx-4"
:form="field"
label="Content"
help="You can add any html code, including iframes"
/>
</div>
<div
v-else
class="border-b py-2 px-4"
>
<p>No settings found.</p>
</div>
<!-- Logic Block -->
<form-block-logic-editor
class="py-2 px-4 border-b"
:form="form"
:field="field"
/>
</div>
</template>
<script>
import FormBlockLogicEditor from "../../components/form-logic-components/FormBlockLogicEditor.vue"
<script setup>
import HiddenRequiredDisabled from './HiddenRequiredDisabled.vue'
export default {
name: "BlockOptions",
components: { FormBlockLogicEditor },
props: {
field: {
type: Object,
required: false,
},
form: {
type: Object,
required: false,
},
},
data() {
return {
editorToolbarCustom: [["bold", "italic", "underline", "link"]],
}
const props = defineProps({
field: {
type: Object,
required: false
},
form: {
type: Object,
required: false
}
})
computed: {},
watch(() => props.field?.width, (val) => {
if (val === undefined || val === null) {
props.field.width = 'full'
}
}, { immediate: true })
watch: {
"field.width": {
handler(val) {
if (val === undefined || val === null) {
this.field.width = "full"
}
},
immediate: true,
},
"field.align": {
handler(val) {
if (val === undefined || val === null) {
this.field.align = "left"
}
},
immediate: true,
},
},
watch(() => props.field?.align, (val) => {
if (val === undefined || val === null) {
props.field.align = 'left'
}
}, { immediate: true })
created() {
if (this.field?.width === undefined || this.field?.width === null) {
this.field.width = "full"
}
},
mounted() {},
methods: {
onFieldHiddenChange(val) {
this.field.hidden = val
if (this.field.hidden) {
this.field.required = false
}
},
onFieldHelpPositionChange(val) {
if (!val) {
this.field.help_position = "below_input"
}
},
},
}
onMounted(() => {
if (props.field?.width === undefined || props.field?.width === null) {
props.field.width = 'full'
}
})
</script>

View File

@@ -1,66 +1,46 @@
<template>
<dropdown
<UPopover
v-if="changeTypeOptions.length > 0"
ref="newTypeDropdown"
dusk="nav-dropdown"
v-model:open="open"
class="-mb-1"
>
<template #trigger="{ toggle }">
<v-button
class="relative"
:class="btnClasses"
size="small"
color="light-gray"
@click.stop="toggle"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-4 w-4 text-blue-600 inline mr-1 -mt-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
/>
</svg>
<span class="whitespace-nowrap">Change Type</span>
</v-button>
</template>
<div class="flex items-center gap-1.5 group">
<Icon
name="heroicons:arrows-right-left-20-solid"
class="flex-shrink-0 w-5 h-5 text-gray-400 group-hover:text-gray-500"
/>
<span class="truncate">Change Type</span>
</div>
<a
v-for="(op, index) in changeTypeOptions"
:key="index"
href="#"
class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="changeType(op.value)"
>
{{ op.name }}
</a>
</dropdown>
<template #panel>
<a
v-for="(op, index) in changeTypeOptions"
:key="index"
href="#"
class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="changeType(op.value)"
>
{{ op.name }}
</a>
</template>
</UPopover>
</template>
<script>
import Dropdown from "~/components/global/Dropdown.vue"
export default {
name: "ChangeFieldType",
components: { Dropdown },
components: {},
props: {
field: {
type: Object,
required: true,
},
btnClasses: {
type: String,
required: true,
},
},
emits: ['changeType'],
data() {
return {}
return {
open: false,
}
},
computed: {
@@ -114,7 +94,7 @@ export default {
changeType(newType) {
if (newType) {
this.$emit("changeType", newType)
this.$refs.newTypeDropdown.close()
this.open = false
}
},
},

View File

@@ -1,47 +1,33 @@
<template>
<div
v-if="field"
class="py-2"
class="pb-2"
>
<!-- General -->
<div class="border-b px-4">
<h3 class="font-semibold block text-lg">
General
</h3>
<p class="text-gray-400 mb-2 text-xs">
Exclude this field or make it required.
</p>
<toggle-switch-input
<div class="px-4">
<text-input
name="name"
class="mt-2"
:form="field"
name="required"
label="Required"
@update:model-value="onFieldRequiredChange"
:required="true"
wrapper-class="mb-2"
label="Field Name"
/>
<toggle-switch-input
:form="field"
name="hidden"
label="Hidden"
@update:model-value="onFieldHiddenChange"
/>
<toggle-switch-input
:form="field"
name="disabled"
label="Disabled"
@update:model-value="onFieldDisabledChange"
<HiddenRequiredDisabled
class="mt-4"
:field="field"
/>
</div>
<!-- Checkbox -->
<div
v-if="field.type === 'checkbox'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Checkbox
</h3>
<p class="text-gray-400 mb-3 text-xs">
Advanced options for checkbox.
</p>
<EditorSectionHeader
icon="i-heroicons-check-circle"
title="Checkbox"
/>
<toggle-switch-input
:form="field"
name="use_toggle_switch"
@@ -53,11 +39,12 @@
<!-- File Uploads -->
<div
v-if="field.type === 'files'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg mb-3">
File uploads
</h3>
<EditorSectionHeader
icon="i-heroicons-paper-clip"
title="File uploads"
/>
<toggle-switch-input
:form="field"
name="multiple"
@@ -92,14 +79,12 @@
<div
v-if="field.type === 'rating'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Rating
</h3>
<p class="text-gray-400 mb-3 text-xs">
Advanced options for rating.
</p>
<EditorSectionHeader
icon="i-heroicons-star"
title="Rating"
/>
<text-input
name="rating_max_value"
native-type="number"
@@ -113,14 +98,12 @@
<div
v-if="field.type === 'scale'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Scale
</h3>
<p class="text-gray-400 mb-3 text-xs">
Advanced options for scale.
</p>
<EditorSectionHeader
icon="i-heroicons-scale-20-solid"
title="Scale"
/>
<text-input
name="scale_min_value"
native-type="number"
@@ -151,14 +134,12 @@
<div
v-if="field.type === 'slider'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Slider
</h3>
<p class="text-gray-400 mb-3 text-xs">
Advanced options for slider.
</p>
<EditorSectionHeader
icon="i-heroicons-adjustments-horizontal"
title="Slider"
/>
<text-input
name="slider_min_value"
native-type="number"
@@ -195,14 +176,12 @@
<!-- Text Options -->
<div
v-if="field.type === 'text' && displayBasedOnAdvanced"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Text Options
</h3>
<p class="text-gray-400 mb-3 text-xs">
Keep it simple or make it a multi-lines input.
</p>
<EditorSectionHeader
icon="i-heroicons-bars-3-bottom-left"
title="Text Options"
/>
<toggle-switch-input
:form="field"
name="multi_lines"
@@ -221,11 +200,12 @@
<!-- Date Options -->
<div
v-if="field.type === 'date'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Date Options
</h3>
<EditorSectionHeader
icon="i-heroicons-calendar-20-solid"
title="Date Options"
/>
<toggle-switch-input
:form="field"
class="mt-3"
@@ -286,14 +266,12 @@
<!-- select/multiselect Options -->
<div
v-if="['select', 'multi_select'].includes(field.type)"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Select Options
</h3>
<p class="text-gray-400 mb-3 text-xs">
Advanced options for your select/multiselect fields.
</p>
<EditorSectionHeader
icon="i-heroicons-chevron-up-down-20-solid"
title="Select Options"
/>
<text-area-input
v-model="optionsText"
:name="field.id + '_options_text'"
@@ -320,22 +298,11 @@
<!-- Customization - Placeholder, Prefill, Relabel, Field Help -->
<div
v-if="displayBasedOnAdvanced"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Customization
</h3>
<p class="text-gray-400 mb-3 text-xs">
Change your form field name, pre-fill a value, add hints, etc.
</p>
<text-input
name="name"
class="mt-3"
:form="field"
:required="true"
label="Field Name"
<EditorSectionHeader
icon="i-heroicons-adjustments-horizontal"
title="Customization"
/>
<toggle-switch-input
@@ -568,7 +535,7 @@
help="Maximum character limit of 2000"
:required="false"
/>
<checkbox-input
<toggle-switch-input
name="show_char_limit"
:form="field"
class="mt-3"
@@ -580,11 +547,13 @@
<!-- Advanced Options -->
<div
v-if="field.type === 'text'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg mb-3">
Advanced Options
</h3>
<EditorSectionHeader
icon="i-heroicons-bars-3-bottom-left"
title="Advanced Options"
/>
<toggle-switch-input
:form="field"
name="generates_uuid"
@@ -600,19 +569,6 @@
@update:model-value="onFieldGenAutoIdChange"
/>
</div>
<!-- Logic Block -->
<form-block-logic-editor
class="py-2 px-4 border-b"
:form="form"
:field="field"
/>
<custom-field-validation
class="py-2 px-4 border-b pb-16"
:form="form"
:field="field"
/>
</div>
</template>
@@ -620,15 +576,15 @@
import timezones from '~/data/timezones.json'
import countryCodes from '~/data/country_codes.json'
import CountryFlag from 'vue-country-flag-next'
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
import CustomFieldValidation from '../../components/CustomFieldValidation.vue'
import MatrixFieldOptions from './MatrixFieldOptions.vue'
import HiddenRequiredDisabled from './HiddenRequiredDisabled.vue'
import EditorSectionHeader from '~/components/open/forms/components/form-components/EditorSectionHeader.vue'
import { format } from 'date-fns'
import { default as _has } from 'lodash/has'
export default {
name: 'FieldOptions',
components: { CountryFlag, FormBlockLogicEditor, CustomFieldValidation, MatrixFieldOptions },
components: { CountryFlag, MatrixFieldOptions, HiddenRequiredDisabled, EditorSectionHeader },
props: {
field: {
type: Object,
@@ -738,28 +694,6 @@ export default {
},
methods: {
onFieldDisabledChange(val) {
this.field.disabled = val
if (this.field.disabled) {
this.field.hidden = false
}
},
onFieldRequiredChange(val) {
this.field.required = val
if (this.field.required) {
this.field.hidden = false
}
},
onFieldHiddenChange(val) {
this.field.hidden = val
if (this.field.hidden) {
this.field.required = false
this.field.disabled = false
} else {
this.field.generates_uuid = false
this.field.generates_auto_increment_id = false
}
},
onFieldDateRangeChange(val) {
this.field.date_range = val
if (this.field.date_range) {

View File

@@ -0,0 +1,116 @@
<template>
<div class="grid grid-cols-3 gap-2">
<button
v-for="option in availableOptions"
:key="option.name"
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500"
:class="[
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {},
{
'border-blue-500': isSelected(option.name),
'hover:bg-gray-100 border-gray-300': !isSelected(option.name)
}
]"
@click="toggleOption(option.name)"
>
<Icon
:name="isSelected(option.name) && option.selectedIcon ? option.selectedIcon : option.icon"
:class="[
'w-4 h-4 mb-1',
{
'text-blue-500': isSelected(option.name),
'text-inherit': !isSelected(option.name),
},
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option.name)) : option.iconClass) : {}
]"
/>
<span
class="text-xs"
:class="{
'text-blue-500': isSelected(option.name),
'text-inherit': !isSelected(option.name),
}"
>{{ isSelected(option.name) ? option.selectedLabel ?? option.label : option.label }}</span>
</button>
</div>
</template>
<script setup>
const props = defineProps({
field: {
type: Object,
required: true
},
canBeDisabled: {
type: Boolean,
default: true
},
canBeRequired: {
type: Boolean,
default: true
},
canBeHidden: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:field'])
const options = ref([
{
name: 'required',
label: 'Required',
icon: 'ph:asterisk-bold',
selectedIcon: 'ph:asterisk-bold',
iconClass: (isActive) => isActive ? 'text-red-500' : '',
},
{
name: 'hidden',
label: 'Hidden',
icon: 'heroicons:eye',
selectedIcon: 'heroicons:eye-slash-solid',
},
{
name: 'disabled',
label: 'Disabled',
icon: 'heroicons:lock-open',
selectedIcon: 'heroicons:lock-closed-solid',
}
])
const availableOptions = computed(() => {
return options.value.filter(option => {
if (option.name === 'disabled') return props.canBeDisabled
if (option.name === 'required') return props.canBeRequired
if (option.name === 'hidden') return props.canBeHidden
return true
})
})
const isSelected = (optionName) => {
return props.field[optionName]
}
const toggleOption = (optionName) => {
const newValue = !props.field[optionName]
if (optionName === 'required' && newValue) {
props.field.hidden = false
} else if (optionName === 'hidden' && newValue) {
props.field.required = false
props.field.disabled = false
props.field.generates_uuid = false
props.field.generates_auto_increment_id = false
} else if (optionName === 'disabled' && newValue) {
props.field.hidden = false
}
if ((optionName === 'disabled' && props.canBeDisabled) ||
(optionName === 'required' && props.canBeRequired) ||
(optionName === 'hidden' && props.canBeHidden)) {
props.field[optionName] = newValue
}
}
</script>

View File

@@ -1,14 +1,12 @@
<template>
<div
v-if="localField.type === 'matrix'"
class="border-b py-2 px-4"
class="px-4"
>
<h3 class="font-semibold block text-lg">
Matrix
</h3>
<p class="text-gray-400 mb-3 text-xs">
Advanced options for matrix.
</p>
<EditorSectionHeader
icon="i-heroicons-table-cells-20-solid"
title="Matrix"
/>
<div class="grid grid-cols-2 gap-4">
<div class="">
@@ -72,6 +70,7 @@
<script setup>
import { ref, watch, computed } from 'vue'
import EditorSectionHeader from '~/components/open/forms/components/form-components/EditorSectionHeader.vue'
const props = defineProps({
modelValue: {