Browse Source

feat: custom relation - WIP

pull/8367/head
Pranav C 4 months ago
parent
commit
6155444885
  1. 270
      packages/nc-gui/components/smartsheet/column/LinkAdvancedOptions.vue
  2. 53
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  3. 19
      packages/nocodb/src/helpers/columnHelpers.ts
  4. 154
      packages/nocodb/src/services/columns.service.ts

270
packages/nc-gui/components/smartsheet/column/LinkAdvancedOptions.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow, useVModel } from '#imports'
import {ModelTypes} from "nocodb-sdk";
import { useColumnCreateStoreOrThrow, useVModel, computed, inject, MetaInj, ref, useI18n,useBase, storeToRefs, useMetas } from '#imports'
import {isVirtualCol, ModelTypes, RelationTypes, UITypes} from "nocodb-sdk";
import type {ColumnType} from "nocodb-sdk";
const props = defineProps<{
value: any
@ -10,6 +11,8 @@ const emit = defineEmits(['update:value'])
const { t } = useI18n()
const meta = inject(MetaInj, ref())
const vModel = useVModel(props, 'value', emit)
const { validateInfos, setAdditionalValidations, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -17,80 +20,11 @@ const { validateInfos, setAdditionalValidations, onDataTypeChange } = useColumnC
const baseStore = useBase()
const { tables } = storeToRefs(baseStore)
setAdditionalValidations({
'custom.colId': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(t('msg.length59Required'))
}
resolve(true)
})
},
},
],
'custom.refTableId': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(t('msg.length59Required'))
}
resolve(true)
})
},
},
],
'custom.refColId': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(t('msg.length59Required'))
}
resolve(true)
})
},
},
],
'custom.juncTableId': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(t('msg.length59Required'))
}
resolve(true)
})
},
},
],
'custom.juncColId': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(t('msg.length59Required'))
}
resolve(true)
})
},
},
],
'custom.juncRefColId': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(t('msg.length59Required'))
}
resolve(true)
})
},
},
],
})
const { metas, getMeta } = useMetas()
const isMm = computed(() => vModel.value.type === RelationTypes.MANY_TO_MANY)
// set default value
vModel.value.custom = {
@ -107,51 +41,90 @@ const refTables = computed(() =>{
})
const columns = $computed(() => {
if (!tables || !tables.length) {
const columns = computed(() => {
if (!meta.value?.columns) {
return []
}
return meta.value?.columns;
return meta.value.columns;
})
const refTableColumns = $computed(() => {
if (!vModel.value.custom?.refTableId) {
const refTableColumns = computed(() => {
if (!vModel.value.custom?.ref_model_id || !metas.value[vModel.value.custom?.ref_model_id]) {
return []
}
return tables.find(table => table.id === vModel.value.custom?.refTableId)?.columns;
return metas.value[vModel.value.custom?.ref_model_id]?.columns.filter(
(c: ColumnType) => !isVirtualCol(c.uidt as UITypes),
)
})
const juncTableColumns = $computed(() => {
if (!vModel.value.custom?.juncTableId) {
const juncTableColumns = computed(() => {
if (!vModel.value.custom?.junc_model_id || !metas.value[vModel.value.custom?.junc_model_id]) {
return []
}
return tables.find(table => table.id === vModel.value.custom?.juncTableId)?.columns;
return metas.value[vModel.value.custom?.junc_model_id]?.columns.filter(
(c: ColumnType) => !isVirtualCol(c.uidt as UITypes),
)
})
const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase())
const onModelIdChange =async (modelId:string) =>{
await getMeta(modelId)
await onDataTypeChange()
}
</script>
<template>
<div>
<div v-if="validateInfos">
<div class="flex flex-row space-x-2">
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
:label="$t('labels.childTable')"
v-bind="validateInfos.childId"
label="Column"
v-bind="validateInfos['custom.column_id']"
>
<a-select
v-model:value="vModel.custom.refTableId"
v-model:value="vModel.custom.column_id"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
<a-select-option v-for="column of columns" :key="column.title" :value="column.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="column" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ column.title }}</template>
<span>{{ column.title }}</span>
</NcTooltip>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="flex flex-row space-x-2">
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
label="Ref table"
v-bind="validateInfos['custom.ref_model_id']"
>
<a-select
v-model:value="vModel.custom.ref_model_id"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onModelIdChange(vModel.custom.ref_model_id)"
>
<a-select-option v-for="table of tables" :key="table.title" :value="table.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500" />
@ -164,6 +137,123 @@ const filterOption = (value: string, option: { key: string }) => option.key.toLo
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
label="Ref column"
v-bind="validateInfos['custom.ref_column_id']"
>
<a-select
v-model:value="vModel.custom.ref_column_id"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<a-select-option v-for="column of refTableColumns" :key="column.title" :value="column.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="column" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ column.title }}</template>
<span>{{ column.title }}</span>
</NcTooltip>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<template v-if="isMm">
<div class="flex flex-row space-x-2">
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
label="Junction table"
v-bind="validateInfos['custom.junc_model_id']"
>
<a-select
v-model:value="vModel.custom.junc_model_id"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onModelIdChange(vModel.custom.ref_model_id)"
>
<a-select-option v-for="table of tables" :key="table.title" :value="table.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ table.title }}</template>
<span>{{ table.title }}</span>
</NcTooltip>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="flex flex-row space-x-2">
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
label="Column in jn table"
v-bind="validateInfos['custom.junc_column_id']"
>
<a-select
v-model:value="vModel.custom.junc_column_id"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<a-select-option v-for="column of juncTableColumns" :key="column.title" :value="column.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="column" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ column.title }}</template>
<span>{{ column.title }}</span>
</NcTooltip>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
label="Ref column in jn table"
v-bind="validateInfos['custom.junc_ref_column_id']"
>
<a-select
v-model:value="vModel.custom.junc_ref_column_id"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<a-select-option v-for="column of juncTableColumns" :key="column.title" :value="column.id">
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="column" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ column.title }}</template>
<span>{{ column.title }}</span>
</NcTooltip>
</div>
</a-select-option>
</a-select>
</a-form-item>
</div>
</template>
</div>
</template>

53
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -157,10 +157,53 @@ const handleUpdateRefTable = () => {
updateFieldName()
})
}
const oneToOneEnabled = ref(false)
const cusValidators = {
'custom.column_id': [
{ required: true, message: t('general.required') }
],
'custom.ref_model_id': [
{ required: true, message: t('general.required') }
],
'custom.ref_column_id': [
{ required: true, message: t('general.required') }
],
}
const cusJuncTableValidations = {
'custom.junc_model_id': [
{ required: true, message: t('general.required') }
],
'custom.junc_column_id': [
{ required: true, message: t('general.required') }
],
'custom.junc_ref_column_id': [
{ required: true, message: t('general.required') }
],
}
const onCustomSwitchToggle = () =>{
if(vModel.value?.is_custom_ltar)
setAdditionalValidations({
childId: [],
...cusValidators,
...(vModel.value.type === RelationTypes.MANY_TO_MANY ? cusJuncTableValidations : {})
})
else
setAdditionalValidations({
childId: [{ required: true, message: t('general.required') }],
})
}
</script>
<template>
<div class="w-full flex flex-col gap-4">
<div class="w-full flex flex-col mb-2 mt-4">
<div class="pb-2">
<a-switch v-model:checked="vModel.is_custom_ltar" size="small" name="Custom" @change="onCustomSwitchToggle"/> Custom
</div>
<div class="border-2 p-6">
<div class="flex flex-col gap-4">
<a-form-item :label="$t('labels.relationType')" v-bind="validateInfos.type" class="nc-ltar-relation-type">
<a-radio-group v-model:value="linkType" name="type" v-bind="validateInfos.type" :disabled="isEdit">
@ -186,6 +229,13 @@ const handleUpdateRefTable = () => {
</a-form-item>
<a-form-item class="flex w-full nc-ltar-child-table" v-bind="validateInfos.childId">
<LazySmartsheetColumnLinkAdvancedOptions v-if="vModel.is_custom_ltar" v-model:value="vModel" class="mt-2" />
<template v-else>
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
:label="$t('labels.childTable')"
v-bind="validateInfos.childId"
>
<a-select
v-model:value="referenceTableChildId"
show-search
@ -211,6 +261,7 @@ const handleUpdateRefTable = () => {
</a-select-option>
</a-select>
</a-form-item>
</template>
<div v-if="isEeUI" class="w-full flex-col">
<div class="flex gap-2 items-center" :class="{ 'mb-2': limitRecToView }">

19
packages/nocodb/src/helpers/columnHelpers.ts

@ -43,6 +43,8 @@ export async function createHmAndBtColumn(
columnMeta = null,
isLinks = false,
colExtra?: any,
parentColumn?: Column,
isCustom = false
) {
// save bt column
{
@ -60,7 +62,7 @@ export async function createHmAndBtColumn(
// db_type:
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
// if self referencing treat it as system field to hide from ui
@ -68,6 +70,10 @@ export async function createHmAndBtColumn(
fk_col_name: fkColName,
fk_index_name: fkColName,
...(type === 'bt' ? colExtra : {}),
meta: {
...(colExtra?.meta || {}),
custom: isCustom
}
});
}
// save hm column
@ -80,6 +86,7 @@ export async function createHmAndBtColumn(
...(columnMeta || {}),
plural: columnMeta?.plural || pluralize(child.title),
singular: columnMeta?.singular || singularize(child.title),
custom: isCustom
};
await Column.insert(context, {
@ -89,7 +96,7 @@ export async function createHmAndBtColumn(
type: 'hm',
fk_target_view_id: childView?.id,
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: child.id,
virtual,
system: isSystemCol,
@ -128,6 +135,8 @@ export async function createOOColumn(
isSystemCol = false,
columnMeta = null,
colExtra?: any,
parentColumn?: Column,
isCustom = false
) {
// save bt column
{
@ -144,7 +153,7 @@ export async function createOOColumn(
// Child View ID is given for relation from parent to child. not for child to parent
fk_target_view_id: null,
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
// if self referencing treat it as system field to hide from ui
@ -157,6 +166,7 @@ export async function createOOColumn(
// one-to-one relation is combination of both hm and bt to identify table which have
// foreign key column(similar to bt) we are adding a boolean flag `bt` under meta
bt: true,
custom: isCustom
},
});
}
@ -176,6 +186,7 @@ export async function createOOColumn(
...(columnMeta || {}),
plural: columnMeta?.plural || pluralize(child.title),
singular: columnMeta?.singular || singularize(child.title),
custom: isCustom
};
await Column.insert(context, {
@ -185,7 +196,7 @@ export async function createOOColumn(
type: 'oo',
fk_target_view_id: childView?.id,
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
fk_parent_column_id: parentColumn?.id || parent.primaryKey.id,
fk_related_model_id: child.id,
virtual,
system: isSystemCol,

154
packages/nocodb/src/services/columns.service.ts

@ -2178,6 +2178,7 @@ export class ColumnsService {
ncMeta,
);
const parentTable = await parentColumn.getModel(context, ncMeta);
const custom = column.meta?.custom;
switch (relationColOpt.type) {
case 'bt':
@ -2192,6 +2193,7 @@ export class ColumnsService {
parentTable,
sqlMgr,
ncMeta,
custom,
});
}
break;
@ -2206,6 +2208,7 @@ export class ColumnsService {
parentTable,
sqlMgr,
ncMeta,
custom,
});
}
break;
@ -2224,6 +2227,7 @@ export class ColumnsService {
ncMeta,
);
if (!custom) {
await this.deleteHmOrBtRelation(
context,
{
@ -2255,6 +2259,7 @@ export class ColumnsService {
},
true,
);
}
const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable(context, ncMeta)
.then((m) => m.getColumns(context, ncMeta));
@ -2287,8 +2292,9 @@ export class ColumnsService {
ncMeta,
);
if (mmTable) {
// delete bt columns in m2m table
if (!custom) {
if (mmTable) {
// delete bt columns in m2m table
await mmTable.getColumns(context, ncMeta);
for (const c of mmTable.columns) {
if (!isLinksOrLTAR(c.uidt)) continue;
@ -2348,6 +2354,7 @@ export class ColumnsService {
}
}
}
}
break;
}
}
@ -2450,6 +2457,7 @@ export class ColumnsService {
sqlMgr,
ncMeta = Noco.ncMeta,
virtual,
custom = false,
}: {
relationColOpt: LinkToAnotherRecordColumn;
source: Source;
@ -2460,11 +2468,13 @@ export class ColumnsService {
sqlMgr: SqlMgrv2;
ncMeta?: MetaService;
virtual?: boolean;
custom?: boolean;
},
ignoreFkDelete = false,
) => {
if (childTable) {
let foreignKeyName;
if (!custom) {
let foreignKeyName;
// if relationColOpt is not provided, extract it from child table
// and get the foreign key name for dropping the foreign key
@ -2507,6 +2517,7 @@ export class ColumnsService {
}
}
}
}
if (!relationColOpt) return;
const columnsInRelatedTable: Column[] = await relationColOpt
@ -2532,7 +2543,7 @@ export class ColumnsService {
// delete virtual columns
await Column.delete(context, relationColOpt.fk_column_id, ncMeta);
if (!ignoreFkDelete) {
if (!ignoreFkDelete && !custom) {
const cTable = await Model.getWithInfo(
context,
{
@ -2603,6 +2614,7 @@ export class ColumnsService {
sqlMgr,
ncMeta = Noco.ncMeta,
virtual,
custom = false,
}: {
relationColOpt: LinkToAnotherRecordColumn;
source: Source;
@ -2613,11 +2625,13 @@ export class ColumnsService {
sqlMgr: SqlMgrv2;
ncMeta?: MetaService;
virtual?: boolean;
custom?: boolean;
},
ignoreFkDelete = false,
) => {
if (childTable) {
let foreignKeyName;
if (!custom) {
let foreignKeyName;
// if relationColOpt is not provided, extract it from child table
// and get the foreign key name for dropping the foreign key
@ -2660,6 +2674,7 @@ export class ColumnsService {
}
}
}
}
if (!relationColOpt) return;
const columnsInRelatedTable: Column[] = await relationColOpt
@ -2685,7 +2700,7 @@ export class ColumnsService {
// delete virtual columns
await Column.delete(context, relationColOpt.fk_column_id, ncMeta);
if (!ignoreFkDelete) {
if (!ignoreFkDelete && !custom) {
const cTable = await Model.getWithInfo(
context,
{
@ -2754,8 +2769,131 @@ export class ColumnsService {
base: Base;
reuse?: ReusableParams;
colExtra?: any;
},
) {
}) {
if ((param.column as any).is_custom_ltar) {
validateParams(['custom'], param.column as any);
validateParams(
['column_id', 'ref_model_id', 'ref_column_id'],
(param.column as any).custom,
);
const ltarCustomPRops: {
column_id: string;
ref_model_id: string;
ref_column_id: string;
junc_model_id: string;
junc_column_id: string;
junc_ref_column_id: string;
} = (param.column as any).custom;
const child = await Model.get(ltarCustomPRops.ref_model_id);
const parent = await Model.get(param.tableId);
const childColumn = await Column.get({
colId: ltarCustomPRops.ref_column_id,
});
const parentColumn = await Column.get({
colId: ltarCustomPRops.column_id,
});
if (
(param.column as LinkToAnotherColumnReqType).type === 'hm' ||
(param.column as LinkToAnotherColumnReqType).type === 'bt'
) {
await createHmAndBtColumn(
child,
parent,
childColumn,
(param.column as LinkToAnotherColumnReqType).type as RelationTypes,
(param.column as LinkToAnotherColumnReqType).title,
null,
(param.column as LinkToAnotherColumnReqType).virtual,
null,
param.column['meta'],
true,
param.colExtra,
parentColumn,
true,
);
} else if ((param.column as LinkToAnotherColumnReqType).type === 'oo') {
await createOOColumn(
child,
parent,
childColumn,
(param.column as LinkToAnotherColumnReqType).type as RelationTypes,
(param.column as LinkToAnotherColumnReqType).title,
null,
(param.column as LinkToAnotherColumnReqType).virtual,
null,
param.column['meta'],
param.colExtra,
parentColumn,
true,
);
} else if ((param.column as LinkToAnotherColumnReqType).type === 'mm') {
await Column.insert({
title: getUniqueColumnAliasName(
await child.getColumns(),
pluralize(parent.title),
),
uidt: param.column.uidt,
type: 'mm',
// ref_db_alias
fk_model_id: child.id,
// db_type:
fk_child_column_id: childColumn.id,
fk_parent_column_id: parentColumn.id,
fk_mm_model_id: ltarCustomPRops.junc_model_id,
fk_mm_child_column_id: ltarCustomPRops.junc_ref_column_id,
fk_mm_parent_column_id: ltarCustomPRops.junc_column_id,
fk_related_model_id: parent.id,
virtual: (param.column as LinkToAnotherColumnReqType).virtual,
meta: {
plural: pluralize(parent.title),
singular: singularize(parent.title),
custom: true,
},
// if self referencing treat it as system field to hide from ui
system: parent.id === child.id,
});
await Column.insert({
title: getUniqueColumnAliasName(
await parent.getColumns(),
param.column.title ?? pluralize(child.title),
),
uidt: param.column.uidt,
type: 'mm',
fk_model_id: parent.id,
fk_mm_model_id: ltarCustomPRops.junc_model_id,
fk_mm_child_column_id: ltarCustomPRops.junc_column_id,
fk_mm_parent_column_id: ltarCustomPRops.junc_ref_column_id,
fk_child_column_id: parentColumn.id,
fk_parent_column_id: childColumn.id,
fk_related_model_id: child.id,
virtual: (param.column as LinkToAnotherColumnReqType).virtual,
meta: {
plural: param.column['meta']?.plural || pluralize(child.title),
singular:
param.column['meta']?.singular || singularize(child.title),
custom: true,
},
// column_order and view_id if provided
...param.colExtra,
});
}
return;
}
validateParams(['parentId', 'childId', 'type'], param.column);
const reuse = param.reuse ?? {};

Loading…
Cancel
Save