Merge pull request #1236 from Budibase/feature/one-many-relationship-changes
Feature/one many relationship changes
This commit is contained in:
commit
46fe177766
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue