<template> <form @submit.prevent="submit"> <div class="row margin-bottom-2"> <div class="col col-6"> <PageSettingsNameFormElement ref="name" v-model="values.name" :disabled="!hasPermission" :has-errors="fieldHasErrors('name')" :validation-state="v$.values.name" :is-creation="isCreation" @blur="v$.values.name.$touch()" /> </div> <div class="col col-6"> <PageSettingsPathFormElement v-model="values.path" :disabled="!hasPermission" :has-errors="fieldHasErrors('path')" :validation-state="v$.values.path" @blur="onPathBlur" /> </div> </div> <div class="row"> <div class="col col-6"> <PageSettingsPathParamsFormElement :disabled="!hasPermission" :path-params="values.path_params" @update="onPathParamUpdate" /> </div> <div class="col col-6"></div> </div> <slot></slot> </form> </template> <script> import { maxLength, required } from '@vuelidate/validators' import form from '@baserow/modules/core/mixins/form' import PageSettingsNameFormElement from '@baserow/modules/builder/components/page/settings/PageSettingsNameFormElement' import PageSettingsPathFormElement from '@baserow/modules/builder/components/page/settings/PageSettingsPathFormElement' import PageSettingsPathParamsFormElement from '@baserow/modules/builder/components/page/settings/PageSettingsPathParamsFormElement' import { getPathParams, PATH_PARAM_REGEX, ILLEGAL_PATH_SAMPLE_CHARACTER, VALID_PATH_CHARACTERS, } from '@baserow/modules/builder/utils/path' import { getNextAvailableNameInSequence, slugify, } from '@baserow/modules/core/utils/string' export default { name: 'PageSettingsForm', components: { PageSettingsPathParamsFormElement, PageSettingsPathFormElement, PageSettingsNameFormElement, }, mixins: [form], inject: ['workspace', 'builder', 'page'], props: { isCreation: { type: Boolean, required: false, default: false, }, }, data() { return { values: { name: '', path: '', path_params: [], }, hasPathBeenEdited: false, } }, computed: { hasPermission() { if (this.isCreation) { return this.$hasPermission( 'builder.create_page', this.builder, this.workspace.id ) } else { return this.$hasPermission( 'builder.page.update', this.page, this.workspace.id ) } }, defaultPathParamType() { return this.$registry.getOrderedList('pathParamType')[0].getType() }, defaultName() { const baseName = this.$t('pageForm.defaultName') return getNextAvailableNameInSequence(baseName, this.pageNames) }, pages() { return this.$store.getters['page/getVisiblePages'](this.builder) }, pageNames() { return this.pages.map((page) => page.name) }, otherPagePaths() { return this.pages .filter((page) => page.id !== this.page?.id) .map((page) => page.path) }, currentPathParams() { return getPathParams(this.values.path) }, }, watch: { // When the path change we want to update the value.path_params value // but try to keep the previous configuration as much as possible currentPathParams(paramNames, oldParamNames) { const result = paramNames.map((param) => ({ name: param, type: 'text', })) const pathParamIndexesByName = this.values.path_params.reduce( (prev, { name }, index) => { if (!prev[name]) { prev[name] = [] } prev[name].push(index) return prev }, {} ) // List of used index of existing params to use them once only. const usedIndex = [] // An index is ok if it has already been associated with an existing param // to prevent double association. const okIndex = [] // First match same names at same position paramNames.forEach((paramName, index) => { if (paramName === oldParamNames[index]) { Object.assign(result[index], this.values.path_params[index]) pathParamIndexesByName[paramName] = pathParamIndexesByName[ paramName ].filter((i) => i !== index) usedIndex.push(index) okIndex.push(index) } }) // Then match previously existing names at another position paramNames.forEach((paramName, index) => { if (okIndex.includes(index)) { return } if (pathParamIndexesByName[paramName]?.length) { const paramIndex = pathParamIndexesByName[paramName].shift() Object.assign(result[index], this.values.path_params[paramIndex]) usedIndex.push(paramIndex) okIndex.push(index) } }) // Then match remaining existing params in same relative order paramNames.forEach((paramName, index) => { if (okIndex.includes(index)) { return } const freeIndex = this.values.path_params.findIndex( (_, index) => !usedIndex.includes(index) ) if (freeIndex !== -1) { Object.assign(result[index], this.values.path_params[freeIndex], { name: paramName, }) usedIndex.push(freeIndex) } }) this.values.path_params = result }, 'values.name': { handler(value) { if (!this.hasPathBeenEdited && this.isCreation) { this.values.path = `/${slugify(value)}` } }, immediate: true, }, }, created() { if (this.isCreation) { this.values.name = this.defaultName } }, mounted() { if (this.isCreation) { this.$refs.name.$refs.input.focus() } }, methods: { generalisePath(path) { return path.replace(PATH_PARAM_REGEX, ILLEGAL_PATH_SAMPLE_CHARACTER) }, onPathBlur() { this.v$.values.path.$touch() this.hasPathBeenEdited = true }, onPathParamUpdate(paramTypeName, paramType) { this.values.path_params.forEach((pathParam) => { if (pathParam.name === paramTypeName) { pathParam.type = paramType } }) }, isNameUnique(name) { return !this.pageNames.includes(name) || name === this.page?.name }, isPathUnique(path) { const pathGeneralised = this.generalisePath(path) return ( !this.otherPagePaths.some( (pathCurrent) => this.generalisePath(pathCurrent) === pathGeneralised ) || path === this.page?.path ) }, pathStartsWithSlash(path) { return path[0] === '/' }, pathHasValidCharacters(path) { return !path .split('') .some((letter) => !VALID_PATH_CHARACTERS.includes(letter)) }, arePathParamsUnique(path) { const pathParams = getPathParams(path) return new Set(pathParams).size === pathParams.length }, }, validations() { return { values: { name: { required, isUnique: this.isNameUnique, maxLength: maxLength(255), }, path: { required, isUnique: this.isPathUnique, maxLength: maxLength(255), startingSlash: this.pathStartsWithSlash, validPathCharacters: this.pathHasValidCharacters, uniquePathParams: this.arePathParamsUnique, }, }, } }, } </script>