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,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: {