Merge pull request #1236 from Budibase/feature/one-many-relationship-changes

Feature/one many relationship changes
This commit is contained in:
Michael Drury 2021-03-03 10:52:54 +00:00 committed by GitHub
commit 46fe177766
5 changed files with 120 additions and 55 deletions

View File

@ -11,12 +11,17 @@
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { FIELDS, AUTO_COLUMN_SUB_TYPES } from "constants/backend" import {
FIELDS,
AUTO_COLUMN_SUB_TYPES,
RelationshipTypes,
} from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils" import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte" import ValuesList from "components/common/ValuesList.svelte"
import DatePicker from "components/common/DatePicker.svelte" import DatePicker from "components/common/DatePicker.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { truncate } from "lodash"
const AUTO_COL = "auto" const AUTO_COL = "auto"
const LINK_TYPE = FIELDS.LINK.type const LINK_TYPE = FIELDS.LINK.type
@ -36,16 +41,7 @@
$backendUiStore.selectedTable.primaryDisplay == null || $backendUiStore.selectedTable.primaryDisplay == null ||
$backendUiStore.selectedTable.primaryDisplay === field.name $backendUiStore.selectedTable.primaryDisplay === field.name
let relationshipTypes = [ let table = $backendUiStore.selectedTable
{ text: "Many to many (N:N)", value: "many-to-many" },
{ text: "One to many (1:N)", value: "one-to-many" },
]
let types = ["Many to many (N:N)", "One to many (1:N)"]
let selectedRelationshipType =
relationshipTypes.find(type => type.value === field.relationshipType)
?.text || "Many to many (N:N)"
let indexes = [...($backendUiStore.selectedTable.indexes || [])] let indexes = [...($backendUiStore.selectedTable.indexes || [])]
let confirmDeleteDialog let confirmDeleteDialog
let deletion let deletion
@ -57,7 +53,7 @@
$: uneditable = $: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS && $backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name) UNEDITABLE_USER_FIELDS.includes(field.name)
$: invalid = field.type === FIELDS.LINK.type && !field.tableId $: invalid = field.type === LINK_TYPE && !field.tableId
// 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 =
@ -67,15 +63,9 @@
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL $: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
$: canBeRequired = $: canBeRequired =
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
$: relationshipOptions = getRelationshipOptions(field)
async function saveColumn() { async function saveColumn() {
// Set relationship type if it's
if (field.type === "link") {
field.relationshipType = relationshipTypes.find(
type => type.text === selectedRelationshipType
).value
}
if (field.type === AUTO_COL) { if (field.type === AUTO_COL) {
field = buildAutoColumn( field = buildAutoColumn(
$backendUiStore.draftTable.name, $backendUiStore.draftTable.name,
@ -110,12 +100,18 @@
if (!definition) { if (!definition) {
return return
} }
field.type = definition.type
field.constraints = definition.constraints
// 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 field.autocolumn
delete field.subtype delete field.subtype
delete field.tableId delete field.tableId
delete field.relationshipType
// add in defaults and initial definition
field.type = definition.type
field.constraints = definition.constraints
// default relationships many to many
if (field.type === LINK_TYPE) {
field.relationshipType = RelationshipTypes.MANY_TO_MANY
}
} }
function onChangeRequired(e) { function onChangeRequired(e) {
@ -153,6 +149,32 @@
confirmDeleteDialog.hide() confirmDeleteDialog.hide()
deletion = false deletion = false
} }
function getRelationshipOptions(field) {
if (!field || !field.tableId) {
return null
}
const linkTable = tableOptions.find(table => table._id === field.tableId)
if (!linkTable) {
return null
}
const thisName = truncate(table.name, { length: 15 }),
linkName = truncate(linkTable.name, { length: 15 })
return [
{
name: `Many ${thisName} rows has many ${linkName} rows`,
value: RelationshipTypes.MANY_TO_MANY,
},
{
name: `One ${thisName} row has many ${linkName} rows`,
value: RelationshipTypes.ONE_TO_MANY,
},
{
name: `Many ${thisName} rows has one ${linkName} row`,
value: RelationshipTypes.MANY_TO_ONE,
},
]
}
</script> </script>
<div class="actions" class:hidden={deletion}> <div class="actions" class:hidden={deletion}>
@ -231,26 +253,32 @@
label="Max Value" label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'} {:else if field.type === 'link'}
<div>
<Label grey extraSmall>Select relationship type</Label>
<div class="radio-buttons">
{#each types as type}
<Radio
disabled={originalName}
name="Relationship type"
value={type}
bind:group={selectedRelationshipType}>
<label for={type}>{type}</label>
</Radio>
{/each}
</div>
</div>
<Select label="Table" thin secondary bind:value={field.tableId}> <Select label="Table" thin secondary bind:value={field.tableId}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each tableOptions as table} {#each tableOptions as table}
<option value={table._id}>{table.name}</option> <option value={table._id}>{table.name}</option>
{/each} {/each}
</Select> </Select>
{#if relationshipOptions && relationshipOptions.length > 0}
<div>
<Label grey extraSmall>Define the relationship</Label>
<div class="radio-buttons">
{#each relationshipOptions as { value, name }}
<Radio
disabled={originalName}
name="Relationship type"
{value}
bind:group={field.relationshipType}>
<div class="radio-button-labels">
<label for={value}>{name.split('has')[0]}</label>
<label class="rel-type-center" for={value}>has</label>
<label for={value}>{name.split('has')[1]}</label>
</div>
</Radio>
{/each}
</div>
</div>
{/if}
<Input <Input
label={`Column Name in Other Table`} label={`Column Name in Other Table`}
thin thin
@ -282,15 +310,16 @@
title="Confirm Deletion" /> title="Confirm Deletion" />
<style> <style>
label {
display: grid;
place-items: center;
}
.radio-buttons { .radio-buttons {
display: flex;
gap: var(--spacing-m); gap: var(--spacing-m);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
.radio-buttons :global(> *) {
margin-top: var(--spacing-s);
width: 100%;
}
.actions { .actions {
display: grid; display: grid;
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);
@ -307,7 +336,17 @@
margin-right: auto; margin-right: auto;
} }
.hidden { .rel-type-center {
display: none; font-weight: 500;
color: var(--grey-6);
margin-right: 4px;
margin-left: 4px;
padding: 1px 3px 1px 3px;
background: var(--grey-3);
border-radius: 2px;
}
.radio-button-labels {
margin-top: 2px;
} }
</style> </style>

View File

@ -73,8 +73,8 @@
{categories} {categories}
{selectedCategory} /> {selectedCategory} />
{#if showDisplayName} {#if definition && definition.name}
<div class="instance-name">{$selectedComponent._instanceName}</div> <div class="instance-name">{definition.name}</div>
{/if} {/if}
<div class="component-props-container"> <div class="component-props-container">

View File

@ -123,3 +123,9 @@ export function isAutoColumnUserRelationship(subtype) {
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
) )
} }
export const RelationshipTypes = {
MANY_TO_MANY: "many-to-many",
ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one",
}

View File

@ -14,6 +14,7 @@ exports.FieldTypes = {
exports.RelationshipTypes = { exports.RelationshipTypes = {
ONE_TO_MANY: "one-to-many", ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one",
MANY_TO_MANY: "many-to-many", MANY_TO_MANY: "many-to-many",
} }

View File

@ -145,6 +145,27 @@ class LinkController {
return true return true
} }
/**
* Given two the field of this table, and the field of the linked table, this makes sure
* the state of relationship type is accurate on both.
*/
handleRelationshipType(field, linkedField) {
if (
!field.relationshipType ||
field.relationshipType === RelationshipTypes.MANY_TO_MANY
) {
linkedField.relationshipType = RelationshipTypes.MANY_TO_MANY
// make sure by default all are many to many (if not specified)
field.relationshipType = RelationshipTypes.MANY_TO_MANY
} else if (field.relationshipType === RelationshipTypes.MANY_TO_ONE) {
// Ensure that the other side of the relationship is locked to one record
linkedField.relationshipType = RelationshipTypes.ONE_TO_MANY
} else if (field.relationshipType === RelationshipTypes.ONE_TO_MANY) {
linkedField.relationshipType = RelationshipTypes.MANY_TO_ONE
}
return { field, linkedField }
}
// all operations here will assume that the table // all operations here will assume that the table
// this operation is related to has linked rows // this operation is related to has linked rows
/** /**
@ -317,34 +338,32 @@ class LinkController {
} catch (err) { } catch (err) {
continue continue
} }
const linkConfig = { const fields = this.handleRelationshipType(field, {
name: field.fieldName, name: field.fieldName,
type: FieldTypes.LINK, type: FieldTypes.LINK,
// these are the props of the table that initiated the link // these are the props of the table that initiated the link
tableId: table._id, tableId: table._id,
fieldName: fieldName, fieldName: fieldName,
} })
// update table schema after checking relationship types
schema[fieldName] = fields.field
const linkedField = fields.linkedField
if (field.autocolumn) { if (field.autocolumn) {
linkConfig.autocolumn = field.autocolumn linkedField.autocolumn = field.autocolumn
}
if (field.relationshipType) {
// Ensure that the other side of the relationship is locked to one record
linkConfig.relationshipType = field.relationshipType
delete field.relationshipType
} }
// check the linked table to make sure we aren't overwriting an existing column // check the linked table to make sure we aren't overwriting an existing column
const existingSchema = linkedTable.schema[field.fieldName] const existingSchema = linkedTable.schema[field.fieldName]
if ( if (
existingSchema != null && existingSchema != null &&
!this.areSchemasEqual(existingSchema, linkConfig) !this.areSchemasEqual(existingSchema, linkedField)
) { ) {
throw new Error("Cannot overwrite existing column.") throw new Error("Cannot overwrite existing column.")
} }
// create the link field in the other table // create the link field in the other table
linkedTable.schema[field.fieldName] = linkConfig linkedTable.schema[field.fieldName] = linkedField
const response = await this._db.put(linkedTable) const response = await this._db.put(linkedTable)
// special case for when linking back to self, make sure rev updated // special case for when linking back to self, make sure rev updated
if (linkedTable._id === table._id) { if (linkedTable._id === table._id) {