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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user