opnform-host-nginx/client/components/open/forms/OpenFormField.vue

438 lines
15 KiB
Vue
Raw Normal View History

2023-12-09 15:47:03 +01:00
<template>
<div
v-if="!isFieldHidden"
:id="'block-' + field.id"
ref="form-block"
2024-05-22 14:41:48 +02:00
class="px-2"
:class="[
getFieldWidthClasses(field),
{
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md': adminPreview,
'cursor-pointer':workingFormStore.showEditFieldSidebar && adminPreview,
'bg-blue-50 hover:!bg-blue-50 dark:bg-gray-800 rounded-md': beingEdited,
}]"
@click="setFieldAsSelected"
>
<div
class="-m-[1px] w-full max-w-full mx-auto"
:class="{'relative transition-colors':adminPreview}"
>
<div
v-if="adminPreview"
class="absolute translate-y-full lg:translate-y-0 -bottom-1 left-1/2 -translate-x-1/2 lg:-translate-x-full lg:-left-1 lg:top-1 lg:bottom-0 hidden group-hover/nffield:block z-50"
>
<div
class="flex lg:flex-col bg-white !bg-white dark:!bg-white border rounded-md shadow-sm z-50 p-[1px] relative"
>
<div
class="p-1 hover:!text-blue-500 dark:hover:!text-blue-500 hover:bg-blue-50 cursor-pointer !text-gray-500 dark:!text-gray-500 flex items-center justify-center rounded-md"
role="button"
@click.prevent="openAddFieldSidebar"
>
Add regex matching filters to open_filters.json (#722) * Add regex matching filters to open_filters.json - Introduce new regex-based filters: matches_regex and does_not_match_regex - Add support for string-based regex matching in both API and client filter configurations - Update filter schemas to include new regex filter options with expected string type * Add barcode comparators and regex filters to open_filters.json and validatePropertiesLogic.js - Introduce new barcode comparators: equals, does_not_equal, contains, does_not_contain, starts_with, ends_with, is_empty, is_not_empty, matches_regex, does_not_match_regex, content_length_equals, content_length_does_not_equal, content_length_greater_than, content_length_greater_than_or_equal_to, content_length_less_than, content_length_less_than_or_equal_to. - Add regex matching filters: matches_regex and does_not_match_regex to open_filters.json. - Remove console log statements from validatePropertiesLogic.js and FormPropertyLogicRule.js for cleaner code. This update enhances the filtering capabilities and improves the validation logic by removing unnecessary debug outputs. * Refactor Vue Components for Improved Readability and Consistency - Update CameraUpload.vue to enhance formatting and maintain consistent indentation for better readability. - Modify OpenFormField.vue to improve tooltip component formatting, ensuring consistent styling across the application. - Refactor FormSecurityAccess.vue to enhance the layout of the conditional rendering for captcha providers. - Adjust RegisterForm.vue to improve the formatting of the terms and conditions section, ensuring consistent indentation and readability. These changes aim to enhance code maintainability and visual consistency across the components, contributing to a cleaner codebase. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
2025-03-15 06:55:42 +01:00
<UTooltip
text="Add new field"
:popper="{ placement: 'right' }"
class="z-[100]"
>
<Icon
name="i-heroicons-plus-circle-20-solid"
class="w-5 h-5 !text-gray-500 dark:!text-gray-500 hover:!text-blue-500 dark:hover:!text-blue-500"
/>
</UTooltip>
2023-12-09 15:47:03 +01:00
</div>
<div
class="p-1 hover:!text-blue-500 dark:hover:!text-blue-500 hover:bg-blue-50 cursor-pointer flex items-center justify-center text-center !text-gray-500 dark:!text-gray-500 rounded-md"
role="button"
@click.prevent="editFieldOptions"
>
Add regex matching filters to open_filters.json (#722) * Add regex matching filters to open_filters.json - Introduce new regex-based filters: matches_regex and does_not_match_regex - Add support for string-based regex matching in both API and client filter configurations - Update filter schemas to include new regex filter options with expected string type * Add barcode comparators and regex filters to open_filters.json and validatePropertiesLogic.js - Introduce new barcode comparators: equals, does_not_equal, contains, does_not_contain, starts_with, ends_with, is_empty, is_not_empty, matches_regex, does_not_match_regex, content_length_equals, content_length_does_not_equal, content_length_greater_than, content_length_greater_than_or_equal_to, content_length_less_than, content_length_less_than_or_equal_to. - Add regex matching filters: matches_regex and does_not_match_regex to open_filters.json. - Remove console log statements from validatePropertiesLogic.js and FormPropertyLogicRule.js for cleaner code. This update enhances the filtering capabilities and improves the validation logic by removing unnecessary debug outputs. * Refactor Vue Components for Improved Readability and Consistency - Update CameraUpload.vue to enhance formatting and maintain consistent indentation for better readability. - Modify OpenFormField.vue to improve tooltip component formatting, ensuring consistent styling across the application. - Refactor FormSecurityAccess.vue to enhance the layout of the conditional rendering for captcha providers. - Adjust RegisterForm.vue to improve the formatting of the terms and conditions section, ensuring consistent indentation and readability. These changes aim to enhance code maintainability and visual consistency across the components, contributing to a cleaner codebase. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
2025-03-15 06:55:42 +01:00
<UTooltip
text="Edit field settings"
:popper="{ placement: 'right' }"
class="z-[100]"
>
<Icon
name="heroicons:cog-8-tooth-20-solid"
class="w-5 h-5 !text-gray-500 dark:!text-gray-500 hover:!text-blue-500 dark:hover:!text-blue-500"
/>
</UTooltip>
2023-12-09 15:47:03 +01:00
</div>
<div
class="p-1 hover:!text-red-600 dark:hover:!text-red-600 hover:bg-red-50 cursor-pointer flex items-center justify-center text-center !text-red-500 dark:!text-red-500 rounded-md"
role="button"
@click.prevent="removeField"
>
Add regex matching filters to open_filters.json (#722) * Add regex matching filters to open_filters.json - Introduce new regex-based filters: matches_regex and does_not_match_regex - Add support for string-based regex matching in both API and client filter configurations - Update filter schemas to include new regex filter options with expected string type * Add barcode comparators and regex filters to open_filters.json and validatePropertiesLogic.js - Introduce new barcode comparators: equals, does_not_equal, contains, does_not_contain, starts_with, ends_with, is_empty, is_not_empty, matches_regex, does_not_match_regex, content_length_equals, content_length_does_not_equal, content_length_greater_than, content_length_greater_than_or_equal_to, content_length_less_than, content_length_less_than_or_equal_to. - Add regex matching filters: matches_regex and does_not_match_regex to open_filters.json. - Remove console log statements from validatePropertiesLogic.js and FormPropertyLogicRule.js for cleaner code. This update enhances the filtering capabilities and improves the validation logic by removing unnecessary debug outputs. * Refactor Vue Components for Improved Readability and Consistency - Update CameraUpload.vue to enhance formatting and maintain consistent indentation for better readability. - Modify OpenFormField.vue to improve tooltip component formatting, ensuring consistent styling across the application. - Refactor FormSecurityAccess.vue to enhance the layout of the conditional rendering for captcha providers. - Adjust RegisterForm.vue to improve the formatting of the terms and conditions section, ensuring consistent indentation and readability. These changes aim to enhance code maintainability and visual consistency across the components, contributing to a cleaner codebase. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
2025-03-15 06:55:42 +01:00
<UTooltip
text="Delete field"
:popper="{ placement: 'right' }"
class="z-[100]"
>
<Icon
name="heroicons:trash-20-solid"
class="w-5 h-5 !text-red-500 dark:!text-red-500 hover:!text-red-600 dark:hover:!text-red-600"
/>
</UTooltip>
</div>
2023-12-09 15:47:03 +01:00
</div>
</div>
<component
:is="getFieldComponents"
v-if="getFieldComponents"
v-bind="inputProperties(field)"
:required="isFieldRequired"
:disabled="isFieldDisabled ? true : null"
/>
2023-12-09 15:47:03 +01:00
<template v-else>
<div
v-if="field.type === 'nf-text' && field.content"
:id="field.id"
:key="field.id"
class="nf-text w-full my-1.5"
:class="[getFieldAlignClasses(field)]"
v-html="field.content"
/>
<div
v-if="field.type === 'nf-code' && field.content"
:id="field.id"
:key="field.id"
class="nf-code w-full px-2 my-1.5"
v-html="field.content"
/>
<div
v-if="field.type === 'nf-divider'"
:id="field.id"
:key="field.id"
class="border-b my-4 w-full mx-2"
/>
<div
v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)"
:id="field.id"
:key="field.id"
class="my-4 w-full px-2"
:class="[getFieldAlignClasses(field)]"
@dblclick="editFieldOptions"
>
<div
v-if="!field.image_block"
class="p-4 border border-dashed text-center"
>
<a
href="#"
class="text-blue-800 dark:text-blue-200"
@click.prevent="editFieldOptions"
>Open block settings to upload image.</a>
2023-12-09 15:47:03 +01:00
</div>
<img
v-else
:alt="field.name"
:src="field.image_block"
class="max-w-full inline-block"
:class="theme.default.borderRadius"
>
2023-12-09 15:47:03 +01:00
</div>
</template>
<div
class="hidden group-hover/nffield:flex translate-x-full absolute right-0 top-0 h-full w-5 flex-col justify-center pl-1 pt-3"
>
<div
class="flex items-center bg-gray-100 dark:bg-gray-800 border rounded-md h-12 text-gray-500 dark:text-gray-400 dark:border-gray-500 cursor-grab handle min-h-[40px]"
>
<Icon
name="clarity:drag-handle-line"
class="h-6 w-6 -ml-1 block shrink-0"
/>
</div>
</div>
2023-12-09 15:47:03 +01:00
</div>
</div>
</template>
<script>
import {computed} from 'vue'
2023-12-18 10:35:00 +01:00
import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import {default as _has} from 'lodash/has'
2023-12-09 15:47:03 +01:00
export default {
name: 'OpenFormField',
components: {},
props: {
form: {
type: Object,
required: true
},
dataForm: {
type: Object,
required: true
},
dataFormValue: {
type: Object,
required: true
},
theme: {
type: Object, default: () => {
const theme = inject("theme", null)
if (theme) {
return theme.value
}
return CachedDefaultTheme.getInstance()
}
2023-12-09 15:47:03 +01:00
},
showHidden: {
type: Boolean,
default: false
},
darkMode: {
type: Boolean,
default: false
},
2023-12-09 15:47:03 +01:00
field: {
type: Object,
required: true
},
adminPreview: {type: Boolean, default: false} // If used in FormEditorPreview
2023-12-09 15:47:03 +01:00
},
setup() {
2023-12-09 15:47:03 +01:00
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
currentWorkspace: computed(() => useWorkspacesStore().getCurrent),
2023-12-09 15:47:03 +01:00
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar)
}
},
computed: {
/**
* Get the right input component for the field/options combination
*/
getFieldComponents() {
2023-12-09 15:47:03 +01:00
const field = this.field
if (field.type === 'text' && field.multi_lines) {
return 'TextAreaInput'
}
if (field.type === 'url' && field.file_upload) {
return 'FileInput'
}
if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) {
return 'FlatSelectInput'
}
if (field.type === 'checkbox' && field.use_toggle_switch) {
return 'ToggleSwitchInput'
}
if (field.type === 'signature') {
return 'SignatureInput'
}
if (field.type === 'phone_number' && !field.use_simple_text_input) {
return 'PhoneInput'
}
return {
text: 'TextInput',
rich_text: 'RichTextAreaInput',
number: 'TextInput',
rating: 'RatingInput',
scale: 'ScaleInput',
slider: 'SliderInput',
select: 'SelectInput',
multi_select: 'SelectInput',
date: 'DateInput',
files: 'FileInput',
checkbox: 'CheckboxInput',
url: 'TextInput',
email: 'TextInput',
phone_number: 'TextInput',
matrix: 'MatrixInput',
barcode: 'BarcodeInput'
}[field.type]
2023-12-09 15:47:03 +01:00
},
isPublicFormPage() {
return this.$route.name === 'forms-slug'
2023-12-09 15:47:03 +01:00
},
isFieldHidden() {
2023-12-09 15:47:03 +01:00
return !this.showHidden && this.shouldBeHidden
},
shouldBeHidden() {
2023-12-09 15:47:03 +01:00
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isHidden()
},
isFieldRequired() {
2023-12-09 15:47:03 +01:00
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isRequired()
},
isFieldDisabled() {
2023-12-09 15:47:03 +01:00
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled()
},
beingEdited() {
2023-12-09 15:47:03 +01:00
return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return item.id === this.field.id
}) === this.selectedFieldIndex
},
selectionFieldsOptions() {
2023-12-09 15:47:03 +01:00
// For auto update hidden options
let fieldsOptions = []
if (['select', 'multi_select', 'status'].includes(this.field.type)) {
fieldsOptions = [...this.field[this.field.type].options]
if (this.field.hidden_options && this.field.hidden_options.length > 0) {
fieldsOptions = fieldsOptions.filter((option) => {
return this.field.hidden_options.indexOf(option.id) < 0
})
}
}
return fieldsOptions
},
fieldSideBarOpened() {
2023-12-09 15:47:03 +01:00
return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
}
},
watch: {},
mounted() {
2023-12-09 15:47:03 +01:00
},
methods: {
editFieldOptions() {
if (!this.adminPreview) return
this.workingFormStore.openSettingsForField(this.field)
},
setFieldAsSelected () {
if (!this.adminPreview || !this.workingFormStore.showEditFieldSidebar) return
2023-12-09 15:47:03 +01:00
this.workingFormStore.openSettingsForField(this.field)
},
openAddFieldSidebar() {
if (!this.adminPreview) return
2023-12-09 15:47:03 +01:00
this.workingFormStore.openAddFieldSidebar(this.field)
},
removeField () {
if (!this.adminPreview) return
this.workingFormStore.removeField(this.field)
},
getFieldWidthClasses(field) {
if (!field.width || field.width === 'full') return 'col-span-full'
2023-12-09 15:47:03 +01:00
else if (field.width === '1/2') {
return 'sm:col-span-6 col-span-full'
2023-12-09 15:47:03 +01:00
} else if (field.width === '1/3') {
return 'sm:col-span-4 col-span-full'
2023-12-09 15:47:03 +01:00
} else if (field.width === '2/3') {
return 'sm:col-span-8 col-span-full'
2023-12-09 15:47:03 +01:00
} else if (field.width === '1/4') {
return 'sm:col-span-3 col-span-full'
2023-12-09 15:47:03 +01:00
} else if (field.width === '3/4') {
return 'sm:col-span-9 col-span-full'
2023-12-09 15:47:03 +01:00
}
},
getFieldAlignClasses(field) {
2023-12-09 15:47:03 +01:00
if (!field.align || field.align === 'left') return 'text-left'
else if (field.align === 'right') {
return 'text-right'
} else if (field.align === 'center') {
return 'text-center'
} else if (field.align === 'justify') {
return 'text-justify'
}
},
/**
* Get the right input component options for the field/options
*/
inputProperties(field) {
2023-12-09 15:47:03 +01:00
const inputProperties = {
key: field.id,
name: field.id,
form: this.dataForm,
label: (field.hide_field_name) ? null : field.name + ((this.shouldBeHidden) ? ' (Hidden Field)' : ''),
color: this.form.color,
placeholder: field.placeholder,
help: field.help,
helpPosition: (field.help_position) ? field.help_position : 'below_input',
uppercaseLabels: this.form.uppercase_labels == 1 || this.form.uppercase_labels == true,
theme: this.theme,
maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : null,
showCharLimit: field.show_char_limit || false,
isDark: this.darkMode,
locale: (this.form?.language) ? this.form.language : 'en'
2023-12-09 15:47:03 +01:00
}
if (field.type === 'matrix') {
inputProperties.rows = field.rows
inputProperties.columns = field.columns
}
if (field.type === 'barcode') {
inputProperties.decoders = field.decoders
}
if (['select','multi_select'].includes(field.type) && !this.isFieldRequired) {
inputProperties.clearable = true
}
2023-12-09 15:47:03 +01:00
if (['select', 'multi_select'].includes(field.type)) {
inputProperties.options = (_has(field, field.type))
2023-12-09 15:47:03 +01:00
? field[field.type].options.map(option => {
return {
name: option.name,
value: option.name
}
})
: []
inputProperties.multiple = (field.type === 'multi_select')
inputProperties.allowCreation = (field.allow_creation === true)
inputProperties.searchable = (inputProperties.options.length > 4)
} else if (field.type === 'date') {
inputProperties.dateFormat = field.date_format
inputProperties.timeFormat = field.time_format
2023-12-09 15:47:03 +01:00
if (field.with_time) {
inputProperties.withTime = true
}
if (field.date_range) {
2023-12-09 15:47:03 +01:00
inputProperties.dateRange = true
}
if (field.disable_past_dates) {
inputProperties.disablePastDates = true
} else if (field.disable_future_dates) {
inputProperties.disableFutureDates = true
}
} else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) {
inputProperties.multiple = (field.multiple !== undefined && field.multiple)
inputProperties.cameraUpload = (field.camera_upload !== undefined && field.camera_upload)
let maxFileSize = (this.form?.workspace && this.form?.workspace.max_file_size) ? this.form?.workspace?.max_file_size : 10
if (field?.max_file_size > 0) {
maxFileSize = Math.min(field.max_file_size, maxFileSize)
}
inputProperties.mbLimit = maxFileSize
2023-12-09 15:47:03 +01:00
inputProperties.accept = (this.form.is_pro && field.allowed_file_types) ? field.allowed_file_types : ''
} else if (field.type === 'rating') {
inputProperties.numberOfStars = parseInt(field.rating_max_value) ?? 5
} else if (field.type === 'scale') {
inputProperties.minScale = parseFloat(field.scale_min_value) ?? 1
inputProperties.maxScale = parseFloat(field.scale_max_value) ?? 5
inputProperties.stepScale = parseFloat(field.scale_step_value) ?? 1
} else if (field.type === 'slider') {
inputProperties.minSlider = parseInt(field.slider_min_value) ?? 0
inputProperties.maxSlider = parseInt(field.slider_max_value) ?? 50
inputProperties.stepSlider = parseInt(field.slider_step_value) ?? 5
2023-12-09 15:47:03 +01:00
} else if (field.type === 'number' || (field.type === 'phone_number' && field.use_simple_text_input)) {
inputProperties.pattern = '/d*'
2023-12-09 15:47:03 +01:00
} else if (field.type === 'phone_number' && !field.use_simple_text_input) {
inputProperties.unavailableCountries = field.unavailable_countries ?? []
} else if (field.type === 'text' && field.secret_input) {
inputProperties.nativeType = 'password'
2023-12-09 15:47:03 +01:00
}
return inputProperties
}
}
}
</script>
<style lang='scss' scoped>
.nf-text {
ol {
@apply list-decimal list-inside;
}
ul {
@apply list-disc list-inside;
}
}
</style>