Merge pull request #9205 from Budibase/fix/block-duplicate-autocolumn-types

Create/Edit Column refactoring and validation updates
This commit is contained in:
deanhannigan 2023-01-31 11:30:49 +00:00 committed by GitHub
commit 02b6890d58
2 changed files with 243 additions and 112 deletions

View File

@ -12,7 +12,7 @@
Modal, Modal,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
@ -48,7 +48,22 @@
const { hide } = getContext(Context.Modal) const { hide } = getContext(Context.Modal)
let fieldDefinitions = cloneDeep(FIELDS) let fieldDefinitions = cloneDeep(FIELDS)
export let field = { export let field
let originalName
let linkEditDisabled
let primaryDisplay
let indexes = [...($tables.selected.indexes || [])]
let isCreating
let table = $tables.selected
let confirmDeleteDialog
let deletion
let savingColumn
let deleteColName
let jsonSchemaModal
let editableColumn = {
type: "string", type: "string",
constraints: fieldDefinitions.STRING.constraints, constraints: fieldDefinitions.STRING.constraints,
@ -56,48 +71,80 @@
fieldName: $tables.selected.name, fieldName: $tables.selected.name,
} }
let originalName = field.name $: if (primaryDisplay) {
const linkEditDisabled = originalName != null editableColumn.constraints.presence = { allowEmpty: false }
let primaryDisplay = }
$tables.selected.primaryDisplay == null ||
$tables.selected.primaryDisplay === field.name
let isCreating = originalName == null
let table = $tables.selected $: if (field && !savingColumn) {
let indexes = [...($tables.selected.indexes || [])] editableColumn = cloneDeep(field)
let confirmDeleteDialog originalName = editableColumn.name ? editableColumn.name + "" : null
let deletion linkEditDisabled = originalName != null
let deleteColName isCreating = originalName == null
let jsonSchemaModal primaryDisplay =
$tables.selected.primaryDisplay == null ||
$tables.selected.primaryDisplay === editableColumn.name
}
$: checkConstraints(field) $: checkConstraints(editableColumn)
$: required = !!field?.constraints?.presence || primaryDisplay $: required = !!editableColumn?.constraints?.presence || primaryDisplay
$: uneditable = $: uneditable =
$tables.selected?._id === TableNames.USERS && $tables.selected?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name) UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
$: invalid = $: invalid =
!field.name || !editableColumn?.name ||
(field.type === LINK_TYPE && !field.tableId) || (editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
Object.keys(errors).length !== 0 Object.keys(errors).length !== 0
$: errors = checkErrors(field) $: errors = checkErrors(editableColumn)
$: datasource = $datasources.list.find( $: datasource = $datasources.list.find(
source => source._id === table?.sourceId source => source._id === table?.sourceId
) )
const getTableAutoColumnTypes = table => {
return Object.keys(table?.schema).reduce((acc, key) => {
let fieldSchema = table?.schema[key]
if (fieldSchema.autocolumn) {
acc.push(fieldSchema.subtype)
}
return acc
}, [])
}
let autoColumnInfo = getAutoColumnInformation()
$: tableAutoColumnsTypes = getTableAutoColumnTypes($tables?.selected)
$: availableAutoColumns = Object.keys(autoColumnInfo).reduce((acc, key) => {
if (!tableAutoColumnsTypes.includes(key)) {
acc[key] = autoColumnInfo[key]
}
return acc
}, {})
$: availableAutoColumnKeys = availableAutoColumns
? Object.keys(availableAutoColumns)
: []
$: autoColumnOptions = editableColumn.autocolumn
? autoColumnInfo
: availableAutoColumns
// used to select what different options can be displayed for column type // used to select what different options can be displayed for column type
$: canBeSearched = $: canBeSearched =
field.type !== LINK_TYPE && editableColumn?.type !== LINK_TYPE &&
field.type !== JSON_TYPE && editableColumn?.type !== JSON_TYPE &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY && editableColumn?.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY && editableColumn?.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY &&
field.type !== FORMULA_TYPE editableColumn?.type !== FORMULA_TYPE
$: canBeDisplay = $: canBeDisplay =
field.type !== LINK_TYPE && editableColumn?.type !== LINK_TYPE &&
field.type !== AUTO_TYPE && editableColumn?.type !== AUTO_TYPE &&
field.type !== JSON_TYPE editableColumn?.type !== JSON_TYPE &&
!editableColumn.autocolumn
$: canBeRequired = $: canBeRequired =
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE editableColumn?.type !== LINK_TYPE &&
$: relationshipOptions = getRelationshipOptions(field) !uneditable &&
editableColumn?.type !== AUTO_TYPE &&
!editableColumn.autocolumn
$: relationshipOptions = getRelationshipOptions(editableColumn)
$: external = table.type === "external" $: external = table.type === "external"
// in the case of internal tables the sourceId will just be undefined // in the case of internal tables the sourceId will just be undefined
$: tableOptions = $tables.list.filter( $: tableOptions = $tables.list.filter(
@ -108,76 +155,90 @@
) )
$: typeEnabled = $: typeEnabled =
!originalName || !originalName ||
(originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1) (originalName &&
SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 &&
!editableColumn?.autocolumn)
async function saveColumn() { async function saveColumn() {
if (field.type === AUTO_TYPE) { savingColumn = true
field = buildAutoColumn($tables.draft.name, field.name, field.subtype) if (errors?.length) {
return
} }
if (field.type !== LINK_TYPE) {
delete field.fieldName let saveColumn = cloneDeep(editableColumn)
if (saveColumn.type === AUTO_TYPE) {
saveColumn = buildAutoColumn(
$tables.draft.name,
saveColumn.name,
saveColumn.subtype
)
}
if (saveColumn.type !== LINK_TYPE) {
delete saveColumn.fieldName
} }
try { try {
await tables.saveField({ await tables.saveField({
originalName, originalName,
field, field: saveColumn,
primaryDisplay, primaryDisplay,
indexes, indexes,
}) })
dispatch("updatecolumns") dispatch("updatecolumns")
} catch (err) { } catch (err) {
notifications.error("Error saving column") console.log(err)
notifications.error(`Error saving column: ${err.message}`)
} }
} }
function cancelEdit() { function cancelEdit() {
field.name = originalName editableColumn.name = originalName
} }
function deleteColumn() { function deleteColumn() {
try { try {
field.name = deleteColName editableColumn.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) { if (editableColumn.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column") notifications.error("You cannot delete the display column")
} else { } else {
tables.deleteField(field) tables.deleteField(editableColumn)
notifications.success(`Column ${field.name} deleted.`) notifications.success(`Column ${editableColumn.name} deleted.`)
confirmDeleteDialog.hide() confirmDeleteDialog.hide()
hide() hide()
deletion = false deletion = false
dispatch("updatecolumns") dispatch("updatecolumns")
} }
} catch (error) { } catch (error) {
notifications.error("Error deleting column") notifications.error(`Error deleting column: ${error.message}`)
} }
} }
function handleTypeChange(event) { function handleTypeChange(event) {
// remove any extra fields that may not be related to this type // remove any extra fields that may not be related to this type
delete field.autocolumn delete editableColumn.autocolumn
delete field.subtype delete editableColumn.subtype
delete field.tableId delete editableColumn.tableId
delete field.relationshipType delete editableColumn.relationshipType
delete field.formulaType delete editableColumn.formulaType
// Add in defaults and initial definition // Add in defaults and initial definition
const definition = fieldDefinitions[event.detail?.toUpperCase()] const definition = fieldDefinitions[event.detail?.toUpperCase()]
if (definition?.constraints) { if (definition?.constraints) {
field.constraints = definition.constraints editableColumn.constraints = definition.constraints
} }
// Default relationships many to many // Default relationships many to many
if (field.type === LINK_TYPE) { if (editableColumn.type === LINK_TYPE) {
field.relationshipType = RelationshipTypes.MANY_TO_MANY editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY
} }
if (field.type === FORMULA_TYPE) { if (editableColumn.type === FORMULA_TYPE) {
field.formulaType = "dynamic" editableColumn.formulaType = "dynamic"
} }
} }
function onChangeRequired(e) { function onChangeRequired(e) {
const req = e.detail const req = e.detail
field.constraints.presence = req ? { allowEmpty: false } : false editableColumn.constraints.presence = req ? { allowEmpty: false } : false
required = req required = req
} }
@ -185,17 +246,17 @@
const isPrimary = e.detail const isPrimary = e.detail
// primary display is always required // primary display is always required
if (isPrimary) { if (isPrimary) {
field.constraints.presence = { allowEmpty: false } editableColumn.constraints.presence = { allowEmpty: false }
} }
} }
function onChangePrimaryIndex(e) { function onChangePrimaryIndex(e) {
indexes = e.detail ? [field.name] : [] indexes = e.detail ? [editableColumn.name] : []
} }
function onChangeSecondaryIndex(e) { function onChangeSecondaryIndex(e) {
if (e.detail) { if (e.detail) {
indexes[1] = field.name indexes[1] = editableColumn.name
} else { } else {
indexes = indexes.slice(0, 1) indexes = indexes.slice(0, 1)
} }
@ -246,11 +307,14 @@
} }
function getAllowedTypes() { function getAllowedTypes() {
if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) { if (
originalName &&
ALLOWABLE_STRING_TYPES.indexOf(editableColumn.type) !== -1
) {
return ALLOWABLE_STRING_OPTIONS return ALLOWABLE_STRING_OPTIONS
} else if ( } else if (
originalName && originalName &&
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1 ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1
) { ) {
return ALLOWABLE_NUMBER_OPTIONS return ALLOWABLE_NUMBER_OPTIONS
} else if (!external) { } else if (!external) {
@ -275,6 +339,9 @@
} }
function checkConstraints(fieldToCheck) { function checkConstraints(fieldToCheck) {
if (!fieldToCheck) {
return
}
// most types need this, just make sure its always present // most types need this, just make sure its always present
if (fieldToCheck && !fieldToCheck.constraints) { if (fieldToCheck && !fieldToCheck.constraints) {
fieldToCheck.constraints = {} fieldToCheck.constraints = {}
@ -296,10 +363,16 @@
} }
function checkErrors(fieldInfo) { function checkErrors(fieldInfo) {
if (!editableColumn) {
return {}
}
function inUse(tbl, column, ogName = null) { function inUse(tbl, column, ogName = null) {
return Object.keys(tbl?.schema || {}).some( const parsedColumn = column ? column.toLowerCase().trim() : column
key => key !== ogName && key === column
) return Object.keys(tbl?.schema || {}).some(key => {
let lowerKey = key.toLowerCase()
return lowerKey !== ogName?.toLowerCase() && lowerKey === parsedColumn
})
} }
const newError = {} const newError = {}
if (!external && fieldInfo.name?.startsWith("_")) { if (!external && fieldInfo.name?.startsWith("_")) {
@ -313,6 +386,11 @@
} else if (inUse($tables.draft, fieldInfo.name, originalName)) { } else if (inUse($tables.draft, fieldInfo.name, originalName)) {
newError.name = `Column name already in use.` newError.name = `Column name already in use.`
} }
if (fieldInfo.type == "auto" && !fieldInfo.subtype) {
newError.subtype = `Auto Column requires a type`
}
if (fieldInfo.fieldName && fieldInfo.tableId) { if (fieldInfo.fieldName && fieldInfo.tableId) {
const relatedTable = $tables.list.find( const relatedTable = $tables.list.find(
tbl => tbl._id === fieldInfo.tableId tbl => tbl._id === fieldInfo.tableId
@ -323,12 +401,6 @@
} }
return newError return newError
} }
onMount(() => {
if (primaryDisplay) {
field.constraints.presence = { allowEmpty: false }
}
})
</script> </script>
<ModalContent <ModalContent
@ -340,19 +412,26 @@
> >
<Input <Input
label="Name" label="Name"
bind:value={field.name} bind:value={editableColumn.name}
disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)} disabled={uneditable ||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
error={errors?.name} error={errors?.name}
/> />
<Select <Select
disabled={!typeEnabled} disabled={!typeEnabled}
label="Type" label="Type"
bind:value={field.type} bind:value={editableColumn.type}
on:change={handleTypeChange} on:change={handleTypeChange}
options={getAllowedTypes()} options={getAllowedTypes()}
getOptionLabel={field => field.name} getOptionLabel={field => field.name}
getOptionValue={field => field.type} getOptionValue={field => field.type}
isOptionEnabled={option => {
if (option.type == AUTO_TYPE) {
return availableAutoColumnKeys?.length > 0
}
return true
}}
/> />
{#if canBeRequired || canBeDisplay} {#if canBeRequired || canBeDisplay}
@ -381,32 +460,32 @@
<div> <div>
<Label>Search Indexes</Label> <Label>Search Indexes</Label>
<Toggle <Toggle
value={indexes[0] === field.name} value={indexes[0] === editableColumn.name}
disabled={indexes[1] === field.name} disabled={indexes[1] === editableColumn.name}
on:change={onChangePrimaryIndex} on:change={onChangePrimaryIndex}
text="Primary" text="Primary"
/> />
<Toggle <Toggle
value={indexes[1] === field.name} value={indexes[1] === editableColumn.name}
disabled={!indexes[0] || indexes[0] === field.name} disabled={!indexes[0] || indexes[0] === editableColumn.name}
on:change={onChangeSecondaryIndex} on:change={onChangeSecondaryIndex}
text="Secondary" text="Secondary"
/> />
</div> </div>
{/if} {/if}
{#if field.type === "string"} {#if editableColumn.type === "string"}
<Input <Input
type="number" type="number"
label="Max Length" label="Max Length"
bind:value={field.constraints.length.maximum} bind:value={editableColumn.constraints.length.maximum}
/> />
{:else if field.type === "options"} {:else if editableColumn.type === "options"}
<ValuesList <ValuesList
label="Options (one per line)" label="Options (one per line)"
bind:values={field.constraints.inclusion} bind:values={editableColumn.constraints.inclusion}
/> />
{:else if field.type === "longform"} {:else if editableColumn.type === "longform"}
<div> <div>
<Label <Label
size="M" size="M"
@ -415,21 +494,24 @@
Formatting Formatting
</Label> </Label>
<Toggle <Toggle
bind:value={field.useRichText} bind:value={editableColumn.useRichText}
text="Enable rich text support (markdown)" text="Enable rich text support (markdown)"
/> />
</div> </div>
{:else if field.type === "array"} {:else if editableColumn.type === "array"}
<ValuesList <ValuesList
label="Options (one per line)" label="Options (one per line)"
bind:values={field.constraints.inclusion} bind:values={editableColumn.constraints.inclusion}
/> />
{:else if field.type === "datetime"} {:else if editableColumn.type === "datetime" && !editableColumn.autocolumn}
<DatePicker <DatePicker
label="Earliest" label="Earliest"
bind:value={field.constraints.datetime.earliest} bind:value={editableColumn.constraints.datetime.earliest}
/>
<DatePicker
label="Latest"
bind:value={editableColumn.constraints.datetime.latest}
/> />
<DatePicker label="Latest" bind:value={field.constraints.datetime.latest} />
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
<div> <div>
<Label <Label
@ -439,25 +521,28 @@
> >
Time zones Time zones
</Label> </Label>
<Toggle bind:value={field.ignoreTimezones} text="Ignore time zones" /> <Toggle
bind:value={editableColumn.ignoreTimezones}
text="Ignore time zones"
/>
</div> </div>
{/if} {/if}
{:else if field.type === "number"} {:else if editableColumn.type === "number" && !editableColumn.autocolumn}
<Input <Input
type="number" type="number"
label="Min Value" label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} bind:value={editableColumn.constraints.numericality.greaterThanOrEqualTo}
/> />
<Input <Input
type="number" type="number"
label="Max Value" label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} bind:value={editableColumn.constraints.numericality.lessThanOrEqualTo}
/> />
{:else if field.type === "link"} {:else if editableColumn.type === "link"}
<Select <Select
label="Table" label="Table"
disabled={linkEditDisabled} disabled={linkEditDisabled}
bind:value={field.tableId} bind:value={editableColumn.tableId}
options={tableOptions} options={tableOptions}
getOptionLabel={table => table.name} getOptionLabel={table => table.name}
getOptionValue={table => table._id} getOptionValue={table => table._id}
@ -466,7 +551,7 @@
<RadioGroup <RadioGroup
disabled={linkEditDisabled} disabled={linkEditDisabled}
label="Define the relationship" label="Define the relationship"
bind:value={field.relationshipType} bind:value={editableColumn.relationshipType}
options={relationshipOptions} options={relationshipOptions}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
getOptionValue={option => option.value} getOptionValue={option => option.value}
@ -476,14 +561,14 @@
<Input <Input
disabled={linkEditDisabled} disabled={linkEditDisabled}
label={`Column name in other table`} label={`Column name in other table`}
bind:value={field.fieldName} bind:value={editableColumn.fieldName}
error={errors.relatedName} error={errors.relatedName}
/> />
{:else if field.type === FORMULA_TYPE} {:else if editableColumn.type === FORMULA_TYPE}
{#if !table.sql} {#if !table.sql}
<Select <Select
label="Formula type" label="Formula type"
bind:value={field.formulaType} bind:value={editableColumn.formulaType}
options={[ options={[
{ label: "Dynamic", value: "dynamic" }, { label: "Dynamic", value: "dynamic" },
{ label: "Static", value: "static" }, { label: "Static", value: "static" },
@ -497,25 +582,28 @@
<ModalBindableInput <ModalBindableInput
title="Formula" title="Formula"
label="Formula" label="Formula"
value={field.formula} value={editableColumn.formula}
on:change={e => (field.formula = e.detail)} on:change={e => (editableColumn.formula = e.detail)}
bindings={getBindings({ table })} bindings={getBindings({ table })}
allowJS allowJS
/> />
{:else if field.type === AUTO_TYPE} {:else if editableColumn.type === JSON_TYPE}
<Select
label="Auto column type"
value={field.subtype}
on:change={e => (field.subtype = e.detail)}
options={Object.entries(getAutoColumnInformation())}
getOptionLabel={option => option[1].name}
getOptionValue={option => option[0]}
/>
{:else if field.type === JSON_TYPE}
<Button primary text on:click={openJsonSchemaEditor} <Button primary text on:click={openJsonSchemaEditor}
>Open schema editor</Button >Open schema editor</Button
> >
{/if} {/if}
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
<Select
label="Auto column type"
value={editableColumn.subtype}
on:change={e => (editableColumn.subtype = e.detail)}
options={Object.entries(autoColumnOptions)}
getOptionLabel={option => option[1].name}
getOptionValue={option => option[0]}
disabled={!availableAutoColumnKeys?.length || editableColumn.autocolumn}
error={errors?.subtype}
/>
{/if}
<div slot="footer"> <div slot="footer">
{#if !uneditable && originalName != null} {#if !uneditable && originalName != null}
@ -525,11 +613,11 @@
</ModalContent> </ModalContent>
<Modal bind:this={jsonSchemaModal}> <Modal bind:this={jsonSchemaModal}>
<JSONSchemaModal <JSONSchemaModal
schema={field.schema} schema={editableColumn.schema}
json={field.json} json={editableColumn.json}
on:save={({ detail }) => { on:save={({ detail }) => {
field.schema = detail.schema editableColumn.schema = detail.schema
field.json = detail.json editableColumn.json = detail.json
}} }}
/> />
</Modal> </Modal>

View File

@ -1,9 +1,45 @@
<script> <script>
import TableDataTable from "components/backend/DataTable/DataTable.svelte" import TableDataTable from "components/backend/DataTable/DataTable.svelte"
import { tables, database } from "stores/backend" import { tables, database } from "stores/backend"
import { Banner } from "@budibase/bbui"
const verifyAutocolumns = table => {
// Check for duplicates
return Object.values(table?.schema || {}).reduce((acc, fieldSchema) => {
if (!fieldSchema.autocolumn || !fieldSchema.subtype) {
return acc
}
let fieldKey = fieldSchema.tableId
? `${fieldSchema.tableId}-${fieldSchema.subtype}`
: fieldSchema.subtype
acc[fieldKey] = acc[fieldKey] || []
acc[fieldKey].push(fieldSchema)
return acc
}, {})
}
$: autoColumnStatus = verifyAutocolumns($tables?.selected)
$: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => {
if (status.length > 1) {
acc = [...acc, ...status]
}
return acc
}, [])
$: invalidColumnText = duplicates.map(entry => {
return `${entry.name} (${entry.subtype})`
})
</script> </script>
{#if $database?._id && $tables?.selected?.name} {#if $database?._id && $tables?.selected?.name}
{#if duplicates?.length}
<div class="alert-wrap">
<Banner type="warning" showCloseButton={false}>
{`Schema Invalid - There are duplicate auto column types defined in this schema.
Please delete the duplicate entries where appropriate: -
${invalidColumnText.join(", ")}`}
</Banner>
</div>
{/if}
<TableDataTable /> <TableDataTable />
{:else}<i>Create your first table to start building</i>{/if} {:else}<i>Create your first table to start building</i>{/if}
@ -13,4 +49,11 @@
color: var(--grey-5); color: var(--grey-5);
margin-top: 2px; margin-top: 2px;
} }
.alert-wrap {
display: flex;
width: 100%;
}
.alert-wrap :global(> *) {
flex: 1;
}
</style> </style>