<template> <div v-auto-overflow-scroll class="context__form context__form--scrollable"> <form class="context__form-container" @submit.prevent="submit"> <FormGroup :error="v$.name.$error"> <FormInput ref="name" v-model="values.name" :error="v$.name.$error" :placeholder="$t('fieldForm.name')" @blur="v$.name.$touch" @input="isPrefilledWithSuggestedFieldName = false" @keydown.enter="handleKeydownEnter($event)" ></FormInput> <template #error> <span v-if="v$.name.required.$invalid"> {{ $t('error.requiredField') }} </span> <span v-else-if="v$.name.mustHaveUniqueFieldName.$invalid"> {{ $t('fieldForm.fieldAlreadyExists') }} </span> <span v-else-if="v$.name.mustNotClashWithReservedName.$invalid"> {{ $t('error.nameNotAllowed') }} </span> <span v-else-if="v$.name.maxLength.$invalid"> {{ $t('error.nameTooLong') }} </span> </template> </FormGroup> <FormGroup v-if="forcedType === null" :error="v$.type.$error"> <Dropdown ref="fieldTypesDropdown" v-model="v$.type.$model" :error="v$.type.$error" :fixed-items="true" :disabled=" defaultValues.immutable_type || defaultValues.immutable_properties " @hide="v$.type.$touch" > <DropdownItem v-for="(fieldType, type) in fieldTypes" :key="type" :icon="fieldType.iconClass" :name="fieldType.getName()" :value="fieldType.type" :disabled=" (primary && !fieldType.canBePrimaryField) || !fieldType.isEnabled(workspace) || fieldType.isDeactivated(workspace.id) " @click="clickOnDeactivatedItem($event, fieldType)" > <i class="select__item-icon" :class="fieldType.iconClass" /> <span class="select__item-name-text" :title="fieldType.getName()">{{ fieldType.getName() }}</span> <i v-if="fieldType.isDeactivated(workspace.id)" class="iconoir-lock" ></i> <component :is="fieldType.getDeactivatedClickModal(workspace.id)" :ref="'deactivatedClickModal-' + fieldType.type" :v-if=" fieldType.isDeactivated(workspace.id) && fieldType.getDeactivatedClickModal(workspace.id) " :name="$t(fieldType.getName())" :workspace="workspace" ></component> </DropdownItem> </Dropdown> <template #error> {{ $t('error.requiredField') }}</template> </FormGroup> <template v-if="hasFormComponent && !defaultValues.immutable_properties"> <component :is="getFormComponent(v$.type.$model)" ref="childForm" :table="table" :field-type="v$.type.$model" :view="view" :primary="primary" :all-fields-in-table="allFieldsInTable" :name="values.name" :default-values="defaultValues" :database="database" @validate="v$.$touch" @suggested-field-name="handleSuggestedFieldName($event)" /> </template> <FormGroup v-if="showDescription" :error="fieldHasErrors('description')" :label="$t('fieldForm.description')" :small-label="true" required > <div class="control__elements"> <FormTextarea ref="description" v-model="v$.description.$model" :min-rows="1" :max-rows="16" auto-expandable :placeholder="$t('fieldForm.description')" size="small" /> </div> </FormGroup> </form> </div> </template> <script> import { mapGetters } from 'vuex' import { required, maxLength } from '@vuelidate/validators' import FormTextarea from '@baserow/modules/core/components/FormTextarea' import { useVuelidate } from '@vuelidate/core' import { reactive, computed } from 'vue' import { getNextAvailableNameInSequence } from '@baserow/modules/core/utils/string' import form from '@baserow/modules/core/mixins/form' import { RESERVED_BASEROW_FIELD_NAMES, MAX_FIELD_NAME_LENGTH, } from '@baserow/modules/database/utils/constants' // @TODO focus form on open export default { name: 'FieldForm', components: { FormTextarea }, mixins: [form], props: { table: { type: Object, required: true, }, view: { type: [Object, null], required: false, default: null, }, primary: { type: Boolean, required: false, default: false, }, forcedType: { type: [String, null], required: false, default: null, }, allFieldsInTable: { type: Array, required: true, }, database: { type: Object, required: true, }, }, data() { return { allowedValues: ['name', 'type', 'description'], values: { name: '', type: '' }, v$: null, isPrefilledWithSuggestedFieldName: false, oldValueType: null, showDescription: false, } }, computed: { // Return the reactive object that can be updated in runtime. workspace() { return this.$store.getters['workspace/get'](this.database.workspace.id) }, fieldTypes() { return this.$registry.getAll('field') }, hasFormComponent() { return !!this.values.type && this.getFormComponent(this.values.type) }, existingFieldId() { return this.defaultValues ? this.defaultValues.id : null }, ...mapGetters({ fields: 'field/getAll', }), isNameFieldEmptyOrPrefilled() { return ( this.v$.name.$model === '' || this.v$.name.$model === this.getNextAvailableFieldName( this.fieldTypes[this.oldValueType]?.getName() ) || this.v$.name.$model === this.getNextAvailableFieldName( this.fieldTypes[this.values.type]?.getName() ) || this.isPrefilledWithSuggestedFieldName ) }, }, watch: { // if the name field is empty or prefilled by a default value // we want to update the name field with the name of the field type // when the field type is changed. 'values.type'(newValueType, oldValueType) { this.oldValueType = oldValueType if (this.isNameFieldEmptyOrPrefilled) { const availableFieldName = this.getNextAvailableFieldName( this.fieldTypes[newValueType]?.getName() ) this.values.name = availableFieldName } this.isPrefilledWithSuggestedFieldName = false }, }, created() { const values = reactive({ name: '', type: this.forcedType || '', description: null, }) const rules = computed(() => ({ name: { required, maxLength: maxLength(MAX_FIELD_NAME_LENGTH), mustHaveUniqueFieldName: this.mustHaveUniqueFieldName, mustNotClashWithReservedName: this.mustNotClashWithReservedName, }, type: { required }, description: {}, })) this.v$ = useVuelidate(rules, values, { $lazy: true }) this.values = values }, methods: { mustHaveUniqueFieldName(param) { let fields = this.fields if (this.existingFieldId !== null) { fields = fields.filter((f) => f.id !== this.existingFieldId) } return !fields.map((f) => f.name).includes(param?.trim()) }, mustNotClashWithReservedName(param) { return !RESERVED_BASEROW_FIELD_NAMES.includes(param?.trim()) }, getFormComponent(type) { const fieldType = this.$registry.get('field', type) if (fieldType.isEnabled(this.workspace)) { return fieldType.getFormComponent() } }, showFieldTypesDropdown(target) { this.$refs.fieldTypesDropdown.show(target) }, handleSuggestedFieldName(event) { if (this.isNameFieldEmptyOrPrefilled) { this.isPrefilledWithSuggestedFieldName = true const availableFieldName = this.getNextAvailableFieldName(event) this.values.name = availableFieldName } }, getNextAvailableFieldName(baseName) { const excludeNames = this.fields.map((f) => f.name) return getNextAvailableNameInSequence(baseName, excludeNames) }, handleKeydownEnter(event) { event.preventDefault() this.$emit('keydown-enter') this.submit() }, clickOnDeactivatedItem(event, fieldType) { if (fieldType.isDeactivated(this.workspace.id)) { this.$refs[`deactivatedClickModal-${fieldType.type}`][0].show() } }, /** * This sets the showDescription flag to display description text editor, even * if values.description is empty. * * Used by parent components. */ showDescriptionField() { this.showDescription = true this.$nextTick(() => { this.$refs.description.focus() }) }, /** * Helper method to get information if description is not empty. * Used by parent components */ isDescriptionFieldNotEmpty() { this.showDescription = !!this.values.description return this.showDescription }, }, } </script>