Browse Source

Merge pull request #6011 from nocodb/revert-5848-feat/ltar-rollup-on-creation

Revert "Feat: Links column type"
pull/6015/head
Pranav C 1 year ago committed by GitHub
parent
commit
32875a1278
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/assets/style.scss
  2. 1
      packages/nc-gui/components/dlg/AirtableImport.vue
  3. 4
      packages/nc-gui/components/erd/TableNode.vue
  4. 6
      packages/nc-gui/components/erd/View.vue
  5. 7
      packages/nc-gui/components/erd/utils.ts
  6. 10
      packages/nc-gui/components/smartsheet/Form.vue
  7. 17
      packages/nc-gui/components/smartsheet/Grid.vue
  8. 2
      packages/nc-gui/components/smartsheet/Pagination.vue
  9. 4
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  10. 11
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  11. 63
      packages/nc-gui/components/smartsheet/column/LinkOptions.vue
  12. 9
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  13. 34
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  14. 36
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  15. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  16. 7
      packages/nc-gui/components/smartsheet/header/Menu.vue
  17. 4
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  18. 1
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  19. 9
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  20. 7
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  21. 8
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  22. 29
      packages/nc-gui/components/tabs/Smartsheet.vue
  23. 121
      packages/nc-gui/components/virtual-cell/Links.vue
  24. 12
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  25. 21
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  26. 18
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  27. 12
      packages/nc-gui/composables/useColumnCreateStore.ts
  28. 4
      packages/nc-gui/composables/useMultiSelect/index.ts
  29. 6
      packages/nc-gui/composables/useSharedFormViewStore.ts
  30. 24
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  31. 4
      packages/nc-gui/composables/useTable.ts
  32. 1
      packages/nc-gui/lang/en.json
  33. 1
      packages/nc-gui/store/project.ts
  34. 6
      packages/nc-gui/utils/columnUtils.ts
  35. 4
      packages/nc-gui/utils/filterUtils.ts
  36. 6
      packages/nc-gui/utils/virtualCell.ts
  37. 107
      packages/noco-docs/content/en/setup-and-usages/links.md
  38. 2
      packages/noco-docs/content/en/setup-and-usages/lookup.md
  39. 1
      packages/nocodb-sdk/src/index.ts
  40. 8
      packages/nocodb-sdk/src/lib/Api.ts
  41. 11
      packages/nocodb-sdk/src/lib/UITypes.ts
  42. 51
      packages/nocodb/src/db/BaseModelSqlv2.ts
  43. 21
      packages/nocodb/src/db/conditionV2.ts
  44. 2
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  45. 3
      packages/nocodb/src/db/genRollupSelectv2.ts
  46. 2
      packages/nocodb/src/db/sortV2.ts
  47. 64
      packages/nocodb/src/helpers/columnHelpers.ts
  48. 9
      packages/nocodb/src/helpers/getAst.ts
  49. 81
      packages/nocodb/src/helpers/populateMeta.ts
  50. 38
      packages/nocodb/src/models/Column.ts
  51. 5
      packages/nocodb/src/models/LinkToAnotherRecordColumn.ts
  52. 39
      packages/nocodb/src/models/LinksColumn.ts
  53. 2
      packages/nocodb/src/models/Sort.ts
  54. 1
      packages/nocodb/src/models/View.ts
  55. 1
      packages/nocodb/src/models/index.ts
  56. 20
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  57. 6
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
  58. 8
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  59. 7
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  60. 8
      packages/nocodb/src/schema/swagger.json
  61. 3
      packages/nocodb/src/services/bases.service.ts
  62. 46
      packages/nocodb/src/services/columns.service.ts
  63. 7
      packages/nocodb/src/services/data-alias-nested.service.ts
  64. 53
      packages/nocodb/src/services/meta-diffs.service.ts
  65. 5
      packages/nocodb/src/services/public-metas.service.ts
  66. 13
      packages/nocodb/src/services/tables.service.ts
  67. 54
      packages/nocodb/tests/unit/factory/column.ts
  68. 113
      packages/nocodb/tests/unit/rest/tests/tableRow.test.ts
  69. 53
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts
  70. 3
      packages/nocodb/tests/unit/tsconfig.json
  71. 220
      tests/playwright/fixtures/expectedBaseDownloadData.txt
  72. 220
      tests/playwright/fixtures/expectedBaseDownloadDataPg.txt
  73. 8
      tests/playwright/fixtures/expectedData.txt
  74. 38
      tests/playwright/fixtures/expectedDataSqlite.txt
  75. 3
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  76. 18
      tests/playwright/pages/Dashboard/Grid/index.ts
  77. 75
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  78. 4
      tests/playwright/quickTests/commonTest.ts
  79. 6
      tests/playwright/tests/db/columns/columnAttachments.spec.ts
  80. 2
      tests/playwright/tests/db/columns/columnBarcode.spec.ts
  81. 6
      tests/playwright/tests/db/columns/columnFormula.spec.ts
  82. 46
      tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts
  83. 14
      tests/playwright/tests/db/columns/columnLookupRollup.spec.ts
  84. 8
      tests/playwright/tests/db/columns/columnLtarDragdrop.spec.ts
  85. 6
      tests/playwright/tests/db/columns/columnMenuOperations.spec.ts
  86. 2
      tests/playwright/tests/db/columns/columnQrCode.spec.ts
  87. 15
      tests/playwright/tests/db/columns/columnRelationalExtendedTests.spec.ts
  88. 18
      tests/playwright/tests/db/features/erd.spec.ts
  89. 4
      tests/playwright/tests/db/features/expandedFormUrl.spec.ts
  90. 40
      tests/playwright/tests/db/features/filters.spec.ts
  91. 2
      tests/playwright/tests/db/features/keyboardShortcuts.spec.ts
  92. 52
      tests/playwright/tests/db/features/metaLTAR.spec.ts
  93. 4
      tests/playwright/tests/db/features/mobileMode.spec.ts
  94. 40
      tests/playwright/tests/db/features/undo-redo.spec.ts
  95. 29
      tests/playwright/tests/db/features/webhook.spec.ts
  96. 8
      tests/playwright/tests/db/general/cellSelection.spec.ts
  97. 6
      tests/playwright/tests/db/general/megaTable.spec.ts
  98. 23
      tests/playwright/tests/db/views/viewForm.spec.ts
  99. 2
      tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts
  100. 30
      tests/playwright/tests/db/views/viewGridShare.spec.ts

2
packages/nc-gui/assets/style.scss

@ -331,5 +331,5 @@ a {
.nc-icon-transition { .nc-icon-transition {
@apply transform transition-transform !hover:(scale-115 text-shadow-sm) !active:(scale-100) @apply transform transition-transform !hover:(scale-115) !active:(scale-100)
} }

1
packages/nc-gui/components/dlg/AirtableImport.vue

@ -1,4 +1,3 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Card as AntCard } from 'ant-design-vue' import type { Card as AntCard } from 'ant-design-vue'
import { import {

4
packages/nc-gui/components/erd/TableNode.vue

@ -2,7 +2,7 @@
import type { NodeProps } from '@vue-flow/core' import type { NodeProps } from '@vue-flow/core'
import { Handle, Position, useVueFlow } from '@vue-flow/core' import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { LinkToAnotherRecordType } from 'nocodb-sdk' import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { NodeData } from './utils' import type { NodeData } from './utils'
import { MetaInj, computed, provide, refAutoReset, toRef, useNuxtApp, watch } from '#imports' import { MetaInj, computed, provide, refAutoReset, toRef, useNuxtApp, watch } from '#imports'
@ -86,7 +86,7 @@ watch(
:class="index + 1 === data.nonPkColumns.length ? 'rounded-b-lg' : 'border-b-1'" :class="index + 1 === data.nonPkColumns.length ? 'rounded-b-lg' : 'border-b-1'"
> >
<div <div
v-if="isLinksOrLTAR(col)" v-if="col.uidt === UITypes.LinkToAnotherRecord"
class="flex w-full" class="flex w-full"
:class="`nc-erd-table-node-${table.table_name}-column-${col.title?.toLowerCase().replace(' ', '_')}`" :class="`nc-erd-table-node-${table.table_name}-column-${col.title?.toLowerCase().replace(' ', '_')}`"
> >

6
packages/nc-gui/components/erd/View.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isLinksOrLTAR } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { ERDConfig } from './utils' import type { ERDConfig } from './utils'
import { reactive, ref, storeToRefs, useMetas, useProject, watch } from '#imports' import { reactive, ref, storeToRefs, useMetas, useProject, watch } from '#imports'
@ -42,7 +42,9 @@ const populateTables = async () => {
(t) => (t) =>
t.id === props.table?.id || t.id === props.table?.id ||
props.table?.columns?.find( props.table?.columns?.find(
(column) => isLinksOrLTAR(column.uidt) && (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id, (column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,
), ),
) )
} else { } else {

7
packages/nc-gui/components/erd/utils.ts

@ -1,5 +1,5 @@
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre' import dagre from 'dagre'
import type { Edge, EdgeMarker, Elements, Node } from '@vue-flow/core' import type { Edge, EdgeMarker, Elements, Node } from '@vue-flow/core'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
@ -73,7 +73,8 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ER
const relations = computed(() => const relations = computed(() =>
erdTables.value.reduce((acc, table) => { erdTables.value.reduce((acc, table) => {
const meta = metasWithIdAsKey.value[table.id!] const meta = metasWithIdAsKey.value[table.id!]
const columns = meta.columns?.filter((column: ColumnType) => isLinksOrLTAR(column) && column.system !== 1) || [] const columns =
meta.columns?.filter((column: ColumnType) => column.uidt === UITypes.LinkToAnotherRecord && column.system !== 1) || []
columns.forEach((column: ColumnType) => { columns.forEach((column: ColumnType) => {
const colOptions = column.colOptions as LinkToAnotherRecordType const colOptions = column.colOptions as LinkToAnotherRecordType
@ -174,7 +175,7 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ER
const columns = const columns =
metasWithIdAsKey.value[table.id].columns?.filter( metasWithIdAsKey.value[table.id].columns?.filter(
(col) => config.showAllColumns || (!config.showAllColumns && isLinksOrLTAR(col)), (col) => config.showAllColumns || (!config.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord),
) || [] ) || []
const pkAndFkColumns = columns.filter(() => config.showPkAndFk).filter((col) => col.pk || col.uidt === UITypes.ForeignKey) const pkAndFkColumns = columns.filter(() => config.showPkAndFk).filter((col) => col.pk || col.uidt === UITypes.ForeignKey)

10
packages/nc-gui/components/smartsheet/Form.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { RelationTypes, UITypes, ViewTypes, getSystemColumns, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, ViewTypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
IsFormInj, IsFormInj,
@ -148,7 +148,7 @@ function isDbRequired(column: Record<string, any>) {
// confirm it's not foreign key // confirm it's not foreign key
!columns.value.some( !columns.value.some(
(c: Record<string, any>) => (c: Record<string, any>) =>
isLinksOrLTAR(c.uidt) && c.uidt === UITypes.LinkToAnotherRecord &&
c?.colOptions?.type === RelationTypes.BELONGS_TO && c?.colOptions?.type === RelationTypes.BELONGS_TO &&
column.fk_column_id === c.colOptions.fk_child_column_id, column.fk_column_id === c.colOptions.fk_child_column_id,
)) || )) ||
@ -297,7 +297,11 @@ function setFormData() {
function isRequired(_columnObj: Record<string, any>, required = false) { function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj let columnObj = _columnObj
if (isLinksOrLTAR(columnObj.uidt) && columnObj.colOptions && columnObj.colOptions.type === RelationTypes.BELONGS_TO) { if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = columns.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record< columnObj = columns.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
string, string,
any any

17
packages/nc-gui/components/smartsheet/Grid.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
@ -504,11 +504,12 @@ watch(contextMenu, () => {
const rowRefs = $ref<any[]>() const rowRefs = $ref<any[]>()
async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = false) { async function clearCell(ctx: { row: number; col: number } | null, skipUpdate = false) {
if (!ctx || !hasEditPermission || (!isLinksOrLTAR(fields.value[ctx.col]) && isVirtualCol(fields.value[ctx.col]))) return if (
!ctx ||
if (fields.value[ctx.col]?.uidt === UITypes.Links) { !hasEditPermission ||
return message.info('Links column clear is not supported yet') (fields.value[ctx.col].uidt !== UITypes.LinkToAnotherRecord && isVirtualCol(fields.value[ctx.col]))
} )
return
const rowObj = data.value[ctx.row] const rowObj = data.value[ctx.row]
const columnObj = fields.value[ctx.col] const columnObj = fields.value[ctx.col]
@ -867,7 +868,6 @@ eventBus.on(async (event, payload) => {
const closeAddColumnDropdown = (scrollToLastCol = false) => { const closeAddColumnDropdown = (scrollToLastCol = false) => {
columnOrder.value = null columnOrder.value = null
addColumnDropdown.value = false addColumnDropdown.value = false
preloadColumn.value = {}
if (scrollToLastCol) { if (scrollToLastCol) {
setTimeout(() => { setTimeout(() => {
const lastAddNewRowHeader = tableHeadEl.value?.querySelector('th:last-child') const lastAddNewRowHeader = tableHeadEl.value?.querySelector('th:last-child')
@ -1268,7 +1268,8 @@ useEventListener(document, 'mouseup', () => {
v-if=" v-if="
contextMenuTarget && contextMenuTarget &&
selectedRange.isSingleCell() && selectedRange.isSingleCell() &&
(isLinksOrLTAR(fields[contextMenuTarget.col]) || !isVirtualCol(fields[contextMenuTarget.col])) (fields[contextMenuTarget.col].uidt === UITypes.LinkToAnotherRecord ||
!isVirtualCol(fields[contextMenuTarget.col]))
" "
@click="clearCell(contextMenuTarget)" @click="clearCell(contextMenuTarget)"
> >

2
packages/nc-gui/components/smartsheet/Pagination.vue

@ -5,8 +5,6 @@ const props = defineProps<{
alignCountOnRight?: boolean alignCountOnRight?: boolean
}>() }>()
const { alignCountOnRight } = props
const paginatedData = inject(PaginationDataInj)! const paginatedData = inject(PaginationDataInj)!
const changePage = inject(ChangePageInj)! const changePage = inject(ChangePageInj)!

4
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -14,7 +14,6 @@ import {
isCount, isCount,
isFormula, isFormula,
isHm, isHm,
isLink,
isLookup, isLookup,
isMm, isMm,
isQrCode, isQrCode,
@ -96,8 +95,7 @@ onUnmounted(() => {
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
> >
<template v-if="intersected"> <template v-if="intersected">
<LazyVirtualCellLinks v-if="isLink(column)" /> <LazyVirtualCellHasMany v-if="isHm(column)" />
<LazyVirtualCellHasMany v-else-if="isHm(column)" />
<LazyVirtualCellManyToMany v-else-if="isMm(column)" /> <LazyVirtualCellManyToMany v-else-if="isMm(column)" />
<LazyVirtualCellBelongsTo v-else-if="isBt(column)" /> <LazyVirtualCellBelongsTo v-else-if="isBt(column)" />
<LazyVirtualCellRollup v-else-if="isRollup(column)" /> <LazyVirtualCellRollup v-else-if="isRollup(column)" />

11
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -58,17 +58,15 @@ const advancedDbOptions = ref(false)
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber] const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup, UITypes.Links] const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const geoDataToggleCondition = (t: { name: UITypes }) => { const geoDataToggleCondition = (t: { name: UITypes }) => {
return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData) return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData)
} }
const showDeprecated = $ref(false)
const uiTypesOptions = computed<typeof uiTypes>(() => { const uiTypesOptions = computed<typeof uiTypes>(() => {
return [ return [
...uiTypes.filter((t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated)), ...uiTypes.filter((t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual)),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk) ...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [ ? [
{ {
@ -189,13 +187,11 @@ useEventListener('keydown', (e: KeyboardEvent) => {
:disabled="isKanban" :disabled="isKanban"
dropdown-class-name="nc-dropdown-column-type" dropdown-class-name="nc-dropdown-column-type"
@change="onUidtOrIdTypeChange" @change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
> >
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt"> <a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<component :is="opt.icon" class="text-grey" /> <component :is="opt.icon" class="text-grey" />
{{ opt.name }} {{ opt.name }}
<span v-if="opt.deprecated" class="!text-xs !text-gray-300">(Deprecated)</span>
</div> </div>
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -213,10 +209,9 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" /> <LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" /> <LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions <LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && (formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links)" v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState" v-model:value="formState"
/> />
<LazySmartsheetColumnLinkOptions v-if="isEdit && formState.uidt === UITypes.Links" v-model:value="formState" />
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" /> <LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<LazySmartsheetColumnSelectOptions <LazySmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"

63
packages/nc-gui/components/smartsheet/column/LinkOptions.vue

@ -1,63 +0,0 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow, useVModel } from '#imports'
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { validateInfos, setAdditionalValidations } = useColumnCreateStoreOrThrow()
setAdditionalValidations({
'meta.singular': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(new Error('The length exceeds the max 59 characters'))
}
resolve(true)
})
},
},
],
'meta.plural': [
{
validator: (_, value: string) => {
return new Promise((resolve, reject) => {
if (value?.length > 59) {
return reject(new Error('The length exceeds the max 59 characters'))
}
resolve(true)
})
},
},
],
})
// set default value
vModel.value.meta = {
singular: '',
plural: '',
...vModel.value.meta,
}
</script>
<template>
<a-row class="my-2" gutter="8">
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.singular']" label="Singular Label">
<a-input v-model:value="vModel.meta.singular" placeholder="Link" class="!w-full nc-link-singular" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.plural']" label="Plural Label">
<a-input v-model:value="vModel.meta.plural" placeholder="Links" class="!w-full nc-link-plural" />
</a-form-item>
</a-col>
</a-row>
</template>

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi, UITypes } from 'nocodb-sdk' import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useProject, useVModel } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline' import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
@ -50,8 +50,6 @@ const refTables = $computed(() => {
}) })
const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase()) const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase())
const isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</script> </script>
<template> <template>
@ -88,7 +86,7 @@ const isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</a-select> </a-select>
</a-form-item> </a-form-item>
</div> </div>
<template v-if="!isXcdbBase || isLinks"> <template v-if="!isXcdbBase">
<div <div
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end" class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions" @click="advancedOptions = !advancedOptions"
@ -99,8 +97,6 @@ const isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</div> </div>
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2"> <div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<LazySmartsheetColumnLinkOptions v-model:value="vModel" class="-my-2" />
<template v-if="!isXcdbBase">
<div class="flex flex-row space-x-2"> <div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')"> <a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select <a-select
@ -138,7 +134,6 @@ const isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</a-checkbox> </a-checkbox>
</a-form-item> </a-form-item>
</div> </div>
</template>
</div> </div>
</template> </template>
</div> </div>

34
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { getRelationName } from './utils'
import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
@ -34,7 +35,7 @@ const refTables = $computed(() => {
} }
const _refTables = meta.columns const _refTables = meta.columns
.filter((column) => isLinksOrLTAR(column) && !column.system && column.base_id === meta?.base_id) .filter((column) => column.uidt === UITypes.LinkToAnotherRecord && !column.system && column.base_id === meta?.base_id)
.map((column) => ({ .map((column) => ({
col: column.colOptions, col: column.colOptions,
column, column,
@ -49,9 +50,7 @@ const columns = $computed<ColumnType[]>(() => {
if (!selectedTable?.id) { if (!selectedTable?.id) {
return [] return []
} }
return metas[selectedTable.id].columns.filter( return metas[selectedTable.id].columns.filter((c: ColumnType) => !isSystemColumn(c) && c.id !== vModel.value.id)
(c: ColumnType) => !isSystemColumn(c) && c.id !== vModel.value.id && c.uidt !== UITypes.Links,
)
}) })
onMounted(() => { onMounted(() => {
@ -65,28 +64,22 @@ const onRelationColChange = () => {
vModel.value.fk_lookup_column_id = columns?.[0]?.id vModel.value.fk_lookup_column_id = columns?.[0]?.id
onDataTypeChange() onDataTypeChange()
} }
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
})
</script> </script>
<template> <template>
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4"> <div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2"> <div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id"> <a-form-item class="flex w-1/2 pb-2" :label="$t('labels.linkToAnotherRecord')" v-bind="validateInfos.fk_relation_column_id">
<a-select <a-select
v-model:value="vModel.fk_relation_column_id" v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table" dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onRelationColChange" @change="onRelationColChange"
> >
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id"> <a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex flex-row h-full pb-0.5 items-center max-w-full"> <div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs flex-shrink flex-grow-0 truncate">{{ table.column.title }}</div> <div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="flex-grow"></div> <div class="text-[0.65rem] text-gray-600">
<div class="text-[0.65rem] text-gray-600 nc-relation-details"> {{ getRelationName(table.col.type) }} {{ table.title || table.table_name }}
<span class="uppercase">{{ table.col.type }}</span> {{ table.title || table.table_name }}
</div> </div>
</div> </div>
</a-select-option> </a-select-option>
@ -101,10 +94,7 @@ const cellIcon = (column: ColumnType) =>
@change="onDataTypeChange" @change="onDataTypeChange"
> >
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id"> <a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
<div class="flex items-center -ml-1 font-semibold text-xs">
<component :is="cellIcon(column)" :column-meta="column" />
{{ column.title }} {{ column.title }}
</div>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -112,8 +102,4 @@ const cellIcon = (column: ColumnType) =>
</div> </div>
</template> </template>
<style scoped> <style scoped></style>
:deep(.ant-select-selector .ant-select-selection-item .nc-relation-details) {
@apply hidden;
}
</style>

36
packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UITypes } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { getRelationName } from './utils'
import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
@ -49,7 +50,10 @@ const refTables = $computed(() => {
const _refTables = meta.columns const _refTables = meta.columns
.filter( .filter(
(c) => (c) =>
isLinksOrLTAR(c) && (c.colOptions as LinkToAnotherRecordType).type !== 'bt' && !c.system && c.base_id === meta?.base_id, c.uidt === UITypes.LinkToAnotherRecord &&
(c.colOptions as LinkToAnotherRecordType).type !== 'bt' &&
!c.system &&
c.base_id === meta?.base_id,
) )
.map((c) => ({ .map((c) => ({
col: c.colOptions, col: c.colOptions,
@ -83,28 +87,22 @@ const onRelationColChange = () => {
vModel.value.fk_rollup_column_id = columns?.[0]?.id vModel.value.fk_rollup_column_id = columns?.[0]?.id
onDataTypeChange() onDataTypeChange()
} }
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
})
</script> </script>
<template> <template>
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4"> <div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2"> <div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id"> <a-form-item class="flex w-1/2 pb-2" :label="$t('labels.linkToAnotherRecord')" v-bind="validateInfos.fk_relation_column_id">
<a-select <a-select
v-model:value="vModel.fk_relation_column_id" v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table" dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onRelationColChange" @change="onRelationColChange"
> >
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id"> <a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex flex-row h-full pb-0.5 items-center max-w-full"> <div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs flex-shrink flex-grow-0 truncate">{{ table.column.title }}</div> <div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="flex-grow"></div> <div class="text-[0.65rem] text-gray-600">
<div class="text-[0.65rem] text-gray-600 nc-relation-details"> ({{ getRelationName(table.col.type) }} {{ table.title || table.table_name }})
<span class="uppercase">{{ table.col.type }}</span> {{ table.title || table.table_name }}
</div> </div>
</div> </div>
</a-select-option> </a-select-option>
@ -119,11 +117,7 @@ const cellIcon = (column: ColumnType) =>
@change="onDataTypeChange" @change="onDataTypeChange"
> >
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id"> <a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
<div class="flex items-center -ml-1 font-semibold text-xs">
<component :is="cellIcon(column)" :column-meta="column" />
{{ column.title }} {{ column.title }}
</div>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -142,9 +136,3 @@ const cellIcon = (column: ColumnType) =>
</a-form-item> </a-form-item>
</div> </div>
</template> </template>
<style scoped>
:deep(.ant-select-selector .ant-select-selection-item .nc-relation-details) {
@apply hidden;
}
</style>

4
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType, ViewType } from 'nocodb-sdk' import type { TableType, ViewType } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { import {
CellClickHookInj, CellClickHookInj,
@ -324,7 +324,7 @@ export default {
<div <div
v-for="(col, i) of fields" v-for="(col, i) of fields"
v-else v-else
v-show="!isVirtualCol(col) || !isNew || isLinksOrLTAR(col)" v-show="!isVirtualCol(col) || !isNew || col.uidt === UITypes.LinkToAnotherRecord"
:key="col.title" :key="col.title"
class="mt-2 py-2" class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`" :class="`nc-expand-col-${col.title}`"

7
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnReqType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnReqType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
ColumnInj, ColumnInj,
@ -66,7 +66,7 @@ const deleteColumn = () =>
await getMeta(meta?.value?.id as string, true) await getMeta(meta?.value?.id as string, true)
/** force-reload related table meta if deleted column is a LTAR and not linked to same table */ /** force-reload related table meta if deleted column is a LTAR and not linked to same table */
if (isLinksOrLTAR(column?.value) && column.value?.colOptions) { if (column?.value?.uidt === UITypes.LinkToAnotherRecord && column.value?.colOptions) {
await getMeta((column.value?.colOptions as LinkToAnotherRecordType).fk_related_model_id!, true) await getMeta((column.value?.colOptions as LinkToAnotherRecordType).fk_related_model_id!, true)
// reload tables if deleted column is mm and include m2m is true // reload tables if deleted column is mm and include m2m is true
@ -181,7 +181,6 @@ const duplicateColumn = async () => {
// construct column create payload // construct column create payload
switch (column?.value.uidt) { switch (column?.value.uidt) {
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
case UITypes.Links:
case UITypes.Lookup: case UITypes.Lookup:
case UITypes.Rollup: case UITypes.Rollup:
case UITypes.Formula: case UITypes.Formula:
@ -316,7 +315,7 @@ const hideField = async () => {
{{ $t('general.edit') }} {{ $t('general.edit') }}
</div> </div>
</a-menu-item> </a-menu-item>
<template v-if="!isLinksOrLTAR(column) || column.colOptions.type !== RelationTypes.BELONGS_TO"> <template v-if="column.uidt !== UITypes.LinkToAnotherRecord || column.colOptions.type !== RelationTypes.BELONGS_TO">
<a-divider class="!my-0" /> <a-divider class="!my-0" />
<a-menu-item @click="sortByColumn('asc')"> <a-menu-item @click="sortByColumn('asc')">
<div v-e="['a:field:sort', { dir: 'asc' }]" class="nc-column-insert-after nc-header-menu-item"> <div v-e="['a:field:sort', { dir: 'asc' }]" class="nc-column-insert-after nc-header-menu-item">

4
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -121,9 +121,7 @@ const closeAddColumnDropdown = () => {
<template #title> <template #title>
{{ tooltipMsg }} {{ tooltipMsg }}
</template> </template>
<span class="name" :class="{ 'truncate': !isForm, 'whitespace-pre-line': isForm }" :title="column.title"> <span class="name" style="white-space: pre-line" :title="column.title"> {{ column.title }}</span>
{{ column.title }}
</span>
</a-tooltip> </a-tooltip>
<span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span> <span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span>

1
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -22,7 +22,6 @@ import CountIcon from '~icons/mdi/counter'
const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => { const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
switch (column.uidt) { switch (column.uidt) {
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
case UITypes.Links:
switch ((column.colOptions as LinkToAnotherRecordType)?.type) { switch ((column.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:
return { icon: iconMap.mm, color: 'text-accent' } return { icon: iconMap.mm, color: 'text-accent' }

9
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumns } from '#imports' import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumns } from '#imports'
const { modelValue, isSort } = defineProps<{ const { modelValue, isSort } = defineProps<{
@ -25,9 +25,6 @@ const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const options = computed<SelectProps['options']>(() => const options = computed<SelectProps['options']>(() =>
meta.value?.columns meta.value?.columns
?.filter((c: ColumnType) => { ?.filter((c: ColumnType) => {
if (c.uidt === UITypes.Links) {
return true
}
if (isSystemColumn(metaColumnById?.value?.[c.id!])) { if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
return ( return (
/** if the field is used in filter, then show it anyway */ /** if the field is used in filter, then show it anyway */
@ -39,7 +36,9 @@ const options = computed<SelectProps['options']>(() =>
return false return false
} else if (isSort) { } else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */ /** ignore hasmany and manytomany relations if it's using within sort menu */
return !(isLinksOrLTAR(c) && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO) return !(
c.uidt === UITypes.LinkToAnotherRecord && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO
)
/** ignore virtual fields which are system fields ( mm relation ) and qr code fields */ /** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
} else { } else {
const isVirtualSystemField = c.colOptions && c.system const isVirtualSystemField = c.colOptions && c.system

7
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { import {
@ -81,7 +80,6 @@ const checkTypeFunctions = {
isInt, isInt,
isFloat, isFloat,
isTextArea, isTextArea,
isLinks: (col: ColumnType) => col.uidt === UITypes.Links,
} }
type FilterType = keyof typeof checkTypeFunctions type FilterType = keyof typeof checkTypeFunctions
@ -147,7 +145,6 @@ const componentMap: Partial<Record<FilterType, any>> = $computed(() => {
isDecimal: Decimal, isDecimal: Decimal,
isInt: Integer, isInt: Integer,
isFloat: Float, isFloat: Float,
isLinks: Integer,
} }
}) })
@ -164,7 +161,6 @@ const componentProps = $computed(() => {
case 'isPercent': case 'isPercent':
case 'isDecimal': case 'isDecimal':
case 'isFloat': case 'isFloat':
case 'isLinks':
case 'isInt': { case 'isInt': {
return { class: 'h-32px' } return { class: 'h-32px' }
} }
@ -180,8 +176,7 @@ const componentProps = $computed(() => {
const hasExtraPadding = $computed(() => { const hasExtraPadding = $computed(() => {
return ( return (
column.value && column.value &&
(column.value?.uidt === UITypes.Links || (isInt(column.value, abstractType) ||
isInt(column.value, abstractType) ||
isDate(column.value, abstractType) || isDate(column.value, abstractType) ||
isDateTime(column.value, abstractType) || isDateTime(column.value, abstractType) ||
isTime(column.value, abstractType) || isTime(column.value, abstractType) ||

8
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { UITypes, isSystemColumn } from 'nocodb-sdk' import { isSystemColumn } from 'nocodb-sdk'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -29,8 +29,10 @@ onClickOutside(searchDropdown, () => (isDropdownOpen.value = false))
const columns = computed(() => const columns = computed(() =>
(meta.value as TableType)?.columns (meta.value as TableType)?.columns
?.filter((column) => !isSystemColumn(column) && column?.uidt !== UITypes.Links) ?.filter((c) => {
?.map((column) => ({ return !isSystemColumn(c)
})
.map((column) => ({
value: column.id, value: column.id,
label: column.title, label: column.title,
column, column,

29
packages/nc-gui/components/tabs/Smartsheet.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
@ -71,13 +71,8 @@ const grid = ref()
const onDrop = async (event: DragEvent) => { const onDrop = async (event: DragEvent) => {
event.preventDefault() event.preventDefault()
try { try {
// extract the data from the event's data transfer object // Access the dropped data
const textData = event.dataTransfer?.getData('text/json') const data = JSON.parse(event.dataTransfer?.getData('text/json')!)
if (!textData) return
// parse the data
const data = JSON.parse(textData)
// Do something with the received data // Do something with the received data
// if dragged item is not from the same base, return // if dragged item is not from the same base, return
@ -96,24 +91,32 @@ const onDrop = async (event: DragEvent) => {
// if already a link column exists, create a new Lookup column // if already a link column exists, create a new Lookup column
const relationCol = parentMeta.columns?.find((c: ColumnType) => { const relationCol = parentMeta.columns?.find((c: ColumnType) => {
if (!isLinksOrLTAR(c)) return false if (c.uidt !== UITypes.LinkToAnotherRecord) return false
const ltarOptions = c.colOptions as LinkToAnotherRecordType const ltarOptions = c.colOptions as LinkToAnotherRecordType
return ltarOptions.fk_related_model_id === childMeta.id if (ltarOptions.type !== 'mm') {
return false
}
if (ltarOptions.fk_related_model_id === childMeta.id) {
return true
}
return false
}) })
if (relationCol) { if (relationCol) {
const lookupCol = childMeta.columns?.find((c) => c.pv) ?? childMeta.columns?.[0] const lookupCol = childMeta.columns?.find((c) => c.pv) ?? childMeta.columns?.[0]
grid.value?.openColumnCreate({ grid.value?.openColumnCreate({
uidt: UITypes.Lookup, uidt: UITypes.Lookup,
title: `${relationCol.title}Lookup`, title: `${data.title}Lookup`,
fk_relation_column_id: relationCol.id, fk_relation_column_id: relationCol.id,
fk_lookup_column_id: lookupCol?.id, fk_lookup_column_id: lookupCol?.id,
}) })
} else { } else {
grid.value?.openColumnCreate({ grid.value?.openColumnCreate({
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `${data.title}`, title: `${data.title}List`,
parentId: parentMeta.id, parentId: parentMeta.id,
childId: childMeta.id, childId: childMeta.id,
parentTable: parentMeta.title, parentTable: parentMeta.title,

121
packages/nc-gui/components/virtual-cell/Links.vue

@ -1,121 +0,0 @@
<script setup lang="ts">
import { computed } from '@vue/reactivity'
import type { ColumnType } from 'nocodb-sdk'
import { ref } from 'vue'
import type { Ref } from 'vue'
import { ActiveCellInj, CellValueInj, ColumnInj, IsUnderLookupInj, inject, useSelectedCellKeyupListener } from '#imports'
const value = inject(CellValueInj, ref(0))
const column = inject(ColumnInj)!
const row = inject(RowInj)!
const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
const isForm = inject(IsFormInj)
const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false)
const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission()
const { state, isNew } = useSmartsheetRowStoreOrThrow()
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
reloadRowTrigger.trigger,
)
const relatedTableDisplayColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
loadRelatedTableMeta()
const textVal = computed(() => {
const parsedValue = +value?.value || 0
if (!parsedValue) {
return 'Empty'
} else if (parsedValue === 1) {
return `1 ${column.value?.meta?.singular || 'Link'}`
} else {
return `${parsedValue} ${column.value?.meta?.plural || 'Links'}`
}
})
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
}
const openChildList = () => {
if (!isLocked.value) {
childListDlg.value = true
}
}
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
if (isLocked.value || listItemsDlg.value) return
childListDlg.value = true
e.stopPropagation()
break
}
})
const localCellValue = computed<any[]>(() => {
if (isNew.value) {
return state?.value?.[column?.value.title as string] ?? []
}
return []
})
</script>
<template>
<div class="flex w-full items-center nc-links-wrapper" @dblclick.stop="openChildList">
<template v-if="!isForm">
<div class="block flex-shrink truncate">
<component
:is="isLocked || isUnderLookup ? 'span' : 'a'"
:title="textVal"
class="text-center pl-3 nc-datatype-link"
:class="{ '!text-gray-300': !value }"
@click.stop.prevent="openChildList"
>
{{ textVal }}
</component>
</div>
<div class="flex-grow" />
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
v-if="!readOnly && isUIAllowed('xcDatatableEditable')"
icon="plus"
class="nc-icon-transition select-none !text-xxl nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus hover:text-shadow-md"
@click.stop="listItemsDlg = true"
/>
</div>
</template>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="relatedTableDisplayColumn" />
<LazyVirtualCellComponentsListChildItems
v-model="childListDlg"
:column="relatedTableDisplayColumn"
:cell-value="localCellValue"
@attach-record="onAttachRecord"
/>
</div>
</template>

12
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -9,6 +9,7 @@ import {
inject, inject,
isAttachment, isAttachment,
ref, ref,
renderValue,
useExpandedFormDetached, useExpandedFormDetached,
useLTARStoreOrThrow, useLTARStoreOrThrow,
} from '#imports' } from '#imports'
@ -18,11 +19,9 @@ interface Props {
item?: any item?: any
column: any column: any
showUnlinkButton: boolean showUnlinkButton: boolean
border?: boolean
readonly?: boolean
} }
const { value, item, column, showUnlinkButton, border = true, readonly: readonlyProp } = defineProps<Props>() const { value, item, column, showUnlinkButton } = defineProps<Props>()
const emit = defineEmits(['unlink']) const emit = defineEmits(['unlink'])
@ -41,7 +40,7 @@ const isLocked = inject(IsLockedInj, ref(false))
const { open } = useExpandedFormDetached() const { open } = useExpandedFormDetached()
function openExpandedForm() { function openExpandedForm() {
if (!readOnly.value && !isLocked.value && !readonlyProp) { if (!readOnly && !isLocked.value) {
open({ open({
isOpen: true, isOpen: true,
row: { row: item, rowMeta: {}, oldRow: { ...item } }, row: { row: item, rowMeta: {}, oldRow: { ...item } },
@ -85,8 +84,9 @@ export default {
class="min-w-max" class="min-w-max"
:class="{ :class="{
'px-1 rounded-full flex-1': !isAttachment(column), 'px-1 rounded-full flex-1': !isAttachment(column),
'border-gray-200 rounded border-1': 'border-gray-200 rounded border-1': ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(
border && ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(column.uidt), column.uidt,
),
}" }"
> >
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :virtual="true" :read-only="true" /> <LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :virtual="true" :read-only="true" />

21
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -13,6 +13,7 @@ import {
iconMap, iconMap,
inject, inject,
ref, ref,
renderValue,
useLTARStoreOrThrow, useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useVModel, useVModel,
@ -39,6 +40,7 @@ const {
childrenListPagination, childrenListPagination,
relatedTableDisplayValueProp, relatedTableDisplayValueProp,
unlink, unlink,
getRelatedTableRowId,
relatedTableMeta, relatedTableMeta,
} = useLTARStoreOrThrow() } = useLTARStoreOrThrow()
@ -107,7 +109,7 @@ const onClick = (row: Row) => {
:body-style="{ padding: 0 }" :body-style="{ padding: 0 }"
wrap-class-name="nc-modal-child-list" wrap-class-name="nc-modal-child-list"
> >
<div class="h-[min(max(calc(100vh_-_300px)_,350px),540px)] flex flex-col py-6"> <div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col py-6">
<div class="flex mb-4 items-center gap-2 px-12"> <div class="flex mb-4 items-center gap-2 px-12">
<div class="flex-1" /> <div class="flex-1" />
<component <component
@ -141,29 +143,26 @@ const onClick = (row: Row) => {
<a-card <a-card
v-for="(row, i) of childrenList?.list ?? state?.[colTitle] ?? []" v-for="(row, i) of childrenList?.list ?? state?.[colTitle] ?? []"
:key="i" :key="i"
class="nc-nested-list-item !my-2 hover:(!bg-gray-200/50 shadow-md)" class="!my-4 hover:(!bg-gray-200/50 shadow-md)"
@click="onClick(row)" @click="onClick(row)"
> >
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0"> <div class="flex-1 overflow-hidden min-w-0">
<VirtualCellComponentsItemChip <VirtualCellComponentsItemChip :value="row[relatedTableDisplayValueProp]" :column="props.column" />
:border="false" <span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
:value="row[relatedTableDisplayValueProp]"
:column="props.column"
/>
</div> </div>
<div v-if="!readonly" class="flex gap-2"> <div v-if="!readonly" class="flex gap-2">
<component <component
:is="iconMap.linkRemove" :is="iconMap.linkRemove"
class="!text-base text-grey hover:(!text-red-500) cursor-pointer nc-icon-transition" class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
data-testid="nc-child-list-icon-unlink" data-testid="nc-child-list-icon-unlink"
@click.stop="unlinkRow(row)" @click.stop="unlinkRow(row)"
/> />
<component <component
:is="iconMap.delete" :is="iconMap.delete"
v-if="!readonly && !isPublic" v-if="!readonly && !isPublic"
class="!text-base text-grey hover:(!text-red-500) cursor-pointer nc-icon-transition" class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
data-testid="nc-child-list-icon-delete" data-testid="nc-child-list-icon-delete"
@click.stop="deleteRelatedRow(row, unlinkIfNewRow)" @click.stop="deleteRelatedRow(row, unlinkIfNewRow)"
/> />
@ -209,8 +208,4 @@ const onClick = (row: Row) => {
:deep(.ant-pagination-item a) { :deep(.ant-pagination-item a) {
line-height: 21px !important; line-height: 21px !important;
} }
:deep(.nc-nested-list-item .ant-card-body) {
@apply !px-1 !py-0;
}
</style> </style>

18
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -12,6 +12,7 @@ import {
inject, inject,
isDrawerExist, isDrawerExist,
ref, ref,
renderValue,
useLTARStoreOrThrow, useLTARStoreOrThrow,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
@ -34,6 +35,7 @@ const {
childrenExcludedListPagination, childrenExcludedListPagination,
relatedTableDisplayValueProp, relatedTableDisplayValueProp,
link, link,
getRelatedTableRowId,
relatedTableMeta, relatedTableMeta,
meta, meta,
row, row,
@ -50,7 +52,6 @@ const selectedRowIndex = ref(0)
const isAltKeyDown = ref(false) const isAltKeyDown = ref(false)
const linkRow = async (row: Record<string, any>) => { const linkRow = async (row: Record<string, any>) => {
childrenExcludedList.value?.list?.splice(selectedRowIndex.value, 1)
if (isNew.value) { if (isNew.value) {
addLTARRef(row, column?.value as ColumnType) addLTARRef(row, column?.value as ColumnType)
saveRow!() saveRow!()
@ -58,7 +59,7 @@ const linkRow = async (row: Record<string, any>) => {
await link(row) await link(row)
} }
if (isAltKeyDown.value) { if (isAltKeyDown.value) {
if (!isNew.value) loadChildrenExcludedList() loadChildrenExcludedList()
} else { } else {
vModel.value = false vModel.value = false
} }
@ -197,7 +198,7 @@ watch(vModel, (nextVal) => {
:body-style="{ padding: 0 }" :body-style="{ padding: 0 }"
wrap-class-name="nc-modal-link-record" wrap-class-name="nc-modal-link-record"
> >
<div class="h-[min(max(calc(100vh_-_300px)_,350px),540px)] flex flex-col py-6"> <div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col py-6">
<div class="flex mb-4 items-center gap-2 px-12"> <div class="flex mb-4 items-center gap-2 px-12">
<a-input <a-input
ref="filterQueryRef" ref="filterQueryRef"
@ -224,7 +225,7 @@ watch(vModel, (nextVal) => {
v-for="(refRow, i) in childrenExcludedList?.list ?? []" v-for="(refRow, i) in childrenExcludedList?.list ?? []"
:key="i" :key="i"
:ref="selectedRowIndex === i ? activeRow : null" :ref="selectedRowIndex === i ? activeRow : null"
class="nc-nested-list-item !my-2 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group" class="!my-4 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
:class="{ 'nc-selected-row': selectedRowIndex === i }" :class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)" @click="linkRow(refRow)"
> >
@ -232,9 +233,10 @@ watch(vModel, (nextVal) => {
:value="refRow[relatedTableDisplayValueProp]" :value="refRow[relatedTableDisplayValueProp]"
:column="props.column" :column="props.column"
:show-unlink-button="false" :show-unlink-button="false"
:border="false"
readonly
/> />
<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1">
({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
</span>
</a-card> </a-card>
</div> </div>
@ -279,8 +281,4 @@ watch(vModel, (nextVal) => {
:deep(.nc-selected-row) { :deep(.nc-selected-row) {
@apply !ring; @apply !ring;
} }
:deep(.nc-nested-list-item .ant-card-body) {
@apply !px-1 !py-0;
}
</style> </style>

12
packages/nc-gui/composables/useColumnCreateStore.ts

@ -1,6 +1,6 @@
import rfdc from 'rfdc' import rfdc from 'rfdc'
import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form' import type { RuleObject } from 'ant-design-vue/es/form'
import { import {
@ -31,8 +31,8 @@ interface ValidationsObj {
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => { (meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const projectStore = useProject() const projectStore = useProject()
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc, getBaseType } = projectStore const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc } = projectStore
const { sqlUis } = storeToRefs(projectStore) const { project, sqlUis } = storeToRefs(projectStore)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -54,8 +54,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const isXcdbBase = computed(() => isXcdbBaseFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0])) const isXcdbBase = computed(() => isXcdbBaseFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const baseType = computed(() => getBaseType(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const idType = null const idType = null
const additionalValidations = ref<ValidationsObj>({}) const additionalValidations = ref<ValidationsObj>({})
@ -103,7 +101,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}) })
}, },
}, },
fieldLengthValidator(baseType.value || ClientType.MYSQL), fieldLengthValidator(project.value?.bases?.[0].type || ClientType.MYSQL),
], ],
uidt: [ uidt: [
{ {
@ -254,7 +252,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
await $api.dbTableColumn.create(meta.value?.id as string, { ...formState.value, ...columnPosition }) await $api.dbTableColumn.create(meta.value?.id as string, { ...formState.value, ...columnPosition })
/** if LTAR column then force reload related table meta */ /** if LTAR column then force reload related table meta */
if (isLinksOrLTAR(formState.value) && meta.value?.id !== formState.value.childId) { if (formState.value.uidt === UITypes.LinkToAnotherRecord && meta.value?.id !== formState.value.childId) {
getMeta(formState.value.childId, true).then(() => {}) getMeta(formState.value.childId, true).then(() => {})
} }

4
packages/nc-gui/composables/useMultiSelect/index.ts

@ -26,6 +26,7 @@ import {
useI18n, useI18n,
useMetas, useMetas,
useProject, useProject,
useUIPermission,
} from '#imports' } from '#imports'
const MAIN_MOUSE_PRESSED = 0 const MAIN_MOUSE_PRESSED = 0
@ -79,6 +80,9 @@ export function useMultiSelect(
() => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)), () => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)),
) )
const { isUIAllowed } = useUIPermission()
const hasEditPermission = $computed(() => isUIAllowed('xcDatatableEditable'))
function makeActive(row: number, col: number) { function makeActive(row: number, col: number) {
if (activeCell.row === row && activeCell.col === col) { if (activeCell.row === row && activeCell.col === col) {
return return

6
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -11,7 +11,7 @@ import type {
TableType, TableType,
ViewType, ViewType,
} from 'nocodb-sdk' } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk' import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vueuse/core' import { isString } from '@vueuse/core'
import { import {
SharedViewPasswordInj, SharedViewPasswordInj,
@ -74,7 +74,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const fieldRequired = (fieldName = 'Value') => helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required) const fieldRequired = (fieldName = 'Value') => helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required)
const formColumns = computed(() => const formColumns = computed(() =>
columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || isLinksOrLTAR(col.uidt)), columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || col.uidt === UITypes.LinkToAnotherRecord),
) )
const loadSharedView = async () => { const loadSharedView = async () => {
@ -151,7 +151,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
) { ) {
obj.localState[column.title!] = { required: fieldRequired(column.label || column.title) } obj.localState[column.title!] = { required: fieldRequired(column.label || column.title) }
} else if ( } else if (
isLinksOrLTAR(column) && column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions && column.colOptions &&
(column.colOptions as LinkToAnotherRecordType).type === RelationTypes.BELONGS_TO (column.colOptions as LinkToAnotherRecordType).type === RelationTypes.BELONGS_TO
) { ) {

24
packages/nc-gui/composables/useSmartsheetRowStore.ts

@ -1,4 +1,4 @@
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
@ -80,11 +80,11 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
try { try {
await $api.dbTableRow.nestedAdd( await $api.dbTableRow.nestedAdd(
NOCO, NOCO,
project.value.id as string, project.value.title as string,
metaValue?.id as string, metaValue?.title as string,
rowId, rowId,
type as 'mm' | 'hm', type as 'mm' | 'hm',
column.id as string, column.title as string,
relatedRowId, relatedRowId,
) )
} catch (e: any) { } catch (e: any) {
@ -96,7 +96,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
const syncLTARRefs = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}) => { const syncLTARRefs = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}) => {
const id = extractPkFromRow(row, metaValue?.columns as ColumnType[]) const id = extractPkFromRow(row, metaValue?.columns as ColumnType[])
for (const column of metaValue?.columns ?? []) { for (const column of metaValue?.columns ?? []) {
if (!isLinksOrLTAR(column)) continue if (column.uidt !== UITypes.LinkToAnotherRecord) continue
const colOptions = column.colOptions as LinkToAnotherRecordType const colOptions = column.colOptions as LinkToAnotherRecordType
@ -132,7 +132,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
// clear LTAR cell // clear LTAR cell
const clearLTARCell = async (column: ColumnType) => { const clearLTARCell = async (column: ColumnType) => {
try { try {
if (!column || !isLinksOrLTAR(column)) return if (!column || column.uidt !== UITypes.LinkToAnotherRecord) return
const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string] const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string]
@ -143,11 +143,11 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
if (!currentRow.value.row[column.title!]) return if (!currentRow.value.row[column.title!]) return
await $api.dbTableRow.nestedRemove( await $api.dbTableRow.nestedRemove(
NOCO, NOCO,
project.value.id as string, project.value.title as string,
meta.value?.id as string, meta.value?.title as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]), extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
'bt' as any, 'bt' as any,
column.id as string, column.title as string,
extractPkFromRow(currentRow.value.row[column.title!], relatedTableMeta?.columns as ColumnType[]), extractPkFromRow(currentRow.value.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
) )
currentRow.value.row[column.title!] = null currentRow.value.row[column.title!] = null
@ -155,11 +155,11 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
for (const link of (currentRow.value.row[column.title!] as Record<string, any>[]) || []) { for (const link of (currentRow.value.row[column.title!] as Record<string, any>[]) || []) {
await $api.dbTableRow.nestedRemove( await $api.dbTableRow.nestedRemove(
NOCO, NOCO,
project.value.id as string, project.value.title as string,
meta.value?.id as string, meta.value?.title as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]), extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
(<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm', (<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm',
column.id as string, column.title as string,
extractPkFromRow(link, relatedTableMeta?.columns as ColumnType[]), extractPkFromRow(link, relatedTableMeta?.columns as ColumnType[]),
) )
} }

4
packages/nc-gui/composables/useTable.ts

@ -1,5 +1,5 @@
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk' import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { import {
Modal, Modal,
SYSTEM_COLUMNS, SYSTEM_COLUMNS,
@ -86,7 +86,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?
async onOk() { async onOk() {
try { try {
const meta = (await getMeta(table.id as string, true)) as TableType const meta = (await getMeta(table.id as string, true)) as TableType
const relationColumns = meta?.columns?.filter((c) => isLinksOrLTAR(c) && !isSystemColumn(c)) const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c))
// Check if table has any relation columns and show notification // Check if table has any relation columns and show notification
// skip for xcdb base // skip for xcdb base

1
packages/nc-gui/lang/en.json

@ -289,7 +289,6 @@
"childTable": "Child table", "childTable": "Child table",
"childColumn": "Child column", "childColumn": "Child column",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "On Update", "onUpdate": "On Update",
"onDelete": "On Delete", "onDelete": "On Delete",
"account": "Account", "account": "Account",

1
packages/nc-gui/store/project.ts

@ -227,6 +227,5 @@ export const useProject = defineStore('projectStore', () => {
isXcdbBase, isXcdbBase,
hasEmptyOrNullFilters, hasEmptyOrNullFilters,
setProject, setProject,
getBaseType,
} }
}) })

6
packages/nc-gui/utils/columnUtils.ts

@ -5,16 +5,10 @@ import LinkVariant from '~icons/mdi/link-variant'
import ID from '~icons/mdi/identifier' import ID from '~icons/mdi/identifier'
const uiTypes = [ const uiTypes = [
{
name: UITypes.Links,
icon: iconMap.link,
virtual: 1,
},
{ {
name: UITypes.LinkToAnotherRecord, name: UITypes.LinkToAnotherRecord,
icon: iconMap.link, icon: iconMap.link,
virtual: 1, virtual: 1,
deprecated: 1,
}, },
{ {
name: UITypes.Lookup, name: UITypes.Lookup,

4
packages/nc-gui/utils/filterUtils.ts

@ -261,13 +261,13 @@ export const comparisonOpList = (
text: 'is blank', text: 'is blank',
value: 'blank', value: 'blank',
ignoreVal: true, ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup], excludedTypes: [UITypes.Checkbox],
}, },
{ {
text: 'is not blank', text: 'is not blank',
value: 'notblank', value: 'notblank',
ignoreVal: true, ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup], excludedTypes: [UITypes.Checkbox],
}, },
] ]

6
packages/nc-gui/utils/virtualCell.ts

@ -1,7 +1,8 @@
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
export const isLTAR = (uidt: string, colOptions: unknown): colOptions is LinkToAnotherRecordType => isLinksOrLTAR(uidt) export const isLTAR = (uidt: string, colOptions: unknown): colOptions is LinkToAnotherRecordType =>
uidt === UITypes.LinkToAnotherRecord
export const isHm = (column: ColumnType) => export const isHm = (column: ColumnType) =>
isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.HAS_MANY isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.HAS_MANY
@ -18,4 +19,3 @@ export const isFormula = (column: ColumnType) => column.uidt === UITypes.Formula
export const isQrCode = (column: ColumnType) => column.uidt === UITypes.QrCode export const isQrCode = (column: ColumnType) => column.uidt === UITypes.QrCode
export const isBarcode = (column: ColumnType) => column.uidt === UITypes.Barcode export const isBarcode = (column: ColumnType) => column.uidt === UITypes.Barcode
export const isCount = (column: ColumnType) => column.uidt === UITypes.Count export const isCount = (column: ColumnType) => column.uidt === UITypes.Count
export const isLink = (column: ColumnType) => column.uidt === UITypes.Links

107
packages/noco-docs/content/en/setup-and-usages/links.md

@ -1,107 +0,0 @@
---
title: "Links"
description: "Understanding Link Columns!"
position: 540
category: "Product"
menuTitle: "Links"
---
### Supported relationship types:
- One to many
- A Table record in first table is related to more than one record of second table. But second table record maps to only one entry of first table
- NocoDB refers to this category of relationship as **has many**
- For every **has many** relation defined, NocoDB augments **belongs to** relationship column in the adjacent table automatically
- Example: Country **has many** Cities. (other way mapping > City **belongs to** Country )
- Many to many
- A Table record in first table is related to more than one record of second table; second table record can also map to more than on record of first table.
- NocoDB refers to this category of relationship as **many to many**
- For every **many to many** relation defined between tables, NocoDB augments **many to many** relationship column in the other table automatically
- Example: Film **has many** Actors. Actor **has many** Films (works on many films)
Further details of relationship types can be found [here](https://afteracademy.com/blog/what-are-the-different-types-of-relationships-in-dbms)
From Release v0.110.0, table records can be connected through relationships using the **Links** column type.
It is important to note that, earlier supported column type **LinkToAnotherRecord** for creating relations is considered deprecated. While the old datatype is still supported for backward compatibility, it is no longer possible to create new fields of that type.
The main distinction between these two column types lies in how the contents are displayed within the cell when links are established between two tables. With the **LinkToAnotherRecord** column type, the cell displays the **Primary value** of the related records. On the other hand, the **Links** column type only shows the **count** of related records.
The decision to switch over to new column type was made to ensure better performance and scalability of the application, more so when the records & the number of records in the related table is large.
Child list to display the related records can be accessed by clicking on the link count displayed on the cell.
Except for the column type, the following procedures remain same as before
- Create, update & delete a relationship column,
- Link & unlink a record,
- Create a lookup and rollup columns
Workflow details are captured below.
## Adding a relationship
![Screenshot 2023-06-27 at 11 03 20 AM](https://github.com/nocodb/nocodb/assets/86527202/b3762fc8-4bba-42ef-8415-41428840ee0e)
1. Create column
Click on '+' button at end of column headers
2. Update column name
Input name in the text box provided
3. Select column type
Select Column type as "Links" from the drop-down menu
4. Choose relationship type
- 'Has Many' corresponds to the 'One-to-many' relationships
- 'Many To Many' corresponds to the 'Many-to-many' relationships
5. Select child table from drop down menu
6. Click on 'Save'
A new column will get created in both the parent table & child table
## Linking records
### 1. Open link record tab
Click on the '+' icon in corresponding row - cell
![Screenshot 2023-06-27 at 11 06 52 AM](https://github.com/nocodb/nocodb/assets/86527202/96a90a6d-544e-4e43-b6d1-fe1aef784257)
### 2. Select from the option displayed
Use 'Filter box' to narrow down on search items.
You can opt to insert a new record as well, using "+ New Record" button.
You can use `ALT + Click` to insert multiple options together.
![Screenshot 2023-06-27 at 11 08 40 AM](https://github.com/nocodb/nocodb/assets/86527202/68246783-8d01-488b-8926-644fca8fa164)
### 3. Column display for "Has Many" relationship
Country 'has many' City
![Screenshot 2023-06-27 at 11 11 50 AM](https://github.com/nocodb/nocodb/assets/86527202/b5bb62b7-37ad-480e-8bf2-d666b775b07a)
### 4. Column display for "Belongs to" relationship [Automatically updated]
City 'belongs to' Country.
Note: Primary value is still used as cell display value for "Belongs to" as it can have only one associated record.
![Screenshot 2023-06-27 at 11 12 27 AM](https://github.com/nocodb/nocodb/assets/86527202/54e9ee75-4af5-49f6-8cd9-275dc53a8915)
## Unlinking records
1. Click on link count to open Child modal
2. Click on Unlink icon against required item
![Screenshot 2023-06-27 at 11 22 00 AM](https://github.com/nocodb/nocodb/assets/86527202/dad3cbc7-289d-45a7-9c49-a72264ed36b1)
## Link label reconfiguration
Use column edit menu for **Links** to reconfigure display label
![Screenshot 2023-06-27 at 11 16 19 AM](https://github.com/nocodb/nocodb/assets/86527202/1aabdd8c-7102-4917-b0c0-b72e1187b0b7)

2
packages/noco-docs/content/en/setup-and-usages/lookup.md

@ -11,7 +11,7 @@ menuTitle: "Lookup"
### Example organization structure ### Example organization structure
Consider an organization with Consider an organization with
- 5 departments (company departments), each department has a team name & associated team code. Each `Team` **has many** `Employees` - relationship has been defined using `LinkToAnotherRecord` or `Links`column - 5 departments (company departments), each department has a team name & associated team code. Each `Team` **has many** `Employees` - relationship has been defined using `LinkToAnotherRecord` column
- 5 employees working at different departments - 5 employees working at different departments

1
packages/nocodb-sdk/src/index.ts

@ -12,7 +12,6 @@ export {
numericUITypes, numericUITypes,
isNumericCol, isNumericCol,
isVirtualCol, isVirtualCol,
isLinksOrLTAR,
} from './lib/UITypes'; } from './lib/UITypes';
export { default as CustomAPI } from './lib/CustomAPI'; export { default as CustomAPI } from './lib/CustomAPI';
export { default as TemplateGenerator } from './lib/TemplateGenerator'; export { default as TemplateGenerator } from './lib/TemplateGenerator';

8
packages/nocodb-sdk/src/lib/Api.ts

@ -433,8 +433,7 @@ export interface ColumnType {
| 'Time' | 'Time'
| 'URL' | 'URL'
| 'Year' | 'Year'
| 'QrCode' | 'QrCode';
| 'Links';
/** Is Unsigned? */ /** Is Unsigned? */
un?: BoolType; un?: BoolType;
/** Is unique? */ /** Is unique? */
@ -1469,7 +1468,7 @@ export interface LinkToAnotherColumnReqType {
/** The type of the relationship */ /** The type of the relationship */
type: 'bt' | 'hm' | 'mm'; type: 'bt' | 'hm' | 'mm';
/** Abstract type of the relationship */ /** Abstract type of the relationship */
uidt: 'LinkToAnotherRecord' | 'Links'; uidt: 'LinkToAnotherRecord';
/** Is this relationship virtual? */ /** Is this relationship virtual? */
virtual?: BoolType; virtual?: BoolType;
} }
@ -1721,8 +1720,7 @@ export interface NormalColumnRequestType {
| 'Time' | 'Time'
| 'URL' | 'URL'
| 'Year' | 'Year'
| 'QrCode' | 'QrCode';
| 'Links';
/** Is this column unique? */ /** Is this column unique? */
un?: BoolType; un?: BoolType;
/** Is this column unique? */ /** Is this column unique? */

11
packages/nocodb-sdk/src/lib/UITypes.ts

@ -38,7 +38,6 @@ enum UITypes {
Barcode = 'Barcode', Barcode = 'Barcode',
QrCode = 'QrCode', QrCode = 'QrCode',
Button = 'Button', Button = 'Button',
Links = 'Links',
} }
export const numericUITypes = [ export const numericUITypes = [
@ -50,7 +49,6 @@ export const numericUITypes = [
UITypes.Rating, UITypes.Rating,
UITypes.Rollup, UITypes.Rollup,
UITypes.Year, UITypes.Year,
UITypes.Links,
]; ];
export function isNumericCol( export function isNumericCol(
@ -81,17 +79,8 @@ export function isVirtualCol(
UITypes.Barcode, UITypes.Barcode,
UITypes.Rollup, UITypes.Rollup,
UITypes.Lookup, UITypes.Lookup,
UITypes.Links,
// UITypes.Count, // UITypes.Count,
].includes(<UITypes>(typeof col === 'object' ? col?.uidt : col)); ].includes(<UITypes>(typeof col === 'object' ? col?.uidt : col));
} }
export function isLinksOrLTAR(
colOrUidt: ColumnType | { uidt: UITypes | string } | UITypes | string
) {
return [UITypes.LinkToAnotherRecord, UITypes.Links].includes(
<UITypes>(typeof colOrUidt === 'object' ? colOrUidt?.uidt : colOrUidt)
);
}
export default UITypes; export default UITypes;

51
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -8,7 +8,6 @@ import { nocoExecute } from 'nc-help';
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
AuditOperationTypes, AuditOperationTypes,
isLinksOrLTAR,
isSystemColumn, isSystemColumn,
isVirtualCol, isVirtualCol,
RelationTypes, RelationTypes,
@ -1367,23 +1366,16 @@ class BaseModelSqlv2 {
{ {
// @ts-ignore // @ts-ignore
const colOptions: LookupColumn = await column.getColOptions(); const colOptions: LookupColumn = await column.getColOptions();
const relCol = await Column.get({
colId: colOptions.fk_relation_column_id,
});
const relColTitle =
relCol.uidt === UITypes.Links
? `_nc_lk_${relCol.title}`
: relCol.title;
proto.__columnAliases[column.title] = { proto.__columnAliases[column.title] = {
path: [ path: [
relColTitle, (await Column.get({ colId: colOptions.fk_relation_column_id }))
?.title,
(await Column.get({ colId: colOptions.fk_lookup_column_id })) (await Column.get({ colId: colOptions.fk_lookup_column_id }))
?.title, ?.title,
], ],
}; };
} }
break; break;
case UITypes.Links:
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
{ {
this._columns[column.title] = column; this._columns[column.title] = column;
@ -1421,11 +1413,7 @@ class BaseModelSqlv2 {
}); });
const self: BaseModelSqlv2 = this; const self: BaseModelSqlv2 = this;
proto[ proto[column.title] = async function (args): Promise<any> {
column.uidt === UITypes.Links
? `_nc_lk_${column.title}`
: column.title
] = async function (args): Promise<any> {
(listLoader as any).args = args; (listLoader as any).args = args;
return listLoader.load( return listLoader.load(
getCompositePk(self.model.primaryKeys, this), getCompositePk(self.model.primaryKeys, this),
@ -1471,11 +1459,7 @@ class BaseModelSqlv2 {
const self: BaseModelSqlv2 = this; const self: BaseModelSqlv2 = this;
// const childColumn = await colOptions.getChildColumn(); // const childColumn = await colOptions.getChildColumn();
proto[ proto[column.title] = async function (args): Promise<any> {
column.uidt === UITypes.Links
? `_nc_lk_${column.title}`
: column.title
] = async function (args): Promise<any> {
(listLoader as any).args = args; (listLoader as any).args = args;
return await listLoader.load( return await listLoader.load(
getCompositePk(self.model.primaryKeys, this), getCompositePk(self.model.primaryKeys, this),
@ -1693,10 +1677,10 @@ class BaseModelSqlv2 {
`${alias || this.model.table_name}.${column.column_name}`, `${alias || this.model.table_name}.${column.column_name}`,
); );
break; break;
case UITypes.LinkToAnotherRecord: case 'LinkToAnotherRecord':
case UITypes.Lookup: case 'Lookup':
break; break;
case UITypes.QrCode: { case 'QrCode': {
const qrCodeColumn = await column.getColOptions<QrCodeColumn>(); const qrCodeColumn = await column.getColOptions<QrCodeColumn>();
const qrValueColumn = await Column.get({ const qrValueColumn = await Column.get({
colId: qrCodeColumn.fk_qr_value_column_id, colId: qrCodeColumn.fk_qr_value_column_id,
@ -1731,7 +1715,7 @@ class BaseModelSqlv2 {
break; break;
} }
case UITypes.Barcode: { case 'Barcode': {
const barcodeColumn = await column.getColOptions<BarcodeColumn>(); const barcodeColumn = await column.getColOptions<BarcodeColumn>();
const barcodeValueColumn = await Column.get({ const barcodeValueColumn = await Column.get({
colId: barcodeColumn.fk_barcode_value_column_id, colId: barcodeColumn.fk_barcode_value_column_id,
@ -1768,7 +1752,7 @@ class BaseModelSqlv2 {
break; break;
} }
case UITypes.Formula: case 'Formula':
{ {
try { try {
const selectQb = await this.getSelectQueryBuilderForFormula( const selectQb = await this.getSelectQueryBuilderForFormula(
@ -1792,8 +1776,7 @@ class BaseModelSqlv2 {
} }
} }
break; break;
case UITypes.Rollup: case 'Rollup':
case UITypes.Links:
qb.select( qb.select(
( (
await genRollupSelectv2({ await genRollupSelectv2({
@ -2124,8 +2107,8 @@ class BaseModelSqlv2 {
let rowId = null; let rowId = null;
const postInsertOps = []; const postInsertOps = [];
const nestedCols = (await this.model.getColumns()).filter((c) => const nestedCols = (await this.model.getColumns()).filter(
isLinksOrLTAR(c), (c) => c.uidt === UITypes.LinkToAnotherRecord,
); );
for (const col of nestedCols) { for (const col of nestedCols) {
@ -3159,10 +3142,7 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId); const column = columns.find((c) => c.id === colId);
if ( if (!column || column.uidt !== UITypes.LinkToAnotherRecord)
!column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
)
NcError.notFound('Column not found'); NcError.notFound('Column not found');
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();
@ -3281,10 +3261,7 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId); const column = columns.find((c) => c.id === colId);
if ( if (!column || column.uidt !== UITypes.LinkToAnotherRecord)
!column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
)
NcError.notFound('Column not found'); NcError.notFound('Column not found');
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>();

21
packages/nocodb/src/db/conditionV2.ts

@ -275,10 +275,7 @@ const parseConditionV2 = async (
return (_qb) => {}; return (_qb) => {};
} else if (column.uidt === UITypes.Lookup) { } else if (column.uidt === UITypes.Lookup) {
return await generateLookupCondition(column, filter, knex, aliasCount); return await generateLookupCondition(column, filter, knex, aliasCount);
} else if ( } else if (column.uidt === UITypes.Rollup && !customWhereClause) {
[UITypes.Rollup, UITypes.Links].includes(column.uidt) &&
!customWhereClause
) {
const builder = ( const builder = (
await genRollupSelectv2({ await genRollupSelectv2({
knex, knex,
@ -419,7 +416,6 @@ const parseConditionV2 = async (
UITypes.Decimal, UITypes.Decimal,
UITypes.Rating, UITypes.Rating,
UITypes.Rollup, UITypes.Rollup,
UITypes.Links,
].includes(column.uidt) ].includes(column.uidt)
) { ) {
qb = qb.where(field, val); qb = qb.where(field, val);
@ -446,14 +442,12 @@ const parseConditionV2 = async (
UITypes.Number, UITypes.Number,
UITypes.Decimal, UITypes.Decimal,
UITypes.Rollup, UITypes.Rollup,
UITypes.Links,
].includes(column.uidt) ].includes(column.uidt)
) { ) {
qb = qb.where((nestedQb) => { qb = qb.where((nestedQb) => {
nestedQb.whereNot(field, val); nestedQb
.whereNot(field, val)
if (column.uidt !== UITypes.Links) .orWhereNull(customWhereClause ? _val : _field);
nestedQb.orWhereNull(customWhereClause ? _val : _field);
}); });
} else if (column.uidt === UITypes.Rating) { } else if (column.uidt === UITypes.Rating) {
// unset rating is considered as NULL // unset rating is considered as NULL
@ -473,10 +467,9 @@ const parseConditionV2 = async (
} }
} else { } else {
qb = qb.where((nestedQb) => { qb = qb.where((nestedQb) => {
nestedQb.whereNot(field, val); nestedQb
.whereNot(field, val)
if (column.uidt !== UITypes.Links) .orWhereNull(customWhereClause ? _val : _field);
nestedQb.orWhereNull(customWhereClause ? _val : _field);
}); });
} }
break; break;

2
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -229,7 +229,6 @@ async function _formulaQueryBuilder(
} }
switch (lookupColumn.uidt) { switch (lookupColumn.uidt) {
case UITypes.Links:
case UITypes.Rollup: case UITypes.Rollup:
{ {
const builder = ( const builder = (
@ -416,7 +415,6 @@ async function _formulaQueryBuilder(
}; };
break; break;
case UITypes.Rollup: case UITypes.Rollup:
case UITypes.Links:
aliasToColumn[col.id] = async (): Promise<any> => { aliasToColumn[col.id] = async (): Promise<any> => {
const qb = await genRollupSelectv2({ const qb = await genRollupSelectv2({
knex, knex,

3
packages/nocodb/src/db/genRollupSelectv2.ts

@ -1,5 +1,4 @@
import { RelationTypes } from 'nocodb-sdk'; import { RelationTypes } from 'nocodb-sdk';
import type { LinksColumn } from '../models';
import type { RollupColumn } from '../models'; import type { RollupColumn } from '../models';
import type { XKnex } from '../db/CustomKnex'; import type { XKnex } from '../db/CustomKnex';
import type { LinkToAnotherRecordColumn } from '../models'; import type { LinkToAnotherRecordColumn } from '../models';
@ -14,7 +13,7 @@ export default async function ({
}: { }: {
knex: XKnex; knex: XKnex;
alias?: string; alias?: string;
columnOptions: RollupColumn | LinksColumn; columnOptions: RollupColumn;
}): Promise<{ builder: Knex.QueryBuilder | any }> { }): Promise<{ builder: Knex.QueryBuilder | any }> {
const relationColumn = await columnOptions.getRelationColumn(); const relationColumn = await columnOptions.getRelationColumn();
const relationColumnOption: LinkToAnotherRecordColumn = const relationColumnOption: LinkToAnotherRecordColumn =

2
packages/nocodb/src/db/sortV2.ts

@ -36,7 +36,6 @@ export default async function sortV2(
switch (column.uidt) { switch (column.uidt) {
case UITypes.Rollup: case UITypes.Rollup:
case UITypes.Links:
{ {
const builder = ( const builder = (
await genRollupSelectv2({ await genRollupSelectv2({
@ -122,7 +121,6 @@ export default async function sortV2(
} }
switch (lookupColumn.uidt) { switch (lookupColumn.uidt) {
case UITypes.Links:
case UITypes.Rollup: case UITypes.Rollup:
{ {
const builder = ( const builder = (

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

@ -1,11 +1,8 @@
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import { GridViewColumn } from '../models';
import Column from '../models/Column'; import Column from '../models/Column';
import { getUniqueColumnAliasName } from '../helpers/getUniqueName'; import { getUniqueColumnAliasName } from '../helpers/getUniqueName';
import validateParams from '../helpers/validateParams'; import validateParams from '../helpers/validateParams';
import type { RollupColumn } from '../models';
import type { import type {
BoolType, BoolType,
ColumnReqType, ColumnReqType,
@ -33,14 +30,12 @@ export async function createHmAndBtColumn(
fkColName?: string, fkColName?: string,
virtual: BoolType = false, virtual: BoolType = false,
isSystemCol = false, isSystemCol = false,
columnMeta = null,
isLinks = false,
) { ) {
// save bt column // save bt column
{ {
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(
await child.getColumns(), await child.getColumns(),
(type === 'bt' && alias) || `${parent.title}`, type === 'bt' ? alias : `${parent.title}`,
); );
await Column.insert<LinkToAnotherRecordColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
title, title,
@ -55,8 +50,7 @@ export async function createHmAndBtColumn(
fk_parent_column_id: parent.primaryKey.id, fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: parent.id, fk_related_model_id: parent.id,
virtual, virtual,
// if self referencing treat it as system field to hide from ui system: isSystemCol,
system: isSystemCol || parent.id === child.id,
fk_col_name: fkColName, fk_col_name: fkColName,
fk_index_name: fkColName, fk_index_name: fkColName,
}); });
@ -65,17 +59,12 @@ export async function createHmAndBtColumn(
{ {
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(
await parent.getColumns(), await parent.getColumns(),
(type === 'hm' && alias) || pluralize(child.title), type === 'hm' ? alias : `${child.title} List`,
); );
const meta = {
plural: columnMeta?.plural || pluralize(child.title),
singular: columnMeta?.singular || singularize(child.title),
};
await Column.insert({ await Column.insert({
title, title,
fk_model_id: parent.id, fk_model_id: parent.id,
uidt: isLinks ? UITypes.Links : UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
type: 'hm', type: 'hm',
fk_child_column_id: childColumn.id, fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id, fk_parent_column_id: parent.primaryKey.id,
@ -84,7 +73,6 @@ export async function createHmAndBtColumn(
system: isSystemCol, system: isSystemCol,
fk_col_name: fkColName, fk_col_name: fkColName,
fk_index_name: fkColName, fk_index_name: fkColName,
meta,
}); });
} }
} }
@ -218,47 +206,3 @@ export const generateFkName = (parent: TableType, child: TableType) => {
.slice(0, 10)}_${randomID(15)}`; .slice(0, 10)}_${randomID(15)}`;
return constraintName; return constraintName;
}; };
export async function populateRollupForLTAR({
column,
columnMeta,
alias,
}: {
column: Column;
columnMeta?: any;
alias?: string;
}) {
const model = await column.getModel();
const views = await model.getViews();
const relatedModel = await column
.getColOptions<LinkToAnotherRecordColumn>()
.then((colOpt) => colOpt.getRelatedTable());
await relatedModel.getColumns();
const pkId =
relatedModel.primaryKey?.id || (await relatedModel.getColumns())[0]?.id;
const meta = {
plural: columnMeta?.plural || pluralize(relatedModel.title),
singular: columnMeta?.singular || singularize(relatedModel.title),
};
await Column.insert<RollupColumn>({
uidt: UITypes.Links,
title: getUniqueColumnAliasName(
await model.getColumns(),
alias || `${relatedModel.title} Count`,
),
fk_rollup_column_id: pkId,
fk_model_id: model.id,
rollup_function: 'count',
fk_relation_column_id: column.id,
meta,
});
const viewCol = await GridViewColumn.list(views[0].id).then((cols) =>
cols.find((c) => c.fk_column_id === column.id),
);
await GridViewColumn.update(viewCol.id, { show: false });
}

9
packages/nocodb/src/helpers/getAst.ts

@ -89,12 +89,11 @@ const getAst = async ({
value = ast; value = ast;
// todo: include field relative to the relation => pk / fk // todo: include field relative to the relation => pk / fk
} else if (col.uidt === UITypes.Links) {
value = 1;
} else { } else {
value = ( value = (Array.isArray(fields) ? fields : fields.split(',')).reduce(
Array.isArray(nestedFields) ? nestedFields : nestedFields.split(',') (o, f) => ({ ...o, [f]: 1 }),
).reduce((o, f) => ({ ...o, [f]: 1 }), {}); {},
);
} }
} else if (col.uidt === UITypes.LinkToAnotherRecord) { } else if (col.uidt === UITypes.LinkToAnotherRecord) {
const model = await col const model = await col

81
packages/nocodb/src/helpers/populateMeta.ts

@ -1,8 +1,5 @@
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { isVirtualCol, RelationTypes } from 'nocodb-sdk'; import { isVirtualCol, RelationTypes } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { GridViewColumn } from '../models';
import Column from '../models/Column'; import Column from '../models/Column';
import Model from '../models/Model'; import Model from '../models/Model';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
@ -12,7 +9,6 @@ import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import getColumnUiType from '../helpers/getColumnUiType'; import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import { getUniqueColumnAliasName } from './getUniqueName'; import { getUniqueColumnAliasName } from './getUniqueName';
import type { RollupColumn } from '../models';
import type LinkToAnotherRecordColumn from '../models/LinkToAnotherRecordColumn'; import type LinkToAnotherRecordColumn from '../models/LinkToAnotherRecordColumn';
import type Base from '../models/Base'; import type Base from '../models/Base';
import type Project from '../models/Project'; import type Project from '../models/Project';
@ -114,7 +110,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
modelA.columns, modelA.columns,
pluralize(modelB.title), `${modelB.title} List`,
), ),
fk_model_id: modelA.id, fk_model_id: modelA.id,
fk_related_model_id: modelB.id, fk_related_model_id: modelB.id,
@ -125,18 +121,14 @@ export async function extractAndGenerateManyToManyRelations(
fk_mm_parent_column_id: fk_mm_parent_column_id:
belongsToCols[1].colOptions.fk_child_column_id, belongsToCols[1].colOptions.fk_child_column_id,
type: RelationTypes.MANY_TO_MANY, type: RelationTypes.MANY_TO_MANY,
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
meta: {
plural: pluralize(modelB.title),
singular: singularize(modelB.title),
},
}); });
} }
if (!isRelationAvailInB) { if (!isRelationAvailInB) {
await Column.insert<LinkToAnotherRecordColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
modelB.columns, modelB.columns,
pluralize(modelA.title), `${modelA.title} List`,
), ),
fk_model_id: modelB.id, fk_model_id: modelB.id,
fk_related_model_id: modelA.id, fk_related_model_id: modelA.id,
@ -147,11 +139,7 @@ export async function extractAndGenerateManyToManyRelations(
fk_mm_parent_column_id: fk_mm_parent_column_id:
belongsToCols[0].colOptions.fk_child_column_id, belongsToCols[0].colOptions.fk_child_column_id,
type: RelationTypes.MANY_TO_MANY, type: RelationTypes.MANY_TO_MANY,
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
meta: {
plural: pluralize(modelA.title),
singular: singularize(modelA.title),
},
}); });
} }
@ -163,7 +151,7 @@ export async function extractAndGenerateManyToManyRelations(
const model = await colOpt.getRelatedTable(); const model = await colOpt.getRelatedTable();
for (const col of await model.getColumns()) { for (const col of await model.getColumns()) {
if (!isLinksOrLTAR(col.uidt)) continue; if (col.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt1 = await col.getColOptions<LinkToAnotherRecordColumn>(); const colOpt1 = await col.getColOptions<LinkToAnotherRecordColumn>();
if (!colOpt1 || colOpt1.type !== RelationTypes.HAS_MANY) continue; if (!colOpt1 || colOpt1.type !== RelationTypes.HAS_MANY) continue;
@ -265,14 +253,10 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
const virtualColumns = [ const virtualColumns = [
...hasMany.map((hm) => { ...hasMany.map((hm) => {
return { return {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
type: 'hm', type: 'hm',
hm, hm,
title: pluralize(hm.title), title: `${hm.title} List`,
meta: {
plural: pluralize(hm.title),
singular: singularize(hm.title),
},
}; };
}), }),
...belongsTo.map((bt) => { ...belongsTo.map((bt) => {
@ -360,7 +344,6 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
order: colOrder++, order: colOrder++,
fk_related_model_id: column.hm ? tnId : rtnId, fk_related_model_id: column.hm ? tnId : rtnId,
system: column.system, system: column.system,
meta: column.meta,
}); });
// nested relations data apis // nested relations data apis
@ -452,53 +435,3 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
return info; return info;
} }
export async function populateRollupColumnAndHideLTAR(
base: Base,
project: Project,
) {
for (const model of await Model.list({
project_id: project.id,
base_id: base.id,
})) {
const columns = await model.getColumns();
const hmAndMmLTARColumns = columns.filter(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type !== RelationTypes.BELONGS_TO &&
!c.system,
);
const views = await model.getViews();
for (const column of hmAndMmLTARColumns) {
const relatedModel = await column
.getColOptions<LinkToAnotherRecordColumn>()
.then((colOpt) => colOpt.getRelatedTable());
await relatedModel.getColumns();
const pkId =
relatedModel.primaryKey?.id || (await relatedModel.getColumns())[0]?.id;
await Column.insert<RollupColumn>({
uidt: UITypes.Links,
title: getUniqueColumnAliasName(
await model.getColumns(),
`${relatedModel.title}`,
),
fk_rollup_column_id: pkId,
fk_model_id: model.id,
rollup_function: 'count',
fk_relation_column_id: column.id,
meta: {
singular: singularize(relatedModel.title),
plural: pluralize(relatedModel.title),
},
});
const viewCol = await GridViewColumn.list(views[0].id).then((cols) =>
cols.find((c) => c.fk_column_id === column.id),
);
await GridViewColumn.update(viewCol.id, { show: false });
}
}
}

38
packages/nocodb/src/models/Column.ts

@ -1,4 +1,4 @@
import { AllowedColumnTypesForQrAndBarcodes, isLinksOrLTAR, UITypes } from 'nocodb-sdk' import { AllowedColumnTypesForQrAndBarcodes, UITypes } from 'nocodb-sdk';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { import {
CacheDelDirection, CacheDelDirection,
@ -10,7 +10,6 @@ import Noco from '../Noco';
import addFormulaErrorIfMissingColumn from '../helpers/addFormulaErrorIfMissingColumn'; import addFormulaErrorIfMissingColumn from '../helpers/addFormulaErrorIfMissingColumn';
import { NcError } from '../helpers/catchError'; import { NcError } from '../helpers/catchError';
import { extractProps } from '../helpers/extractProps'; import { extractProps } from '../helpers/extractProps';
import { stringifyMetaProp } from '../utils/modelUtils';
import FormulaColumn from './FormulaColumn'; import FormulaColumn from './FormulaColumn';
import LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn';
import LookupColumn from './LookupColumn'; import LookupColumn from './LookupColumn';
@ -22,7 +21,6 @@ import Sort from './Sort';
import Filter from './Filter'; import Filter from './Filter';
import QrCodeColumn from './QrCodeColumn'; import QrCodeColumn from './QrCodeColumn';
import BarcodeColumn from './BarcodeColumn'; import BarcodeColumn from './BarcodeColumn';
import { LinksColumn } from './index';
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'; import type { ColumnReqType, ColumnType } from 'nocodb-sdk';
export default class Column<T = any> implements ColumnType { export default class Column<T = any> implements ColumnType {
@ -208,7 +206,6 @@ export default class Column<T = any> implements ColumnType {
); );
break; break;
} }
case UITypes.Links:
case UITypes.LinkToAnotherRecord: { case UITypes.LinkToAnotherRecord: {
await LinkToAnotherRecordColumn.insert( await LinkToAnotherRecordColumn.insert(
{ {
@ -425,9 +422,6 @@ export default class Column<T = any> implements ColumnType {
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
res = await LinkToAnotherRecordColumn.read(this.id, ncMeta); res = await LinkToAnotherRecordColumn.read(this.id, ncMeta);
break; break;
case UITypes.Links:
res = await LinksColumn.read(this.id, ncMeta);
break;
case UITypes.MultiSelect: case UITypes.MultiSelect:
res = await SelectOption.read(this.id, ncMeta); res = await SelectOption.read(this.id, ncMeta);
break; break;
@ -658,7 +652,7 @@ export default class Column<T = any> implements ColumnType {
} }
} }
// get rollup/links column and delete // get rollup column and delete
{ {
const cachedList = await NocoCache.getList(CacheScope.COL_ROLLUP, [id]); const cachedList = await NocoCache.getList(CacheScope.COL_ROLLUP, [id]);
let { list: rollups } = cachedList; let { list: rollups } = cachedList;
@ -705,7 +699,7 @@ export default class Column<T = any> implements ColumnType {
} }
// if relation column check lookup and rollup and delete // if relation column check lookup and rollup and delete
if (isLinksOrLTAR(col.uidt)) { if (col.uidt === UITypes.LinkToAnotherRecord) {
{ {
// get lookup columns using relation and delete // get lookup columns using relation and delete
const cachedList = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]); const cachedList = await NocoCache.getList(CacheScope.COL_LOOKUP, [id]);
@ -784,7 +778,6 @@ export default class Column<T = any> implements ColumnType {
cacheScopeName = CacheScope.COL_LOOKUP; cacheScopeName = CacheScope.COL_LOOKUP;
break; break;
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
case UITypes.Links:
colOptionTableName = MetaTable.COL_RELATIONS; colOptionTableName = MetaTable.COL_RELATIONS;
cacheScopeName = CacheScope.COL_RELATION; cacheScopeName = CacheScope.COL_RELATION;
break; break;
@ -1213,29 +1206,4 @@ export default class Column<T = any> implements ColumnType {
} }
return fieldLengthLimit; return fieldLengthLimit;
} }
static async updateMeta(
{ colId, meta }: { colId: string; meta: any },
ncMeta = Noco.ncMeta,
) {
// get existing cache
const key = `${CacheScope.COLUMN}:${colId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
// update meta
o.meta = meta;
// set cache
await NocoCache.set(key, o);
}
// set meta
await ncMeta.metaUpdate(
null,
null,
MetaTable.COLUMNS,
{
meta: stringifyMetaProp(meta),
},
colId,
);
}
} }

5
packages/nocodb/src/models/LinkToAnotherRecordColumn.ts

@ -7,8 +7,7 @@ import Column from './Column';
import type { BoolType } from 'nocodb-sdk'; import type { BoolType } from 'nocodb-sdk';
export default class LinkToAnotherRecordColumn { export default class LinkToAnotherRecordColumn {
id: string; fk_column_id?: string;
fk_column_id: string;
fk_child_column_id?: string; fk_child_column_id?: string;
fk_parent_column_id?: string; fk_parent_column_id?: string;
fk_mm_model_id?: string; fk_mm_model_id?: string;
@ -127,4 +126,6 @@ export default class LinkToAnotherRecordColumn {
} }
return colData ? new LinkToAnotherRecordColumn(colData) : null; return colData ? new LinkToAnotherRecordColumn(colData) : null;
} }
id: string;
} }

39
packages/nocodb/src/models/LinksColumn.ts

@ -1,39 +0,0 @@
import Noco from '../Noco';
import { Column, LinkToAnotherRecordColumn } from './';
import type { RollupColumn } from './';
export default class LinksColumn
extends LinkToAnotherRecordColumn
implements RollupColumn
{
rollup_function = 'count' as RollupColumn['rollup_function'];
get fk_relation_column_id() {
return this.fk_column_id;
}
get fk_rollup_column_id() {
if (this.type === 'hm') {
return this.fk_child_column_id;
} else if (this.type === 'mm') {
return this.fk_parent_column_id;
}
}
async getRelationColumn(ncMeta = Noco.ncMeta): Promise<Column> {
return await Column.get({ colId: this.fk_column_id }, ncMeta);
}
async getRollupColumn(ncMeta = Noco.ncMeta): Promise<Column> {
return await Column.get({ colId: this.fk_rollup_column_id }, ncMeta);
}
public static async read(columnId: string, ncMeta = Noco.ncMeta) {
const colData = await super.read(columnId, ncMeta);
return colData && new LinksColumn(colData);
}
public static async insert(data: Partial<LinksColumn>, ncMeta = Noco.ncMeta) {
const colData = await super.insert(data, ncMeta);
return colData && new LinksColumn(colData);
}
}

2
packages/nocodb/src/models/Sort.ts

@ -78,13 +78,13 @@ export default class Sort {
const row = await ncMeta.metaInsert2(null, null, MetaTable.SORT, insertObj); const row = await ncMeta.metaInsert2(null, null, MetaTable.SORT, insertObj);
if (sortObj.push_to_top) { if (sortObj.push_to_top) {
// todo: delete cache
const sortList = await ncMeta.metaList2(null, null, MetaTable.SORT, { const sortList = await ncMeta.metaList2(null, null, MetaTable.SORT, {
condition: { fk_view_id: sortObj.fk_view_id }, condition: { fk_view_id: sortObj.fk_view_id },
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
}); });
await NocoCache.delAll(CacheScope.SORT, `${sortObj.fk_view_id}:*`);
await NocoCache.setList(CacheScope.SORT, [sortObj.fk_view_id], sortList); await NocoCache.setList(CacheScope.SORT, [sortObj.fk_view_id], sortList);
} else { } else {
await NocoCache.appendToList( await NocoCache.appendToList(

1
packages/nocodb/src/models/View.ts

@ -1,3 +1,4 @@
import { title } from 'process';
import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk'; import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import Noco from '../Noco'; import Noco from '../Noco';
import { import {

1
packages/nocodb/src/models/index.ts

@ -34,4 +34,3 @@ export { default as SyncLogs } from './SyncLogs';
export { default as SyncSource } from './SyncSource'; export { default as SyncSource } from './SyncSource';
export { default as User } from './User'; export { default as User } from './User';
export { default as View } from './View'; export { default as View } from './View';
export { default as LinksColumn } from './LinksColumn';

20
packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts

@ -9,7 +9,6 @@ import utc from 'dayjs/plugin/utc';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import { Job } from 'bull';
import { isLinksOrLTAR } from 'nocodb-sdk';
import extractRolesObj from '../../../../utils/extractRolesObj'; import extractRolesObj from '../../../../utils/extractRolesObj';
import { AttachmentsService } from '../../../../services/attachments.service'; import { AttachmentsService } from '../../../../services/attachments.service';
import { ColumnsService } from '../../../../services/columns.service'; import { ColumnsService } from '../../../../services/columns.service';
@ -271,7 +270,7 @@ export class AtImportProcessor {
// base mapping table // base mapping table
const aTblNcTypeMap = { const aTblNcTypeMap = {
foreignKey: UITypes.Links, foreignKey: UITypes.LinkToAnotherRecord,
text: UITypes.SingleLineText, text: UITypes.SingleLineText,
multilineText: UITypes.LongText, multilineText: UITypes.LongText,
richText: UITypes.LongText, richText: UITypes.LongText,
@ -786,7 +785,7 @@ export class AtImportProcessor {
const ncTbl: any = await this.columnsService.columnAdd({ const ncTbl: any = await this.columnsService.columnAdd({
tableId: srcTableId, tableId: srcTableId,
column: { column: {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: ncName.title, title: ncName.title,
column_name: ncName.column_name, column_name: ncName.column_name,
parentId: srcTableId, parentId: srcTableId,
@ -868,14 +867,14 @@ export class AtImportProcessor {
updateMigrationSkipLog( updateMigrationSkipLog(
parentTblSchema?.title, parentTblSchema?.title,
ncLinkMappingTable[x].nc.title, ncLinkMappingTable[x].nc.title,
UITypes.Links, UITypes.LinkToAnotherRecord,
'Link error', 'Link error',
); );
continue; continue;
} }
// hack // fix me // hack // fix me
if (!isLinksOrLTAR(parentLinkColumn)) { if (parentLinkColumn.uidt !== 'LinkToAnotherRecord') {
parentLinkColumn = parentTblSchema.columns.find( parentLinkColumn = parentTblSchema.columns.find(
(col) => col.title === ncLinkMappingTable[x].nc.title + '_2', (col) => col.title === ncLinkMappingTable[x].nc.title + '_2',
); );
@ -889,7 +888,7 @@ export class AtImportProcessor {
// //
childLinkColumn = childTblSchema.columns.find( childLinkColumn = childTblSchema.columns.find(
(col) => (col) =>
isLinksOrLTAR(col) && col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions.fk_child_column_id === col.colOptions.fk_child_column_id ===
parentLinkColumn.colOptions.fk_child_column_id && parentLinkColumn.colOptions.fk_child_column_id &&
col.colOptions.fk_parent_column_id === col.colOptions.fk_parent_column_id ===
@ -901,7 +900,7 @@ export class AtImportProcessor {
// //
childLinkColumn = childTblSchema.columns.find( childLinkColumn = childTblSchema.columns.find(
(col) => (col) =>
isLinksOrLTAR(col) && col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions.fk_child_column_id === col.colOptions.fk_child_column_id ===
parentLinkColumn.colOptions.fk_parent_column_id && parentLinkColumn.colOptions.fk_parent_column_id &&
col.colOptions.fk_parent_column_id === col.colOptions.fk_parent_column_id ===
@ -1432,7 +1431,7 @@ export class AtImportProcessor {
// always process LTAR, Lookup, and Rollup columns as we delete the key after processing // always process LTAR, Lookup, and Rollup columns as we delete the key after processing
if ( if (
!value && !value &&
!isLinksOrLTAR(dt) && dt !== UITypes.LinkToAnotherRecord &&
dt !== UITypes.Lookup && dt !== UITypes.Lookup &&
dt !== UITypes.Rollup dt !== UITypes.Rollup
) { ) {
@ -1451,7 +1450,6 @@ export class AtImportProcessor {
// we will pick up LTAR once all table data's are in place // we will pick up LTAR once all table data's are in place
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
case UITypes.Links:
if (storeLinks) { if (storeLinks) {
if (ncLinkDataStore[table.title][record.id] === undefined) if (ncLinkDataStore[table.title][record.id] === undefined)
ncLinkDataStore[table.title][record.id] = { ncLinkDataStore[table.title][record.id] = {
@ -1972,7 +1970,9 @@ export class AtImportProcessor {
migrationStatsObj.aTbl.rollup = aTblRollup.length; migrationStatsObj.aTbl.rollup = aTblRollup.length;
const ncTbl = await nc_getTableSchema(aTblSchema[idx].name); const ncTbl = await nc_getTableSchema(aTblSchema[idx].name);
const linkColumn = ncTbl.columns.filter((x) => isLinksOrLTAR(x)); const linkColumn = ncTbl.columns.filter(
(x) => x.uidt === UITypes.LinkToAnotherRecord,
);
const lookup = ncTbl.columns.filter((x) => x.uidt === UITypes.Lookup); const lookup = ncTbl.columns.filter((x) => x.uidt === UITypes.Lookup);
const rollup = ncTbl.columns.filter((x) => x.uidt === UITypes.Rollup); const rollup = ncTbl.columns.filter((x) => x.uidt === UITypes.Rollup);

6
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

@ -2,8 +2,8 @@ import { Readable } from 'stream';
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import { Job } from 'bull';
import papaparse from 'papaparse'; import papaparse from 'papaparse';
import { UITypes } from 'nocodb-sdk';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { Base, Column, Model, Project } from '../../../../models'; import { Base, Column, Model, Project } from '../../../../models';
import { ProjectsService } from '../../../../services/projects.service'; import { ProjectsService } from '../../../../services/projects.service';
import { findWithIdentifier } from '../../../../helpers/exportImportHelpers'; import { findWithIdentifier } from '../../../../helpers/exportImportHelpers';
@ -137,7 +137,7 @@ export class DuplicateProcessor {
await sourceModel.getColumns(); await sourceModel.getColumns();
const relatedModelIds = sourceModel.columns const relatedModelIds = sourceModel.columns
.filter((col) => isLinksOrLTAR(col)) .filter((col) => col.uidt === UITypes.LinkToAnotherRecord)
.map((col) => col.colOptions.fk_related_model_id) .map((col) => col.colOptions.fk_related_model_id)
.filter((id) => id); .filter((id) => id);
@ -186,7 +186,7 @@ export class DuplicateProcessor {
const bts = md.columns const bts = md.columns
.filter( .filter(
(c) => (c) =>
isLinksOrLTAR(c) && c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type === 'bt' && c.colOptions.type === 'bt' &&
c.colOptions.fk_related_model_id === modelId, c.colOptions.fk_related_model_id === modelId,
) )

8
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -1,5 +1,5 @@
import { Readable } from 'stream'; import { Readable } from 'stream';
import { isLinksOrLTAR, UITypes, ViewTypes } from 'nocodb-sdk'; import { UITypes, ViewTypes } from 'nocodb-sdk';
import { unparse } from 'papaparse'; import { unparse } from 'papaparse';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import NcConnectionMgrv2 from '../../../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../../../utils/common/NcConnectionMgrv2';
@ -352,12 +352,14 @@ export class ExportService {
.map((c) => c.title) .map((c) => c.title)
.join(',') .join(',')
: model.columns : model.columns
.filter((c) => !isLinksOrLTAR(c)) .filter((c) => c.uidt !== UITypes.LinkToAnotherRecord)
.map((c) => c.title) .map((c) => c.title)
.join(','); .join(',');
const mmColumns = model.columns.filter( const mmColumns = model.columns.filter(
(col) => isLinksOrLTAR(col) && col.colOptions?.type === 'mm', (col) =>
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions?.type === 'mm',
); );
const hasLink = mmColumns.length > 0; const hasLink = mmColumns.length > 0;

7
packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts

@ -1,7 +1,6 @@
import { UITypes, ViewTypes } from 'nocodb-sdk'; import { UITypes, ViewTypes } from 'nocodb-sdk';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import papaparse from 'papaparse'; import papaparse from 'papaparse';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { import {
findWithIdentifier, findWithIdentifier,
generateUniqueName, generateUniqueName,
@ -126,7 +125,7 @@ export class ImportService {
const reducedColumnSet = modelData.columns.filter( const reducedColumnSet = modelData.columns.filter(
(a) => (a) =>
!isLinksOrLTAR(a) && a.uidt !== UITypes.LinkToAnotherRecord &&
a.uidt !== UITypes.Lookup && a.uidt !== UITypes.Lookup &&
a.uidt !== UITypes.Rollup && a.uidt !== UITypes.Rollup &&
a.uidt !== UITypes.Formula && a.uidt !== UITypes.Formula &&
@ -166,7 +165,9 @@ export class ImportService {
const modelData = data.model; const modelData = data.model;
const table = tableReferences.get(modelData.id); const table = tableReferences.get(modelData.id);
const linkedColumnSet = modelData.columns.filter((a) => isLinksOrLTAR(a)); const linkedColumnSet = modelData.columns.filter(
(a) => a.uidt === UITypes.LinkToAnotherRecord,
);
for (const col of linkedColumnSet) { for (const col of linkedColumnSet) {
if (col.colOptions) { if (col.colOptions) {

8
packages/nocodb/src/schema/swagger.json

@ -14894,8 +14894,7 @@
"Time", "Time",
"URL", "URL",
"Year", "Year",
"QrCode", "QrCode"
"Links"
], ],
"type": "string" "type": "string"
}, },
@ -17432,7 +17431,7 @@
"description": "The type of the relationship" "description": "The type of the relationship"
}, },
"uidt": { "uidt": {
"enum": ["LinkToAnotherRecord", "Links"], "enum": ["LinkToAnotherRecord"],
"type": "string", "type": "string",
"description": "Abstract type of the relationship" "description": "Abstract type of the relationship"
}, },
@ -17982,8 +17981,7 @@
"Time", "Time",
"URL", "URL",
"Year", "Year",
"QrCode", "QrCode"
"Links"
], ],
"type": "string", "type": "string",
"description": "UI Data Type" "description": "UI Data Type"

3
packages/nocodb/src/services/bases.service.ts

@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { T } from 'nc-help'; import { T } from 'nc-help';
import { populateMeta, validatePayload } from '../helpers'; import { populateMeta, validatePayload } from '../helpers';
import { populateRollupColumnAndHideLTAR } from '../helpers/populateMeta';
import { syncBaseMigration } from '../helpers/syncMigration'; import { syncBaseMigration } from '../helpers/syncMigration';
import { Base, Project } from '../models'; import { Base, Project } from '../models';
import type { BaseReqType } from 'nocodb-sdk'; import type { BaseReqType } from 'nocodb-sdk';
@ -70,8 +69,6 @@ export class BasesService {
const info = await populateMeta(base, project); const info = await populateMeta(base, project);
await populateRollupColumnAndHideLTAR(base, project);
T.emit('evt_api_created', info); T.emit('evt_api_created', info);
delete base.config; delete base.config;

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

@ -2,14 +2,12 @@ import { Injectable } from '@nestjs/common';
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
AuditOperationTypes, AuditOperationTypes,
isLinksOrLTAR,
isVirtualCol, isVirtualCol,
substituteColumnAliasWithIdInFormula, substituteColumnAliasWithIdInFormula,
substituteColumnIdWithAliasInFormula, substituteColumnIdWithAliasInFormula,
UITypes, UITypes,
} from 'nocodb-sdk'; } from 'nocodb-sdk';
import { T } from 'nc-help'; import { T } from 'nc-help';
import { pluralize, singularize } from 'inflection';
import formulaQueryBuilderv2 from '../db/formulav2/formulaQueryBuilderv2'; import formulaQueryBuilderv2 from '../db/formulav2/formulaQueryBuilderv2';
import ProjectMgrv2 from '../db/sql-mgr/v2/ProjectMgrv2'; import ProjectMgrv2 from '../db/sql-mgr/v2/ProjectMgrv2';
import { import {
@ -121,7 +119,6 @@ export class ColumnsService {
UITypes.QrCode, UITypes.QrCode,
UITypes.Barcode, UITypes.Barcode,
UITypes.ForeignKey, UITypes.ForeignKey,
UITypes.Links,
].includes(column.uidt) ].includes(column.uidt)
) { ) {
if (column.uidt === colBody.uidt) { if (column.uidt === colBody.uidt) {
@ -159,19 +156,11 @@ export class ColumnsService {
...column, ...column,
...colBody, ...colBody,
}); });
} else { } else if (colBody.title !== column.title) {
if (colBody.title !== column.title) {
await Column.updateAlias(param.columnId, { await Column.updateAlias(param.columnId, {
title: colBody.title, title: colBody.title,
}); });
} }
if ('meta' in colBody && column.uidt === UITypes.Links) {
await Column.updateMeta({
colId: param.columnId,
meta: colBody.meta,
});
}
}
await this.updateRollupOrLookup(colBody, column); await this.updateRollupOrLookup(colBody, column);
} else { } else {
NcError.notImplemented( NcError.notImplemented(
@ -933,7 +922,6 @@ export class ColumnsService {
} }
break; break;
case UITypes.Links:
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
await this.createLTARColumn({ ...param, base, project }); await this.createLTARColumn({ ...param, base, project });
T.emit('evt', { evt_type: 'relation:created' }); T.emit('evt', { evt_type: 'relation:created' });
@ -1182,8 +1170,6 @@ export class ColumnsService {
case UITypes.Formula: case UITypes.Formula:
await Column.delete(param.columnId, ncMeta); await Column.delete(param.columnId, ncMeta);
break; break;
// Since Links is just an extended version of LTAR, we can use the same logic
case UITypes.Links:
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
{ {
const relationColOpt = const relationColOpt =
@ -1254,7 +1240,7 @@ export class ColumnsService {
.then((m) => m.getColumns(ncMeta)); .then((m) => m.getColumns(ncMeta));
for (const c of columnsInRelatedTable) { for (const c of columnsInRelatedTable) {
if (!isLinksOrLTAR(c.uidt)) continue; if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta); await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if ( if (
@ -1275,7 +1261,7 @@ export class ColumnsService {
// delete bt columns in m2m table // delete bt columns in m2m table
await mmTable.getColumns(ncMeta); await mmTable.getColumns(ncMeta);
for (const c of mmTable.columns) { for (const c of mmTable.columns) {
if (!isLinksOrLTAR(c.uidt)) continue; if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta); await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOpt.type === 'bt') { if (colOpt.type === 'bt') {
@ -1286,7 +1272,7 @@ export class ColumnsService {
// delete hm columns in parent table // delete hm columns in parent table
await parentTable.getColumns(ncMeta); await parentTable.getColumns(ncMeta);
for (const c of parentTable.columns) { for (const c of parentTable.columns) {
if (!isLinksOrLTAR(c.uidt)) continue; if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta); await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOpt.fk_related_model_id === mmTable.id) { if (colOpt.fk_related_model_id === mmTable.id) {
@ -1297,7 +1283,7 @@ export class ColumnsService {
// delete hm columns in child table // delete hm columns in child table
await childTable.getColumns(ncMeta); await childTable.getColumns(ncMeta);
for (const c of childTable.columns) { for (const c of childTable.columns) {
if (!isLinksOrLTAR(c.uidt)) continue; if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta); await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOpt.fk_related_model_id === mmTable.id) { if (colOpt.fk_related_model_id === mmTable.id) {
@ -1556,7 +1542,6 @@ export class ColumnsService {
const sqlMgr = await ProjectMgrv2.getSqlMgr({ const sqlMgr = await ProjectMgrv2.getSqlMgr({
id: param.base.project_id, id: param.base.project_id,
}); });
const isLinks = param.column.uidt === UITypes.Links;
// if xcdb base then treat as virtual relation to avoid creating foreign key // if xcdb base then treat as virtual relation to avoid creating foreign key
if (param.base.is_meta) { if (param.base.is_meta) {
@ -1657,9 +1642,6 @@ export class ColumnsService {
(param.column as LinkToAnotherColumnReqType).title, (param.column as LinkToAnotherColumnReqType).title,
foreignKeyName, foreignKeyName,
(param.column as LinkToAnotherColumnReqType).virtual, (param.column as LinkToAnotherColumnReqType).virtual,
null,
param.column['meta'],
isLinks,
); );
} else if ((param.column as LinkToAnotherColumnReqType).type === 'mm') { } else if ((param.column as LinkToAnotherColumnReqType).type === 'mm') {
const aTn = `${param.project?.prefix ?? ''}_nc_m2m_${randomID()}`; const aTn = `${param.project?.prefix ?? ''}_nc_m2m_${randomID()}`;
@ -1780,9 +1762,9 @@ export class ColumnsService {
await Column.insert({ await Column.insert({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
await child.getColumns(), await child.getColumns(),
pluralize(parent.title), `${parent.title} List`,
), ),
uidt: isLinks ? UITypes.Links : UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
type: 'mm', type: 'mm',
// ref_db_alias // ref_db_alias
@ -1797,20 +1779,14 @@ export class ColumnsService {
fk_mm_parent_column_id: parentCol.id, fk_mm_parent_column_id: parentCol.id,
fk_related_model_id: parent.id, fk_related_model_id: parent.id,
virtual: (param.column as LinkToAnotherColumnReqType).virtual, virtual: (param.column as LinkToAnotherColumnReqType).virtual,
meta: {
plural: pluralize(parent.title),
singular: singularize(parent.title),
},
// if self referencing treat it as system field to hide from ui
system: parent.id === child.id,
}); });
await Column.insert({ await Column.insert({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
await parent.getColumns(), await parent.getColumns(),
param.column.title ?? pluralize(child.title), param.column.title ?? `${child.title} List`,
), ),
uidt: isLinks ? UITypes.Links : UITypes.LinkToAnotherRecord, uidt: UITypes.LinkToAnotherRecord,
type: 'mm', type: 'mm',
fk_model_id: parent.id, fk_model_id: parent.id,
@ -1823,10 +1799,6 @@ export class ColumnsService {
fk_mm_parent_column_id: childCol.id, fk_mm_parent_column_id: childCol.id,
fk_related_model_id: child.id, fk_related_model_id: child.id,
virtual: (param.column as LinkToAnotherColumnReqType).virtual, virtual: (param.column as LinkToAnotherColumnReqType).virtual,
meta: {
plural: param.column['meta']?.plural || pluralize(child.title),
singular: param.column['meta']?.singular || singularize(child.title),
},
}); });
// todo: create index for virtual relations as well // todo: create index for virtual relations as well

7
packages/nocodb/src/services/data-alias-nested.service.ts

@ -34,10 +34,7 @@ export class DataAliasNestedService {
const column = await getColumnByIdOrName(param.columnName, model); const column = await getColumnByIdOrName(param.columnName, model);
if ( if (column.uidt !== UITypes.LinkToAnotherRecord)
!column ||
![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)
)
NcError.badRequest('Column is not LTAR'); NcError.badRequest('Column is not LTAR');
const data = await baseModel.mmList( const data = await baseModel.mmList(
@ -206,7 +203,7 @@ export class DataAliasNestedService {
const column = await getColumnByIdOrName(param.columnName, model); const column = await getColumnByIdOrName(param.columnName, model);
if (![UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)) if (column.uidt !== UITypes.LinkToAnotherRecord)
NcError.badRequest('Column is not LTAR'); NcError.badRequest('Column is not LTAR');
const data = await baseModel.hmList( const data = await baseModel.hmList(

53
packages/nocodb/src/services/meta-diffs.service.ts

@ -1,13 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import { isVirtualCol, ModelTypes, RelationTypes, UITypes } from 'nocodb-sdk';
isLinksOrLTAR,
isVirtualCol,
ModelTypes,
RelationTypes,
UITypes,
} from 'nocodb-sdk';
import { T } from 'nc-help'; import { T } from 'nc-help';
import { pluralize, singularize } from 'inflection';
import { Base, Column, Model, Project } from '../models'; import { Base, Column, Model, Project } from '../models';
import ModelXcMetaFactory from '../db/sql-mgr/code/models/xc/ModelXcMetaFactory'; import ModelXcMetaFactory from '../db/sql-mgr/code/models/xc/ModelXcMetaFactory';
import getColumnUiType from '../helpers/getColumnUiType'; import getColumnUiType from '../helpers/getColumnUiType';
@ -16,7 +9,7 @@ import { getUniqueColumnAliasName } from '../helpers/getUniqueName';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import NcHelp from '../utils/NcHelp'; import NcHelp from '../utils/NcHelp';
import type { LinksColumn, LinkToAnotherRecordColumn } from '../models'; import type { LinkToAnotherRecordColumn } from '../models';
// todo:move enum and types // todo:move enum and types
export enum MetaDiffType { export enum MetaDiffType {
@ -229,13 +222,12 @@ export class MetaDiffsService {
if ( if (
[ [
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.Links,
UITypes.Rollup, UITypes.Rollup,
UITypes.Lookup, UITypes.Lookup,
UITypes.Formula, UITypes.Formula,
].includes(column.uidt) ].includes(column.uidt)
) { ) {
if (isLinksOrLTAR(column.uidt)) { if (column.uidt === UITypes.LinkToAnotherRecord) {
virtualRelationColumns.push(column); virtualRelationColumns.push(column);
} }
@ -515,7 +507,6 @@ export class MetaDiffsService {
UITypes.Rollup, UITypes.Rollup,
UITypes.Lookup, UITypes.Lookup,
UITypes.Formula, UITypes.Formula,
UITypes.Links,
].includes(column.uidt) ].includes(column.uidt)
) { ) {
continue; continue;
@ -754,10 +745,10 @@ export class MetaDiffsService {
} else if (change.relationType === RelationTypes.HAS_MANY) { } else if (change.relationType === RelationTypes.HAS_MANY) {
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(
childModel.columns, childModel.columns,
pluralize(childModel.title || childModel.table_name), `${childModel.title || childModel.table_name} List`,
); );
await Column.insert<LinkToAnotherRecordColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title, title,
fk_model_id: parentModel.id, fk_model_id: parentModel.id,
fk_related_model_id: childModel.id, fk_related_model_id: childModel.id,
@ -766,10 +757,6 @@ export class MetaDiffsService {
fk_child_column_id: childCol.id, fk_child_column_id: childCol.id,
virtual: false, virtual: false,
fk_index_name: change.cstn, fk_index_name: change.cstn,
meta: {
plural: pluralize(childModel.title),
singular: singularize(childModel.title),
},
}); });
} }
}); });
@ -953,10 +940,10 @@ export class MetaDiffsService {
} else if (change.relationType === RelationTypes.HAS_MANY) { } else if (change.relationType === RelationTypes.HAS_MANY) {
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(
childModel.columns, childModel.columns,
pluralize(childModel.title || childModel.table_name), `${childModel.title || childModel.table_name} List`,
); );
await Column.insert<LinksColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title, title,
fk_model_id: parentModel.id, fk_model_id: parentModel.id,
fk_related_model_id: childModel.id, fk_related_model_id: childModel.id,
@ -992,7 +979,7 @@ export class MetaDiffsService {
const colChildOpt = const colChildOpt =
await belongsToCol.getColOptions<LinkToAnotherRecordColumn>(); await belongsToCol.getColOptions<LinkToAnotherRecordColumn>();
for (const col of await model.getColumns()) { for (const col of await model.getColumns()) {
if (isLinksOrLTAR(col.uidt)) { if (col.uidt === UITypes.LinkToAnotherRecord) {
const colOpt = await col.getColOptions<LinkToAnotherRecordColumn>(); const colOpt = await col.getColOptions<LinkToAnotherRecordColumn>();
if ( if (
colOpt && colOpt &&
@ -1051,10 +1038,10 @@ export class MetaDiffsService {
); );
if (!isRelationAvailInA) { if (!isRelationAvailInA) {
await Column.insert<LinksColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
modelA.columns, modelA.columns,
pluralize(modelB.title), `${modelB.title} List`,
), ),
fk_model_id: modelA.id, fk_model_id: modelA.id,
fk_related_model_id: modelB.id, fk_related_model_id: modelB.id,
@ -1067,18 +1054,14 @@ export class MetaDiffsService {
fk_mm_parent_column_id: fk_mm_parent_column_id:
belongsToCols[1].colOptions.fk_child_column_id, belongsToCols[1].colOptions.fk_child_column_id,
type: RelationTypes.MANY_TO_MANY, type: RelationTypes.MANY_TO_MANY,
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
meta: {
plural: pluralize(modelB.title),
singular: singularize(modelB.title),
},
}); });
} }
if (!isRelationAvailInB) { if (!isRelationAvailInB) {
await Column.insert<LinksColumn>({ await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName( title: getUniqueColumnAliasName(
modelB.columns, modelB.columns,
pluralize(modelA.title), `${modelA.title} List`,
), ),
fk_model_id: modelB.id, fk_model_id: modelB.id,
fk_related_model_id: modelA.id, fk_related_model_id: modelA.id,
@ -1091,11 +1074,7 @@ export class MetaDiffsService {
fk_mm_parent_column_id: fk_mm_parent_column_id:
belongsToCols[0].colOptions.fk_child_column_id, belongsToCols[0].colOptions.fk_child_column_id,
type: RelationTypes.MANY_TO_MANY, type: RelationTypes.MANY_TO_MANY,
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
meta: {
plural: pluralize(modelA.title),
singular: singularize(modelA.title),
},
}); });
} }
@ -1107,7 +1086,7 @@ export class MetaDiffsService {
const model = await colOpt.getRelatedTable(); const model = await colOpt.getRelatedTable();
for (const col of await model.getColumns()) { for (const col of await model.getColumns()) {
if (!isLinksOrLTAR(col.uidt)) continue; if (col.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt1 = const colOpt1 =
await col.getColOptions<LinkToAnotherRecordColumn>(); await col.getColOptions<LinkToAnotherRecordColumn>();

5
packages/nocodb/src/services/public-metas.service.ts

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ErrorMessages, RelationTypes, UITypes } from 'nocodb-sdk'; import { ErrorMessages, RelationTypes, UITypes } from 'nocodb-sdk';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { NcError } from '../helpers/catchError'; import { NcError } from '../helpers/catchError';
import { Base, Column, Model, Project, View } from '../models'; import { Base, Column, Model, Project, View } from '../models';
import type { LinkToAnotherRecordColumn, LookupColumn } from '../models'; import type { LinkToAnotherRecordColumn, LookupColumn } from '../models';
@ -42,7 +41,7 @@ export class PublicMetasService {
column.pk || column.pk ||
view.model.columns.some( view.model.columns.some(
(c1) => (c1) =>
isLinksOrLTAR(c1.uidt) && c1.uidt === UITypes.LinkToAnotherRecord &&
(<LinkToAnotherRecordColumn>c1.colOptions).type === (<LinkToAnotherRecordColumn>c1.colOptions).type ===
RelationTypes.BELONGS_TO && RelationTypes.BELONGS_TO &&
view.columns.some((vc) => vc.fk_column_id === c1.id && vc.show) && view.columns.some((vc) => vc.fk_column_id === c1.id && vc.show) &&
@ -78,7 +77,7 @@ export class PublicMetasService {
col: Column<any>; col: Column<any>;
relatedMetas: Record<string, Model>; relatedMetas: Record<string, Model>;
}) { }) {
if (isLinksOrLTAR(col.uidt)) { if (UITypes.LinkToAnotherRecord === col.uidt) {
await this.extractLTARRelatedMetas({ await this.extractLTARRelatedMetas({
ltarColOption: await col.getColOptions<LinkToAnotherRecordColumn>(), ltarColOption: await col.getColOptions<LinkToAnotherRecordColumn>(),
relatedMetas, relatedMetas,

13
packages/nocodb/src/services/tables.service.ts

@ -3,7 +3,6 @@ import DOMPurify from 'isomorphic-dompurify';
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
AuditOperationTypes, AuditOperationTypes,
isLinksOrLTAR,
isVirtualCol, isVirtualCol,
ModelTypes, ModelTypes,
UITypes, UITypes,
@ -15,7 +14,13 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import getColumnUiType from '../helpers/getColumnUiType'; import getColumnUiType from '../helpers/getColumnUiType';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import { Audit, Column, Model, ModelRoleVisibility, Project } from '../models'; import {
Audit,
Column,
Model,
ModelRoleVisibility,
Project,
} from '../models';
import Noco from '../Noco'; import Noco from '../Noco';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { validatePayload } from '../helpers'; import { validatePayload } from '../helpers';
@ -151,7 +156,9 @@ export class TablesService {
const project = await Project.getWithInfo(table.project_id); const project = await Project.getWithInfo(table.project_id);
const base = project.bases.find((b) => b.id === table.base_id); const base = project.bases.find((b) => b.id === table.base_id);
const relationColumns = table.columns.filter((c) => isLinksOrLTAR(c)); const relationColumns = table.columns.filter(
(c) => c.uidt === UITypes.LinkToAnotherRecord,
);
if (relationColumns?.length && !base.is_meta) { if (relationColumns?.length && !base.is_meta) {
const referredTables = await Promise.all( const referredTables = await Promise.all(

54
packages/nocodb/tests/unit/factory/column.ts

@ -1,13 +1,13 @@
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import request from 'supertest'; import request from 'supertest';
import Column from '../../../src/models/Column';
import FormViewColumn from '../../../src/models/FormViewColumn';
import GalleryViewColumn from '../../../src/models/GalleryViewColumn';
import GridViewColumn from '../../../src/models/GridViewColumn';
import Model from '../../../src/models/Model'; import Model from '../../../src/models/Model';
import { isPg, isSqlite } from '../init/db'; import Project from '../../../src/models/Project';
import type Column from '../../../src/models/Column'; import View from '../../../src/models/View';
import type FormViewColumn from '../../../src/models/FormViewColumn'; import { isSqlite, isPg } from '../init/db';
import type GalleryViewColumn from '../../../src/models/GalleryViewColumn';
import type GridViewColumn from '../../../src/models/GridViewColumn';
import type Project from '../../../src/models/Project';
import type View from '../../../src/models/View';
const defaultColumns = function (context) { const defaultColumns = function (context) {
return [ return [
@ -55,7 +55,7 @@ const createColumn = async (context, table, columnAttr) => {
}); });
const column: Column = (await table.getColumns()).find( const column: Column = (await table.getColumns()).find(
(column) => column.title === columnAttr.title, (column) => column.title === columnAttr.title
); );
return column; return column;
}; };
@ -76,7 +76,7 @@ const createRollupColumn = async (
table: Model; table: Model;
relatedTableName: string; relatedTableName: string;
relatedTableColumnTitle: string; relatedTableColumnTitle: string;
}, }
) => { ) => {
const childBases = await project.getBases(); const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({ const childTable = await Model.getByIdOrName({
@ -86,14 +86,13 @@ const createRollupColumn = async (
}); });
const childTableColumns = await childTable.getColumns(); const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find( const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle, (column) => column.title === relatedTableColumnTitle
); );
const ltarColumn = (await table.getColumns()).find( const ltarColumn = (await table.getColumns()).find(
(column) => (column) =>
(column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord &&
column.uidt === UITypes.LinkToAnotherRecord) && column.colOptions?.fk_related_model_id === childTable.id
column.colOptions?.fk_related_model_id === childTable.id,
); );
const rollupColumn = await createColumn(context, table, { const rollupColumn = await createColumn(context, table, {
@ -123,7 +122,7 @@ const createLookupColumn = async (
table: Model; table: Model;
relatedTableName: string; relatedTableName: string;
relatedTableColumnTitle: string; relatedTableColumnTitle: string;
}, }
) => { ) => {
const childBases = await project.getBases(); const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({ const childTable = await Model.getByIdOrName({
@ -133,20 +132,19 @@ const createLookupColumn = async (
}); });
const childTableColumns = await childTable.getColumns(); const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find( const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle, (column) => column.title === relatedTableColumnTitle
); );
if (!childTableColumn) { if (!childTableColumn) {
throw new Error( throw new Error(
`Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`, `Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`
); );
} }
const ltarColumn = (await table.getColumns()).find( const ltarColumn = (await table.getColumns()).find(
(column) => (column) =>
(column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord &&
column.uidt === UITypes.LinkToAnotherRecord) && column.colOptions?.fk_related_model_id === childTable.id
column.colOptions?.fk_related_model_id === childTable.id,
); );
const lookupColumn = await createColumn(context, table, { const lookupColumn = await createColumn(context, table, {
title: title, title: title,
@ -170,15 +168,15 @@ const createQrCodeColumn = async (
title: string; title: string;
table: Model; table: Model;
referencedQrValueTableColumnTitle: string; referencedQrValueTableColumnTitle: string;
}, }
) => { ) => {
const referencedQrValueTableColumnId = await table const referencedQrValueTableColumnId = await table
.getColumns() .getColumns()
.then( .then(
(cols) => (cols) =>
cols.find( cols.find(
(column) => column.title == referencedQrValueTableColumnTitle, (column) => column.title == referencedQrValueTableColumnTitle
)['id'], )['id']
); );
const qrCodeColumn = await createColumn(context, table, { const qrCodeColumn = await createColumn(context, table, {
@ -200,15 +198,15 @@ const createBarcodeColumn = async (
title: string; title: string;
table: Model; table: Model;
referencedBarcodeValueTableColumnTitle: string; referencedBarcodeValueTableColumnTitle: string;
}, }
) => { ) => {
const referencedBarcodeValueTableColumnId = await table const referencedBarcodeValueTableColumnId = await table
.getColumns() .getColumns()
.then( .then(
(cols) => (cols) =>
cols.find( cols.find(
(column) => column.title == referencedBarcodeValueTableColumnTitle, (column) => column.title == referencedBarcodeValueTableColumnTitle
)['id'], )['id']
); );
const barcodeColumn = await createColumn(context, table, { const barcodeColumn = await createColumn(context, table, {
@ -232,12 +230,12 @@ const createLtarColumn = async (
parentTable: Model; parentTable: Model;
childTable: Model; childTable: Model;
type: string; type: string;
}, }
) => { ) => {
const ltarColumn = await createColumn(context, parentTable, { const ltarColumn = await createColumn(context, parentTable, {
title: title, title: title,
column_name: title, column_name: title,
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
parentId: parentTable.id, parentId: parentTable.id,
childId: childTable.id, childId: childTable.id,
type: type, type: type,
@ -248,7 +246,7 @@ const createLtarColumn = async (
const updateViewColumn = async ( const updateViewColumn = async (
context, context,
{ view, column, attr }: { column: Column; view: View; attr: any }, { view, column, attr }: { column: Column; view: View; attr: any }
) => { ) => {
const res = await request(context.app) const res = await request(context.app)
.patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`) .patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`)

113
packages/nocodb/tests/unit/rest/tests/tableRow.test.ts

@ -427,6 +427,10 @@ function tableTest() {
relatedTableColumnTitle: 'FirstName', relatedTableColumnTitle: 'FirstName',
}); });
const paymentListColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const nestedFilter = { const nestedFilter = {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -439,6 +443,12 @@ function tableTest() {
comparison_op: 'like', comparison_op: 'like',
value: '%a%', value: '%a%',
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
], ],
}; };
@ -503,6 +513,10 @@ function tableTest() {
relatedTableColumnTitle: 'FirstName', relatedTableColumnTitle: 'FirstName',
}); });
const paymentListColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const returnDateColumn = (await rentalTable.getColumns()).find( const returnDateColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'ReturnDate', (c) => c.title === 'ReturnDate',
); );
@ -519,6 +533,12 @@ function tableTest() {
comparison_op: 'like', comparison_op: 'like',
value: '%a%', value: '%a%',
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{ {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -611,7 +631,7 @@ function tableTest() {
}); });
const paymentListColumn = (await customerTable.getColumns()).find( const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payments', (c) => c.title === 'Payment List',
); );
const activeColumn = (await customerTable.getColumns()).find( const activeColumn = (await customerTable.getColumns()).find(
@ -766,12 +786,16 @@ function tableTest() {
relatedTableColumnTitle: 'RentalDate', relatedTableColumnTitle: 'RentalDate',
}); });
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find( const activeColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Active', (c) => c.title === 'Active',
); );
const nestedFields = { const nestedFields = {
Rentals: { fields: ['RentalDate', 'ReturnDate'] }, 'Rental List': { fields: ['RentalDate', 'ReturnDate'] },
}; };
const nestedFilter = [ const nestedFilter = [
@ -794,6 +818,12 @@ function tableTest() {
comparison_op: 'lte', comparison_op: 'lte',
value: 30, value: 30,
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{ {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -837,7 +867,7 @@ function tableTest() {
} }
const nestedRentalResponse = Object.keys( const nestedRentalResponse = Object.keys(
ascResponse.body.list[0]['Rentals'], ascResponse.body.list[0]['Rental List'],
); );
if ( if (
nestedRentalResponse.includes('ReturnDate') && nestedRentalResponse.includes('ReturnDate') &&
@ -1067,12 +1097,16 @@ function tableTest() {
relatedTableColumnTitle: 'RentalDate', relatedTableColumnTitle: 'RentalDate',
}); });
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find( const activeColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Active', (c) => c.title === 'Active',
); );
const nestedFields = { const nestedFields = {
Rentals: { 'Rental List': {
f: 'RentalDate,ReturnDate', f: 'RentalDate,ReturnDate',
}, },
}; };
@ -1097,6 +1131,12 @@ function tableTest() {
comparison_op: 'lte', comparison_op: 'lte',
value: 30, value: 30,
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{ {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -1132,7 +1172,7 @@ function tableTest() {
throw new Error('Wrong filter'); throw new Error('Wrong filter');
} }
const nestedRentalResponse = Object.keys(ascResponse.body['Rentals']); const nestedRentalResponse = Object.keys(ascResponse.body['Rental List']);
if ( if (
nestedRentalResponse.includes('RentalId') && nestedRentalResponse.includes('RentalId') &&
nestedRentalResponse.includes('RentalDate') && nestedRentalResponse.includes('RentalDate') &&
@ -1250,15 +1290,24 @@ function tableTest() {
) )
.set('xc-auth', context.token) .set('xc-auth', context.token)
.query({ .query({
'nested[Films][fields]': 'Title,ReleaseYear,Language', 'nested[Film List][fields]': 'Title,ReleaseYear,Language',
}) })
.expect(200); .expect(200);
const record = response.body; const record = response.body;
expect(record['Film List']).length(19);
expect(record['Film List'][0]).to.have.all.keys(
'Title',
'ReleaseYear',
'Language',
);
// for SQLite Sakila, Language is null
if (isPg(context)) { if (isPg(context)) {
expect(record['Films']).to.equal('19'); expect(record['Film List'][0]['Language']).to.have.all.keys(
} else { 'Name',
expect(record['Films']).to.equal(19); 'LanguageId',
);
} }
}); });
@ -1634,7 +1683,7 @@ function tableTest() {
it('Nested row list hm', async () => { it('Nested row list hm', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.get( .get(
@ -1653,7 +1702,7 @@ function tableTest() {
it('Nested row list hm with limit and offset', async () => { it('Nested row list hm with limit and offset', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.get( .get(
@ -1679,7 +1728,7 @@ function tableTest() {
it('Row list hm with invalid table id', async () => { it('Row list hm with invalid table id', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.get( .get(
@ -1703,7 +1752,7 @@ function tableTest() {
// const visibleColumns = [firstNameColumn]; // const visibleColumns = [firstNameColumn];
// const rentalListColumn = (await customerTable.getColumns()).find( // const rentalListColumn = (await customerTable.getColumns()).find(
// (column) => column.title === 'Rentals' // (column) => column.title === 'Rental List'
// )!; // )!;
// const response = await request(context.app) // const response = await request(context.app)
// .get(`/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/${rowId}/hm/${rentalListColumn.id}`) // .get(`/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/${rowId}/hm/${rentalListColumn.id}`)
@ -1731,7 +1780,7 @@ function tableTest() {
}); });
const filmTable = await getTable({ project: sakilaProject, name: 'film' }); const filmTable = await getTable({ project: sakilaProject, name: 'film' });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.get( .get(
@ -1755,7 +1804,7 @@ function tableTest() {
}); });
const filmTable = await getTable({ project: sakilaProject, name: 'film' }); const filmTable = await getTable({ project: sakilaProject, name: 'film' });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.get( .get(
@ -1786,7 +1835,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.get( .get(
@ -1804,7 +1853,7 @@ function tableTest() {
it('Create hm relation with invalid table id', async () => { it('Create hm relation with invalid table id', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const refId = 1; const refId = 1;
const response = await request(context.app) const response = await request(context.app)
@ -1861,7 +1910,7 @@ function tableTest() {
// it.only('Create list mm existing ref row id', async () => { // it.only('Create list mm existing ref row id', async () => {
// const rowId = 1; // const rowId = 1;
// const rentalListColumn = (await customerTable.getColumns()).find( // const rentalListColumn = (await customerTable.getColumns()).find(
// (column) => column.title === 'Rentals' // (column) => column.title === 'Rental List'
// )!; // )!;
// const refId = 1; // const refId = 1;
@ -1879,7 +1928,7 @@ function tableTest() {
it('Create list hm', async () => { it('Create list hm', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const refId = 1; const refId = 1;
@ -1966,7 +2015,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const refId = 1; const refId = 1;
@ -1988,7 +2037,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const refId = 2; const refId = 2;
@ -2057,7 +2106,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const refId = 1; const refId = 1;
@ -2097,7 +2146,7 @@ function tableTest() {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const refId = 76; const refId = 76;
@ -2146,9 +2195,8 @@ function tableTest() {
column: ltarColumn, column: ltarColumn,
type: 'hm', type: 'hm',
}); });
const childRow = row['Ltar'][0];
// read rows of related table
const childRow = (await listRow({ project, table: relatedTable }))[0];
const response = await request(context.app) const response = await request(context.app)
.delete( .delete(
`/api/v1/db/data/noco/${project.id}/${table.id}/${row['Id']}/hm/${ltarColumn.id}/${childRow['Id']}`, `/api/v1/db/data/noco/${project.id}/${table.id}/${row['Id']}/hm/${ltarColumn.id}/${childRow['Id']}`,
@ -2158,8 +2206,7 @@ function tableTest() {
const updatedRow = await getRow(context, { project, table, id: row['Id'] }); const updatedRow = await getRow(context, { project, table, id: row['Id'] });
// LTAR now returns rollup count if (updatedRow['Ltar'].length !== 0) {
if (!(updatedRow['Ltar'] === 0 || updatedRow['Ltar'] === '0')) {
throw new Error('Was not deleted'); throw new Error('Was not deleted');
} }
@ -2173,7 +2220,7 @@ function tableTest() {
it('Exclude list hm', async () => { it('Exclude list hm', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
@ -2192,7 +2239,7 @@ function tableTest() {
it('Exclude list hm with limit and offset', async () => { it('Exclude list hm with limit and offset', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
@ -2224,7 +2271,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
@ -2247,7 +2294,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
@ -2321,7 +2368,7 @@ function tableTest() {
it('Create nested hm relation with invalid table id', async () => { it('Create nested hm relation with invalid table id', async () => {
const rowId = 1; const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find( const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals', (column) => column.title === 'Rental List',
)!; )!;
const refId = 1; const refId = 1;
const response = await request(context.app) const response = await request(context.app)
@ -2344,7 +2391,7 @@ function tableTest() {
name: 'actor', name: 'actor',
}); });
const filmListColumn = (await actorTable.getColumns()).find( const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films', (column) => column.title === 'Film List',
)!; )!;
const response = await request(context.app) const response = await request(context.app)
.post( .post(

53
packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

@ -19,7 +19,6 @@ import {
getOneRow, getOneRow,
getRow, getRow,
} from '../../factory/row'; } from '../../factory/row';
import { isPg } from '../../init/db';
import type { ColumnType } from 'nocodb-sdk'; import type { ColumnType } from 'nocodb-sdk';
import type View from '../../../../src/models/View'; import type View from '../../../../src/models/View';
import type Model from '../../../../src/models/Model'; import type Model from '../../../../src/models/Model';
@ -522,6 +521,10 @@ function viewRowTests() {
relatedTableColumnTitle: 'FirstName', relatedTableColumnTitle: 'FirstName',
}); });
const paymentListColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const nestedFilter = { const nestedFilter = {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -534,6 +537,12 @@ function viewRowTests() {
comparison_op: 'like', comparison_op: 'like',
value: '%a%', value: '%a%',
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
], ],
}; };
@ -613,12 +622,16 @@ function viewRowTests() {
relatedTableColumnTitle: 'RentalDate', relatedTableColumnTitle: 'RentalDate',
}); });
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find( const activeColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Active', (c) => c.title === 'Active',
); );
const nestedFields = { const nestedFields = {
Rentals: { fields: ['RentalDate', 'ReturnDate'] }, 'Rental List': { fields: ['RentalDate', 'ReturnDate'] },
}; };
const nestedFilter = [ const nestedFilter = [
@ -641,6 +654,12 @@ function viewRowTests() {
comparison_op: 'lte', comparison_op: 'lte',
value: 30, value: 30,
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{ {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -682,10 +701,18 @@ function viewRowTests() {
throw new Error('Wrong filter'); throw new Error('Wrong filter');
} }
if (isPg(context)) { const nestedRentalResponse = Object.keys(
expect(ascResponse.body.list[0]['Rentals']).to.equal('12'); ascResponse.body.list[0]['Rental List'][0],
} else { );
expect(ascResponse.body.list[0]['Rentals']).to.equal(12);
if (
!(
nestedRentalResponse.includes('ReturnDate') &&
nestedRentalResponse.includes('RentalDate') &&
nestedRentalResponse.length === 2
)
) {
throw new Error('Wrong nested fields');
} }
}; };
@ -864,12 +891,16 @@ function viewRowTests() {
attr: { show: true }, attr: { show: true },
}); });
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find( const activeColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Active', (c) => c.title === 'Active',
); );
const nestedFields = { const nestedFields = {
Rentals: { f: 'RentalDate,ReturnDate' }, 'Rental List': { f: 'RentalDate,ReturnDate' },
}; };
const nestedFilter = [ const nestedFilter = [
@ -892,6 +923,12 @@ function viewRowTests() {
comparison_op: 'lte', comparison_op: 'lte',
value: 30, value: 30,
}, },
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{ {
is_group: true, is_group: true,
status: 'create', status: 'create',
@ -927,7 +964,7 @@ function viewRowTests() {
throw new Error('Wrong filter'); throw new Error('Wrong filter');
} }
const nestedRentalResponse = Object.keys(ascResponse.body['Rentals']); const nestedRentalResponse = Object.keys(ascResponse.body['Rental List']);
if ( if (
nestedRentalResponse.includes('RentalId') && nestedRentalResponse.includes('RentalId') &&
nestedRentalResponse.includes('RentalDate') && nestedRentalResponse.includes('RentalDate') &&

3
packages/nocodb/tests/unit/tsconfig.json

@ -46,8 +46,7 @@
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"lib": [ "lib": [
"es2017", "es2017"
"dom"
], ],
"types": [ "types": [
"mocha", "node" "mocha", "node"

220
tests/playwright/fixtures/expectedBaseDownloadData.txt

@ -1,110 +1,110 @@
Country,Cities Country,City List
Afghanistan,1 Afghanistan,Kabul
Algeria,3 Algeria,"Batna, Bchar, Skikda"
American Samoa,1 American Samoa,Tafuna
Angola,2 Angola,"Benguela, Namibe"
Anguilla,1 Anguilla,South Hill
Argentina,13 Argentina,"Almirante Brown, Avellaneda, Baha Blanca, Crdoba, Escobar, Ezeiza, La Plata, Merlo, Quilmes, San Miguel de Tucumn, Santa F, Tandil, Vicente Lpez"
Armenia,1 Armenia,Yerevan
Australia,1 Australia,Woodridge
Austria,3 Austria,"Graz, Linz, Salzburg"
Azerbaijan,2 Azerbaijan,"Baku, Sumqayit"
Bahrain,1 Bahrain,al-Manama
Bangladesh,3 Bangladesh,"Dhaka, Jamalpur, Tangail"
Belarus,2 Belarus,"Mogiljov, Molodetno"
Bolivia,2 Bolivia,"El Alto, Sucre"
Brazil,28 Brazil,"Alvorada, Angra dos Reis, Anpolis, Aparecida de Goinia, Araatuba, Bag, Belm, Blumenau, Boa Vista, Braslia, Goinia, Guaruj, guas Lindas de Gois, Ibirit, Juazeiro do Norte, Juiz de Fora, Luzinia, Maring, Po, Poos de Caldas, Rio Claro, Santa Brbara dOeste, Santo Andr, So Bernardo do Campo, So Leopoldo"
Brunei,1 Brunei,Bandar Seri Begawan
Bulgaria,2 Bulgaria,"Ruse, Stara Zagora"
Cambodia,2 Cambodia,"Battambang, Phnom Penh"
Cameroon,2 Cameroon,"Bamenda, Yaound"
Canada,7 Canada,"Gatineau, Halifax, Lethbridge, London, Oshawa, Richmond Hill, Vancouver"
Chad,1 Chad,NDjamna
Chile,3 Chile,"Antofagasta, Coquimbo, Rancagua"
China,53 China,"Baicheng, Baiyin, Binzhou, Changzhou, Datong, Daxian, Dongying, Emeishan, Enshi, Ezhou, Fuyu, Fuzhou, Haining, Hami, Hohhot, Huaian, Jinchang, Jining, Jinzhou, Junan, Korla, Laiwu, Laohekou, Lengshuijiang, Leshan"
Colombia,6 Colombia,"Buenaventura, Dos Quebradas, Florencia, Pereira, Sincelejo, Sogamoso"
"Congo, The Democratic Republic of the",2 "Congo, The Democratic Republic of the","Lubumbashi, Mwene-Ditu"
Czech Republic,1 Czech Republic,Olomouc
Dominican Republic,3 Dominican Republic,"La Romana, San Felipe de Puerto Plata, Santiago de los Caballeros"
Ecuador,3 Ecuador,"Loja, Portoviejo, Robamba"
Egypt,6 Egypt,"Bilbays, Idfu, Mit Ghamr, Qalyub, Sawhaj, Shubra al-Khayma"
Estonia,1 Estonia,Tartu
Ethiopia,1 Ethiopia,Addis Abeba
Faroe Islands,1 Faroe Islands,Trshavn
Finland,1 Finland,Oulu
France,4 France,"Brest, Le Mans, Toulon, Toulouse"
French Guiana,1 French Guiana,Cayenne
French Polynesia,2 French Polynesia,"Faaa, Papeete"
Gambia,1 Gambia,Banjul
Germany,7 Germany,"Duisburg, Erlangen, Halle/Saale, Mannheim, Saarbrcken, Siegen, Witten"
Greece,2 Greece,"Athenai, Patras"
Greenland,1 Greenland,Nuuk
Holy See (Vatican City State),1 Holy See (Vatican City State),Citt del Vaticano
Hong Kong,1 Hong Kong,Kowloon and New Kowloon
Hungary,1 Hungary,Szkesfehrvr
India,60 India,"Adoni, Ahmadnagar, Allappuzha (Alleppey), Ambattur, Amroha, Balurghat, Berhampore (Baharampur), Bhavnagar, Bhilwara, Bhimavaram, Bhopal, Bhusawal, Bijapur, Chandrapur, Chapra, Dhule (Dhulia), Etawah, Firozabad, Gandhinagar, Gulbarga, Haldia, Halisahar, Hoshiarpur, Hubli-Dharwad, Jaipur"
Indonesia,14 Indonesia,"Cianjur, Ciomas, Ciparay, Gorontalo, Jakarta, Lhokseumawe, Madiun, Pangkal Pinang, Pemalang, Pontianak, Probolinggo, Purwakarta, Surakarta, Tegal"
Iran,8 Iran,"Arak, Esfahan, Kermanshah, Najafabad, Qomsheh, Shahr-e Kord, Sirjan, Tabriz"
Iraq,1 Iraq,Mosul
Israel,4 Israel,"Ashdod, Ashqelon, Bat Yam, Tel Aviv-Jaffa"
Italy,7 Italy,"Alessandria, Bergamo, Brescia, Brindisi, Livorno, Syrakusa, Udine"
Japan,31 Japan,"Akishima, Fukuyama, Higashiosaka, Hino, Hiroshima, Isesaki, Iwaki, Iwakuni, Iwatsuki, Izumisano, Kakamigahara, Kamakura, Kanazawa, Koriyama, Kurashiki, Kuwana, Matsue, Miyakonojo, Nagareyama, Okayama, Okinawa, Omiya, Onomichi, Otsu, Sagamihara"
Kazakstan,2 Kazakstan,"Pavlodar, Zhezqazghan"
Kenya,2 Kenya,"Kisumu, Nyeri"
Kuwait,1 Kuwait,Jalib al-Shuyukh
Latvia,2 Latvia,"Daugavpils, Liepaja"
Liechtenstein,1 Liechtenstein,Vaduz
Lithuania,1 Lithuania,Vilnius
Madagascar,1 Madagascar,Mahajanga
Malawi,1 Malawi,Lilongwe
Malaysia,3 Malaysia,"Ipoh, Kuching, Sungai Petani"
Mexico,30 Mexico,"Acua, Allende, Atlixco, Carmen, Celaya, Coacalco de Berriozbal, Coatzacoalcos, Cuauhtmoc, Cuautla, Cuernavaca, El Fuerte, Guadalajara, Hidalgo, Huejutla de Reyes, Huixquilucan, Jos Azueta, Jurez, La Paz, Matamoros, Mexicali, Monclova, Nezahualcyotl, Pachuca de Soto, Salamanca, San Felipe del Progreso"
Moldova,1 Moldova,Chisinau
Morocco,3 Morocco,"Beni-Mellal, Nador, Sal"
Mozambique,3 Mozambique,"Beira, Naala-Porto, Tete"
Myanmar,2 Myanmar,"Monywa, Myingyan"
Nauru,1 Nauru,Yangor
Nepal,1 Nepal,Birgunj
Netherlands,5 Netherlands,"Amersfoort, Apeldoorn, Ede, Emmen, s-Hertogenbosch"
New Zealand,1 New Zealand,Hamilton
Nigeria,13 Nigeria,"Benin City, Deba Habe, Effon-Alaiye, Ife, Ikerre, Ilorin, Kaduna, Ogbomosho, Ondo, Owo, Oyo, Sokoto, Zaria"
North Korea,1 North Korea,Pyongyang
Oman,2 Oman,"Masqat, Salala"
Pakistan,5 Pakistan,"Dadu, Mandi Bahauddin, Mardan, Okara, Shikarpur"
Paraguay,3 Paraguay,"Asuncin, Ciudad del Este, San Lorenzo"
Peru,4 Peru,"Callao, Hunuco, Lima, Sullana"
Philippines,20 Philippines,"Baybay, Bayugan, Bislig, Cabuyao, Cavite, Davao, Gingoog, Hagonoy, Iligan, Imus, Lapu-Lapu, Mandaluyong, Ozamis, Santa Rosa, Taguig, Talavera, Tanauan, Tanza, Tarlac, Tuguegarao"
Poland,8 Poland,"Bydgoszcz, Czestochowa, Jastrzebie-Zdrj, Kalisz, Lublin, Plock, Tychy, Wroclaw"
Puerto Rico,2 Puerto Rico,"Arecibo, Ponce"
Romania,2 Romania,"Botosani, Bucuresti"
Runion,1 Runion,Saint-Denis
Russian Federation,28 Russian Federation,"Atinsk, Balaiha, Dzerzinsk, Elista, Ivanovo, Jaroslavl, Jelets, Kaliningrad, Kamyin, Kirovo-Tepetsk, Kolpino, Korolev, Kurgan, Kursk, Lipetsk, Ljubertsy, Maikop, Moscow, Nabereznyje Telny, Niznekamsk, Novoterkassk, Pjatigorsk, Serpuhov, Smolensk, Syktyvkar"
Saint Vincent and the Grenadines,1 Saint Vincent and the Grenadines,Kingstown
Saudi Arabia,5 Saudi Arabia,"Abha, al-Hawiya, al-Qatif, Jedda, Tabuk"
Senegal,1 Senegal,Ziguinchor
Slovakia,1 Slovakia,Bratislava
South Africa,11 South Africa,"Boksburg, Botshabelo, Chatsworth, Johannesburg, Kimberley, Klerksdorp, Newcastle, Paarl, Rustenburg, Soshanguve, Springs"
South Korea,5 South Korea,"Cheju, Kimchon, Naju, Tonghae, Uijongbu"
Spain,5 Spain,"A Corua (La Corua), Donostia-San Sebastin, Gijn, Ourense (Orense), Santiago de Compostela"
Sri Lanka,1 Sri Lanka,Jaffna
Sudan,2 Sudan,"al-Qadarif, Omdurman"
Sweden,1 Sweden,Malm
Switzerland,3 Switzerland,"Basel, Bern, Lausanne"
Taiwan,10 Taiwan,"Changhwa, Chiayi, Chungho, Fengshan, Hsichuh, Lungtan, Nantou, Tanshui, Touliu, Tsaotun"
Tanzania,3 Tanzania,"Mwanza, Tabora, Zanzibar"
Thailand,3 Thailand,"Nakhon Sawan, Pak Kret, Songkhla"
Tonga,1 Tonga,Nukualofa
Tunisia,1 Tunisia,Sousse
Turkey,15 Turkey,"Adana, Balikesir, Batman, Denizli, Eskisehir, Gaziantep, Inegl, Kilis, Ktahya, Osmaniye, Sivas, Sultanbeyli, Tarsus, Tokat, Usak"
Turkmenistan,1 Turkmenistan,Ashgabat
Tuvalu,1 Tuvalu,Funafuti
Ukraine,6 Ukraine,"Kamjanets-Podilskyi, Konotop, Mukateve, ostka, Simferopol, Sumy"
United Arab Emirates,3 United Arab Emirates,"Abu Dhabi, al-Ayn, Sharja"
United Kingdom,8 United Kingdom,"Bradford, Dundee, London, Southampton, Southend-on-Sea, Southport, Stockport, York"
United States,35 United States,"Akron, Arlington, Augusta-Richmond County, Aurora, Bellevue, Brockton, Cape Coral, Citrus Heights, Clarksville, Compton, Dallas, Dayton, El Monte, Fontana, Garden Grove, Garland, Grand Prairie, Greensboro, Joliet, Kansas City, Lancaster, Laredo, Lincoln, Manchester, Memphis"
Venezuela,7 Venezuela,"Barcelona, Caracas, Cuman, Maracabo, Ocumare del Tuy, Valencia, Valle de la Pascua"
Vietnam,6 Vietnam,"Cam Ranh, Haiphong, Hanoi, Nam Dinh, Nha Trang, Vinh"
"Virgin Islands, U.S.",1 "Virgin Islands, U.S.",Charlotte Amalie
Yemen,4 Yemen,"Aden, Hodeida, Sanaa, Taizz"
Yugoslavia,2 Yugoslavia,"Kragujevac, Novi Sad"
Zambia,1 Zambia,Kitwe

220
tests/playwright/fixtures/expectedBaseDownloadDataPg.txt

@ -1,110 +1,110 @@
Country,Cities Country,City List
Afghanistan,1 Afghanistan,Kabul
Algeria,3 Algeria,"Batna, Bchar, Skikda"
American Samoa,1 American Samoa,Tafuna
Angola,2 Angola,"Benguela, Namibe"
Anguilla,1 Anguilla,South Hill
Argentina,13 Argentina,"Almirante Brown, Avellaneda, Baha Blanca, Crdoba, Escobar, Ezeiza, La Plata, Merlo, Quilmes, San Miguel de Tucumn, Santa F, Tandil, Vicente Lpez"
Armenia,1 Armenia,Yerevan
Australia,1 Australia,Woodridge
Austria,3 Austria,"Graz, Linz, Salzburg"
Azerbaijan,2 Azerbaijan,"Baku, Sumqayit"
Bahrain,1 Bahrain,al-Manama
Bangladesh,3 Bangladesh,"Dhaka, Jamalpur, Tangail"
Belarus,2 Belarus,"Mogiljov, Molodetno"
Bolivia,2 Bolivia,"El Alto, Sucre"
Brazil,28 Brazil,"Alvorada, Angra dos Reis, Anpolis, Aparecida de Goinia, Araatuba, Bag, Belm, Blumenau, Boa Vista, Braslia, Goinia, Guaruj, guas Lindas de Gois, Ibirit, Juazeiro do Norte, Juiz de Fora, Luzinia, Maring, Po, Poos de Caldas, Rio Claro, Santa Brbara dOeste, Santo Andr, So Bernardo do Campo, So Leopoldo"
Brunei,1 Brunei,Bandar Seri Begawan
Bulgaria,2 Bulgaria,"Ruse, Stara Zagora"
Cambodia,2 Cambodia,"Battambang, Phnom Penh"
Cameroon,2 Cameroon,"Bamenda, Yaound"
Canada,7 Canada,"Gatineau, Halifax, Lethbridge, London, Oshawa, Richmond Hill, Vancouver"
Chad,1 Chad,NDjamna
Chile,3 Chile,"Antofagasta, Coquimbo, Rancagua"
China,53 China,"Baicheng, Baiyin, Binzhou, Changzhou, Datong, Daxian, Dongying, Emeishan, Enshi, Ezhou, Fuyu, Fuzhou, Haining, Hami, Hohhot, Huaian, Jinchang, Jining, Jinzhou, Junan, Korla, Laiwu, Laohekou, Lengshuijiang, Leshan"
Colombia,6 Colombia,"Buenaventura, Dos Quebradas, Florencia, Pereira, Sincelejo, Sogamoso"
"Congo, The Democratic Republic of the",2 "Congo, The Democratic Republic of the","Lubumbashi, Mwene-Ditu"
Czech Republic,1 Czech Republic,Olomouc
Dominican Republic,3 Dominican Republic,"La Romana, San Felipe de Puerto Plata, Santiago de los Caballeros"
Ecuador,3 Ecuador,"Loja, Portoviejo, Robamba"
Egypt,6 Egypt,"Bilbays, Idfu, Mit Ghamr, Qalyub, Sawhaj, Shubra al-Khayma"
Estonia,1 Estonia,Tartu
Ethiopia,1 Ethiopia,Addis Abeba
Faroe Islands,1 Faroe Islands,Trshavn
Finland,1 Finland,Oulu
France,4 France,"Brest, Le Mans, Toulon, Toulouse"
French Guiana,1 French Guiana,Cayenne
French Polynesia,2 French Polynesia,"Faaa, Papeete"
Gambia,1 Gambia,Banjul
Germany,7 Germany,"Duisburg, Erlangen, Halle/Saale, Mannheim, Saarbrcken, Siegen, Witten"
Greece,2 Greece,"Athenai, Patras"
Greenland,1 Greenland,Nuuk
Holy See (Vatican City State),1 Holy See (Vatican City State),Citt del Vaticano
Hong Kong,1 Hong Kong,Kowloon and New Kowloon
Hungary,1 Hungary,Szkesfehrvr
India,60 India,"Adoni, Ahmadnagar, Allappuzha (Alleppey), Ambattur, Amroha, Balurghat, Berhampore (Baharampur), Bhavnagar, Bhilwara, Bhimavaram, Bhopal, Bhusawal, Bijapur, Chandrapur, Chapra, Dhule (Dhulia), Etawah, Firozabad, Gandhinagar, Gulbarga, Haldia, Halisahar, Hoshiarpur, Hubli-Dharwad, Jaipur"
Indonesia,14 Indonesia,"Cianjur, Ciomas, Ciparay, Gorontalo, Jakarta, Lhokseumawe, Madiun, Pangkal Pinang, Pemalang, Pontianak, Probolinggo, Purwakarta, Surakarta, Tegal"
Iran,8 Iran,"Arak, Esfahan, Kermanshah, Najafabad, Qomsheh, Shahr-e Kord, Sirjan, Tabriz"
Iraq,1 Iraq,Mosul
Israel,4 Israel,"Ashdod, Ashqelon, Bat Yam, Tel Aviv-Jaffa"
Italy,7 Italy,"Alessandria, Bergamo, Brescia, Brindisi, Livorno, Syrakusa, Udine"
Japan,31 Japan,"Akishima, Fukuyama, Higashiosaka, Hino, Hiroshima, Isesaki, Iwaki, Iwakuni, Iwatsuki, Izumisano, Kakamigahara, Kamakura, Kanazawa, Koriyama, Kurashiki, Kuwana, Matsue, Miyakonojo, Nagareyama, Okayama, Okinawa, Omiya, Onomichi, Otsu, Sagamihara"
Kazakstan,2 Kazakstan,"Pavlodar, Zhezqazghan"
Kenya,2 Kenya,"Kisumu, Nyeri"
Kuwait,1 Kuwait,Jalib al-Shuyukh
Latvia,2 Latvia,"Daugavpils, Liepaja"
Liechtenstein,1 Liechtenstein,Vaduz
Lithuania,1 Lithuania,Vilnius
Madagascar,1 Madagascar,Mahajanga
Malawi,1 Malawi,Lilongwe
Malaysia,3 Malaysia,"Ipoh, Kuching, Sungai Petani"
Mexico,30 Mexico,"Acua, Allende, Atlixco, Carmen, Celaya, Coacalco de Berriozbal, Coatzacoalcos, Cuauhtmoc, Cuautla, Cuernavaca, El Fuerte, Guadalajara, Hidalgo, Huejutla de Reyes, Huixquilucan, Jos Azueta, Jurez, La Paz, Matamoros, Mexicali, Monclova, Nezahualcyotl, Pachuca de Soto, Salamanca, San Felipe del Progreso"
Moldova,1 Moldova,Chisinau
Morocco,3 Morocco,"Beni-Mellal, Nador, Sal"
Mozambique,3 Mozambique,"Beira, Naala-Porto, Tete"
Myanmar,2 Myanmar,"Monywa, Myingyan"
Nauru,1 Nauru,Yangor
Nepal,1 Nepal,Birgunj
Netherlands,5 Netherlands,"Amersfoort, Apeldoorn, Ede, Emmen, s-Hertogenbosch"
New Zealand,1 New Zealand,Hamilton
Nigeria,13 Nigeria,"Benin City, Deba Habe, Effon-Alaiye, Ife, Ikerre, Ilorin, Kaduna, Ogbomosho, Ondo, Owo, Oyo, Sokoto, Zaria"
North Korea,1 North Korea,Pyongyang
Oman,2 Oman,"Masqat, Salala"
Pakistan,5 Pakistan,"Dadu, Mandi Bahauddin, Mardan, Okara, Shikarpur"
Paraguay,3 Paraguay,"Asuncin, Ciudad del Este, San Lorenzo"
Peru,4 Peru,"Callao, Hunuco, Lima, Sullana"
Philippines,20 Philippines,"Baybay, Bayugan, Bislig, Cabuyao, Cavite, Davao, Gingoog, Hagonoy, Iligan, Imus, Lapu-Lapu, Mandaluyong, Ozamis, Santa Rosa, Taguig, Talavera, Tanauan, Tanza, Tarlac, Tuguegarao"
Poland,8 Poland,"Bydgoszcz, Czestochowa, Jastrzebie-Zdrj, Kalisz, Lublin, Plock, Tychy, Wroclaw"
Puerto Rico,2 Puerto Rico,"Arecibo, Ponce"
Romania,2 Romania,"Botosani, Bucuresti"
Runion,1 Runion,Saint-Denis
Russian Federation,28 Russian Federation,"Atinsk, Balaiha, Dzerzinsk, Elista, Ivanovo, Jaroslavl, Jelets, Kaliningrad, Kamyin, Kirovo-Tepetsk, Kolpino, Korolev, Kurgan, Kursk, Lipetsk, Ljubertsy, Maikop, Moscow, Nabereznyje Telny, Niznekamsk, Novoterkassk, Pjatigorsk, Serpuhov, Smolensk, Syktyvkar"
Saint Vincent and the Grenadines,1 Saint Vincent and the Grenadines,Kingstown
Saudi Arabia,5 Saudi Arabia,"Abha, al-Hawiya, al-Qatif, Jedda, Tabuk"
Senegal,1 Senegal,Ziguinchor
Slovakia,1 Slovakia,Bratislava
South Africa,11 South Africa,"Boksburg, Botshabelo, Chatsworth, Johannesburg, Kimberley, Klerksdorp, Newcastle, Paarl, Rustenburg, Soshanguve, Springs"
South Korea,5 South Korea,"Cheju, Kimchon, Naju, Tonghae, Uijongbu"
Spain,5 Spain,"A Corua (La Corua), Donostia-San Sebastin, Gijn, Ourense (Orense), Santiago de Compostela"
Sri Lanka,1 Sri Lanka,Jaffna
Sudan,2 Sudan,"al-Qadarif, Omdurman"
Sweden,1 Sweden,Malm
Switzerland,3 Switzerland,"Basel, Bern, Lausanne"
Taiwan,10 Taiwan,"Changhwa, Chiayi, Chungho, Fengshan, Hsichuh, Lungtan, Nantou, Tanshui, Touliu, Tsaotun"
Tanzania,3 Tanzania,"Mwanza, Tabora, Zanzibar"
Thailand,3 Thailand,"Nakhon Sawan, Pak Kret, Songkhla"
Tonga,1 Tonga,Nukualofa
Tunisia,1 Tunisia,Sousse
Turkey,15 Turkey,"Adana, Balikesir, Batman, Denizli, Eskisehir, Gaziantep, Inegl, Kilis, Ktahya, Osmaniye, Sivas, Sultanbeyli, Tarsus, Tokat, Usak"
Turkmenistan,1 Turkmenistan,Ashgabat
Tuvalu,1 Tuvalu,Funafuti
Ukraine,6 Ukraine,"Kamjanets-Podilskyi, Konotop, Mukateve, ostka, Simferopol, Sumy"
United Arab Emirates,3 United Arab Emirates,"Abu Dhabi, al-Ayn, Sharja"
United Kingdom,8 United Kingdom,"Bradford, Dundee, London, Southampton, Southend-on-Sea, Southport, Stockport, York"
United States,35 United States,"Akron, Arlington, Augusta-Richmond County, Aurora, Bellevue, Brockton, Cape Coral, Citrus Heights, Clarksville, Compton, Dallas, Dayton, El Monte, Fontana, Garden Grove, Garland, Grand Prairie, Greensboro, Joliet, Kansas City, Lancaster, Laredo, Lincoln, Manchester, Memphis"
Venezuela,7 Venezuela,"Barcelona, Caracas, Cuman, Maracabo, Ocumare del Tuy, Valencia, Valle de la Pascua"
Vietnam,6 Vietnam,"Cam Ranh, Haiphong, Hanoi, Nam Dinh, Nha Trang, Vinh"
"Virgin Islands, U.S.",1 "Virgin Islands, U.S.",Charlotte Amalie
Yemen,4 Yemen,"Aden, Hodeida, Sanaa, Taizz"
Yugoslavia,2 Yugoslavia,"Kragujevac, Novi Sad"
Zambia,1 Zambia,Kitwe

8
tests/playwright/fixtures/expectedData.txt

@ -1,4 +1,4 @@
Address,District,PostalCode,Phone,Location,Customers,Staffs,City Address,District,PostalCode,Phone,Location,Customer List,Staff List,City
1661 Abha Drive,Tamil Nadu,14400,270456873752,"{""x"":78.8214191,""y"":10.3812871}",1,0,Pudukkottai 1661 Abha Drive,Tamil Nadu,14400,270456873752,"{""x"":78.8214191,""y"":10.3812871}",1,,Pudukkottai
1993 Tabuk Lane,Tamil Nadu,64221,648482415405,"{""x"":80.1270701,""y"":12.9246028}",1,0,Tambaram 1993 Tabuk Lane,Tamil Nadu,64221,648482415405,"{""x"":80.1270701,""y"":12.9246028}",2,,Tambaram
381 Kabul Way,Taipei,87272,55477302294,"{""x"":0,""y"":0}",1,0,Hsichuh 381 Kabul Way,Taipei,87272,55477302294,"{""x"":0,""y"":0}",2,,Hsichuh

38
tests/playwright/fixtures/expectedDataSqlite.txt

@ -1,19 +1,19 @@
Address,District,PostalCode,Phone,Customers,Staffs,City Address,District,PostalCode,Phone,Customer List,Staff List,City
1013 Tabuk Boulevard," ",96203," ",1,0,Kanchrapara 1013 Tabuk Boulevard," ",96203," ",2,,Kanchrapara
1168 Najafabad Parkway," ",40301," ",1,0,Kabul 1168 Najafabad Parkway," ",40301," ",1,,Kabul
1294 Firozabad Drive," ",70618," ",1,0,Pingxiang 1294 Firozabad Drive," ",70618," ",2,,Pingxiang
1342 Abha Boulevard," ",10714," ",1,0,Bucuresti 1342 Abha Boulevard," ",10714," ",2,,Bucuresti
1368 Maracabo Boulevard," ",32716," ",1,0,South Hill 1368 Maracabo Boulevard," ",32716," ",2,,South Hill
1427 Tabuk Place," ",31342," ",1,0,Cape Coral 1427 Tabuk Place," ",31342," ",2,,Cape Coral
1519 Santiago de los Caballeros Loop," ",22025," ",1,0,Mwene-Ditu 1519 Santiago de los Caballeros Loop," ",22025," ",2,,Mwene-Ditu
1661 Abha Drive," ",14400," ",1,0,Pudukkottai 1661 Abha Drive," ",14400," ",1,,Pudukkottai
17 Kabul Boulevard," ",38594," ",1,0,Nagareyama 17 Kabul Boulevard," ",38594," ",1,,Nagareyama
1838 Tabriz Lane," ",1195," ",1,0,Dhaka 1838 Tabriz Lane," ",1195," ",1,,Dhaka
1888 Kabul Drive," ",20936," ",1,0,Ife 1888 Kabul Drive," ",20936," ",1,,Ife
1892 Nabereznyje Telny Lane," ",28396," ",1,0,Tafuna 1892 Nabereznyje Telny Lane," ",28396," ",2,,Tafuna
1993 Tabuk Lane," ",64221," ",1,0,Tambaram 1993 Tabuk Lane," ",64221," ",2,,Tambaram
217 Botshabelo Place," ",49521," ",1,0,Davao 217 Botshabelo Place," ",49521," ",2,,Davao
381 Kabul Way," ",87272," ",1,0,Hsichuh 381 Kabul Way," ",87272," ",2,,Hsichuh
44 Najafabad Way," ",61391," ",1,0,Donostia-San Sebastin 44 Najafabad Way," ",61391," ",2,,Donostia-San Sebastin
48 Maracabo Place," ",1570," ",1,0,Talavera 48 Maracabo Place," ",1570," ",1,,Talavera
669 Firozabad Loop," ",92265," ",1,0,al-Ayn 669 Firozabad Loop," ",92265," ",1,,al-Ayn

3
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -139,7 +139,6 @@ export class ColumnPageObject extends BasePage {
.locator(`.ant-select-item`, { .locator(`.ant-select-item`, {
hasText: childColumn, hasText: childColumn,
}) })
.last()
.click(); .click();
break; break;
case 'Rollup': case 'Rollup':
@ -163,7 +162,7 @@ export class ColumnPageObject extends BasePage {
.nth(0) .nth(0)
.click(); .click();
break; break;
case 'Links': case 'LinkToAnotherRecord':
await this.get() await this.get()
.locator('.nc-ltar-relation-type >> .ant-radio') .locator('.nc-ltar-relation-type >> .ant-radio')
.nth(relationType === 'Has Many' ? 0 : 1) .nth(relationType === 'Has Many' ? 0 : 1)

18
tests/playwright/pages/Dashboard/Grid/index.ts

@ -296,15 +296,15 @@ export class GridPage extends BasePage {
await expect(await this.rootPage.locator('text=Insert New Row')).not.toBeVisible(); await expect(await this.rootPage.locator('text=Insert New Row')).not.toBeVisible();
// in cell-add // in cell-add
await this.cell.get({ index: 0, columnHeader: 'Cities' }).hover(); await this.cell.get({ index: 0, columnHeader: 'City List' }).hover();
await expect( await expect(
await this.cell.get({ index: 0, columnHeader: 'Cities' }).locator('.nc-action-icon.nc-plus') await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon.nc-plus')
).not.toBeVisible(); ).not.toBeVisible();
// expand row // expand row
await this.cell.get({ index: 0, columnHeader: 'Cities' }).hover(); await this.cell.get({ index: 0, columnHeader: 'City List' }).hover();
await expect( await expect(
await this.cell.get({ index: 0, columnHeader: 'Cities' }).locator('.nc-action-icon >> nth=0') await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon >> nth=0')
).not.toBeVisible(); ).not.toBeVisible();
} }
@ -327,9 +327,15 @@ export class GridPage extends BasePage {
await expect(await this.rootPage.locator('text=Insert New Row')).toBeVisible(); await expect(await this.rootPage.locator('text=Insert New Row')).toBeVisible();
// in cell-add // in cell-add
await this.cell.get({ index: 0, columnHeader: 'Cities' }).hover(); await this.cell.get({ index: 0, columnHeader: 'City List' }).hover();
await expect( await expect(
await this.cell.get({ index: 0, columnHeader: 'Cities' }).locator('.nc-action-icon.nc-plus') await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon.nc-plus')
).toBeVisible();
// expand row
await this.cell.get({ index: 0, columnHeader: 'City List' }).hover();
await expect(
await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon.nc-arrow-expand')
).toBeVisible(); ).toBeVisible();
} }

75
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -9,7 +9,6 @@ import { RatingCellPageObject } from './RatingCell';
import { DateCellPageObject } from './DateCell'; import { DateCellPageObject } from './DateCell';
import { DateTimeCellPageObject } from './DateTimeCell'; import { DateTimeCellPageObject } from './DateTimeCell';
import { GeoDataCellPageObject } from './GeoDataCell'; import { GeoDataCellPageObject } from './GeoDataCell';
import { getTextExcludeIconText } from '../../../../tests/utils/general';
import { YearCellPageObject } from './YearCell'; import { YearCellPageObject } from './YearCell';
import { TimeCellPageObject } from './TimeCell'; import { TimeCellPageObject } from './TimeCell';
@ -83,9 +82,9 @@ export class CellPageObject extends BasePage {
} }
async inCellExpand({ index, columnHeader }: CellProps) { async inCellExpand({ index, columnHeader }: CellProps) {
// await this.get({ index, columnHeader }).hover(); await this.get({ index, columnHeader }).hover();
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => this.get({ index, columnHeader }).locator('.nc-datatype-link').click(), uiAction: () => this.get({ index, columnHeader }).locator('.nc-action-icon >> nth=0').click(),
requestUrlPathToMatch: '/api/v1/db/data/noco/', requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],
}); });
@ -274,51 +273,41 @@ export class CellPageObject extends BasePage {
async verifyVirtualCell({ async verifyVirtualCell({
index, index,
columnHeader, columnHeader,
type,
count, count,
value, value,
verifyChildList = false, verifyChildList = false,
options,
}: CellProps & { }: CellProps & {
count?: number; count?: number;
type?: string; value: string[];
value?: string[];
verifyChildList?: boolean; verifyChildList?: boolean;
options?: { singular?: string; plural?: string };
}) { }) {
// const count = value.length;
const cell = await this.get({ index, columnHeader }); const cell = await this.get({ index, columnHeader });
const linkText = await cell.locator('.nc-datatype-link'); const chips = cell.locator('.chips > .chip');
await cell.scrollIntoViewIfNeeded();
// lazy load- give enough time for cell to load await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.rootPage.waitForTimeout(1000);
if (type === 'bt') { // verify chip count & contents
const chips = cell.locator('.chips > .chip'); if (count) await expect(chips).toHaveCount(count);
await expect(await chips.count()).toBe(count);
// verify only the elements that are passed in
for (let i = 0; i < value.length; ++i) { for (let i = 0; i < value.length; ++i) {
await chips.nth(i).locator('.name').waitFor({ state: 'visible' }); await chips.nth(i).locator('.name').waitFor({ state: 'visible' });
await chips.nth(i).locator('.name').scrollIntoViewIfNeeded(); await chips.nth(i).locator('.name').scrollIntoViewIfNeeded();
await expect(await chips.nth(i).locator('.name')).toHaveText(value[i]); await chips.nth(i).locator('.name').waitFor({ state: 'visible' });
} const chipText = await chips.nth(i).locator('.name').textContent();
return; expect(value).toContain(chipText);
}
// verify chip count & contents
if (count) {
await expect(await cell.innerText()).toContain(`${count} ${count === 1 ? options.singular : options.plural}`);
} }
if (verifyChildList) { if (verifyChildList) {
// open child list // open child list
await this.get({ index, columnHeader }).hover(); await this.get({ index, columnHeader }).hover();
const arrow_expand = await this.get({ index, columnHeader }).locator('.nc-arrow-expand');
// arrow expand doesn't exist for bt columns // arrow expand doesn't exist for bt columns
if (await linkText.count()) { if (await arrow_expand.count()) {
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => linkText.click(), uiAction: () => arrow_expand.click(),
requestUrlPathToMatch: '/api/v1/db', requestUrlPathToMatch: '/api/v1/db',
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],
}); });
@ -338,24 +327,12 @@ export class CellPageObject extends BasePage {
async unlinkVirtualCell({ index, columnHeader }: CellProps) { async unlinkVirtualCell({ index, columnHeader }: CellProps) {
const cell = this.get({ index, columnHeader }); const cell = this.get({ index, columnHeader });
const isLink = await cell.locator('.nc-datatype-link').count();
// Count will be 0 for BT columns
if (!isLink) {
await cell.click(); await cell.click();
await cell.locator('.nc-icon.unlink-icon').click();
}
// For HM/MM columns
else {
await cell.locator('.nc-datatype-link').click();
await this.waitForResponse({ await this.waitForResponse({
uiAction: () => this.rootPage.locator(`[data-testid="nc-child-list-icon-unlink"]`).first().click(), uiAction: () => cell.locator('.unlink-icon').first().click(),
requestUrlPathToMatch: '/api/v1/db/data/noco/', requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],
}); });
await this.rootPage.keyboard.press('Escape');
}
} }
async verifyRoleAccess(param: { role: string }) { async verifyRoleAccess(param: { role: string }) {
@ -375,27 +352,19 @@ export class CellPageObject extends BasePage {
); );
// virtual cell // virtual cell
const vCell = await this.get({ index: 0, columnHeader: 'Cities' }); const vCell = await this.get({ index: 0, columnHeader: 'City List' });
await vCell.hover(); await vCell.hover();
// in-cell add // in-cell add
await expect(await vCell.locator('.nc-action-icon.nc-plus:visible')).toHaveCount( await expect(await vCell.locator('.nc-action-icon.nc-plus:visible')).toHaveCount(
param.role === 'creator' || param.role === 'editor' ? 1 : 0 param.role === 'creator' || param.role === 'editor' ? 1 : 0
); );
// in-cell expand (all have access) // in-cell expand (all have access)
// PR8504 await expect(await vCell.locator('.nc-action-icon.nc-arrow-expand:visible')).toHaveCount(1);
// await expect(await vCell.locator('.nc-action-icon.nc-arrow-expand:visible')).toHaveCount(1); await vCell.click();
const linkText = await getTextExcludeIconText(vCell);
expect(linkText).toContain('1 City');
// PR8504
// await vCell.click();
// unlink // unlink
// PR8504 await expect(await vCell.locator('.nc-icon.unlink-icon:visible')).toHaveCount(
// await expect(await vCell.locator('.nc-icon.unlink-icon:visible')).toHaveCount( param.role === 'creator' || param.role === 'editor' ? 1 : 0
// param.role === 'creator' || param.role === 'editor' ? 1 : 0 );
// );
} }
async copyToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) { async copyToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) {

4
tests/playwright/quickTests/commonTest.ts

@ -99,7 +99,7 @@ const quickVerify = async ({
rating: recordsVirtualCells.Rating, rating: recordsVirtualCells.Rating,
}); });
// Links // LinkToAnotherRecord
await dashboard.grid.cell.verifyVirtualCell({ await dashboard.grid.cell.verifyVirtualCell({
index: cellIndex, index: cellIndex,
columnHeader: 'Actor', columnHeader: 'Actor',
@ -121,7 +121,7 @@ const quickVerify = async ({
value: recordsVirtualCells.Computation, value: recordsVirtualCells.Computation,
}); });
// Links // LinkToAnotherRecord
await dashboard.grid.cell.verifyVirtualCell({ await dashboard.grid.cell.verifyVirtualCell({
index: cellIndex, index: cellIndex,
columnHeader: 'Producer', columnHeader: 'Producer',

6
tests/playwright/tests/db/columns/columnAttachments.spec.ts

@ -92,11 +92,9 @@ test.describe('Attachment column', () => {
const rows = csvArray.slice(1); const rows = csvArray.slice(1);
const cells = rows[10].split(','); const cells = rows[10].split(',');
await expect(columns).toBe('Country,Cities,testAttach'); await expect(columns).toBe('Country,City List,testAttach');
await expect(cells[0]).toBe('Bahrain'); await expect(cells[0]).toBe('Bahrain');
// PR8504 await expect(cells[1]).toBe('al-Manama');
// await expect(cells[1]).toBe('al-Manama');
await expect(cells[1]).toBe('1');
await expect(cells[2].includes('5.json(http://localhost:8080/download/')).toBe(true); await expect(cells[2].includes('5.json(http://localhost:8080/download/')).toBe(true);
}); });

2
tests/playwright/tests/db/columns/columnBarcode.spec.ts

@ -67,7 +67,7 @@ test.describe('Virtual Columns', () => {
// and compare the base64 encoded codes/src attributes for the first 3 rows. // and compare the base64 encoded codes/src attributes for the first 3 rows.
// Column data from City table (Sakila DB) // Column data from City table (Sakila DB)
/** /**
* City LastUpdate Addresses Country * City LastUpdate Address List Country
* A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain * A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain
* Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia * Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia
* Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates * Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates

6
tests/playwright/tests/db/columns/columnFormula.spec.ts

@ -6,7 +6,7 @@ import { isPg, isSqlite } from '../../../setup/db';
// Add formula to be verified here & store expected results for 5 rows // Add formula to be verified here & store expected results for 5 rows
// Column data from City table (Sakila DB) // Column data from City table (Sakila DB)
/** /**
* City LastUpdate Addresses Country * City LastUpdate Address List Country
* A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain * A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain
* Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia * Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia
* Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates * Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates
@ -131,8 +131,8 @@ const formulaDataByDbType = (context: NcContext) => [
result: isPg(context) ? ['false', 'false', 'false', 'false', 'false'] : ['0', '0', '0', '0', '0'], result: isPg(context) ? ['false', 'false', 'false', 'false', 'false'] : ['0', '0', '0', '0', '0'],
}, },
{ {
formula: `IF((SEARCH({City}, "Ad") != 0), "2.0","WRONG")`, formula: `IF((SEARCH({Address List}, "Parkway") != 0), "2.0","WRONG")`,
result: ['WRONG', 'WRONG', 'WRONG', 'WRONG', '2.0'], result: ['WRONG', 'WRONG', 'WRONG', '2.0', '2.0'],
}, },
// additional tests for formula case-insensitivity // additional tests for formula case-insensitivity

46
tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts

@ -31,13 +31,13 @@ test.describe('LTAR create & update', () => {
// Create LTAR-HM column // Create LTAR-HM column
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Link1-2hm', title: 'Link1-2hm',
type: 'Links', type: 'LinkToAnotherRecord',
childTable: 'Sheet2', childTable: 'Sheet2',
relationType: 'Has Many', relationType: 'Has Many',
}); });
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Link1-2mm', title: 'Link1-2mm',
type: 'Links', type: 'LinkToAnotherRecord',
childTable: 'Sheet2', childTable: 'Sheet2',
relationType: 'Many To many', relationType: 'Many To many',
}); });
@ -46,7 +46,7 @@ test.describe('LTAR create & update', () => {
await dashboard.treeView.openTable({ title: 'Sheet2', networkResponse: false }); await dashboard.treeView.openTable({ title: 'Sheet2', networkResponse: false });
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Link2-1hm', title: 'Link2-1hm',
type: 'Links', type: 'LinkToAnotherRecord',
childTable: 'Sheet1', childTable: 'Sheet1',
relationType: 'Has Many', relationType: 'Has Many',
}); });
@ -67,7 +67,7 @@ test.describe('LTAR create & update', () => {
type: 'belongsTo', type: 'belongsTo',
}); });
await dashboard.expandedForm.fillField({ await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1s', columnTitle: 'Sheet1 List',
value: '1a', value: '1a',
type: 'manyToMany', type: 'manyToMany',
}); });
@ -84,7 +84,7 @@ test.describe('LTAR create & update', () => {
await dashboard.linkRecord.select('1b'); await dashboard.linkRecord.select('1b');
await dashboard.grid.cell.inCellAdd({ await dashboard.grid.cell.inCellAdd({
index: 1, index: 1,
columnHeader: 'Sheet1s', columnHeader: 'Sheet1 List',
}); });
await dashboard.linkRecord.select('1b'); await dashboard.linkRecord.select('1b');
await dashboard.grid.cell.inCellAdd({ await dashboard.grid.cell.inCellAdd({
@ -102,7 +102,7 @@ test.describe('LTAR create & update', () => {
type: 'belongsTo', type: 'belongsTo',
}); });
await dashboard.expandedForm.fillField({ await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1s', columnTitle: 'Sheet1 List',
value: '1c', value: '1c',
type: 'manyToMany', type: 'manyToMany',
}); });
@ -123,10 +123,10 @@ test.describe('LTAR create & update', () => {
const expected = [ const expected = [
[['1a'], ['1b'], ['1c']], [['1a'], ['1b'], ['1c']],
[['1 Sheet1'], ['1 Sheet1'], ['1 Sheet1']], [['1a'], ['1b'], ['1c']],
[['1 Sheet1'], ['1 Sheet1'], ['1 Sheet1']], [['1a'], ['1b'], ['1c']],
]; ];
const colHeaders = ['Sheet1', 'Sheet1s', 'Link2-1hm']; const colHeaders = ['Sheet1', 'Sheet1 List', 'Link2-1hm'];
// verify LTAR cell values // verify LTAR cell values
for (let i = 0; i < expected.length; i++) { for (let i = 0; i < expected.length; i++) {
@ -136,8 +136,6 @@ test.describe('LTAR create & update', () => {
columnHeader: colHeaders[i], columnHeader: colHeaders[i],
count: 1, count: 1,
value: expected[i][j], value: expected[i][j],
type: i === 0 ? 'bt' : undefined,
options: { singular: 'Sheet1', plural: 'Sheet1s' },
}); });
} }
} }
@ -146,8 +144,8 @@ test.describe('LTAR create & update', () => {
await dashboard.treeView.openTable({ title: 'Sheet1' }); await dashboard.treeView.openTable({ title: 'Sheet1' });
const expected2 = [ const expected2 = [
[['1 Sheet2'], ['1 Sheet2'], ['1 Sheet2']], [['2a'], ['2b'], ['2c']],
[['1 Sheet2'], ['1 Sheet2'], ['1 Sheet2']], [['2a'], ['2b'], ['2c']],
[['2a'], ['2b'], ['2c']], [['2a'], ['2b'], ['2c']],
]; ];
const colHeaders2 = ['Link1-2hm', 'Link1-2mm', 'Sheet2']; const colHeaders2 = ['Link1-2hm', 'Link1-2mm', 'Sheet2'];
@ -160,13 +158,11 @@ test.describe('LTAR create & update', () => {
columnHeader: colHeaders2[i], columnHeader: colHeaders2[i],
count: 1, count: 1,
value: expected2[i][j], value: expected2[i][j],
type: i === 2 ? 'bt' : undefined,
options: { singular: 'Sheet2', plural: 'Sheet2s' },
}); });
} }
} }
// Unlink LTAR cells // verify LTAR cell values
for (let i = 0; i < expected2.length; i++) { for (let i = 0; i < expected2.length; i++) {
for (let j = 0; j < expected2[i].length; j++) { for (let j = 0; j < expected2[i].length; j++) {
await dashboard.grid.cell.unlinkVirtualCell({ await dashboard.grid.cell.unlinkVirtualCell({
@ -192,7 +188,7 @@ test.describe('LTAR create & update', () => {
Country: string; Country: string;
formula?: string; formula?: string;
SLT?: string; SLT?: string;
Cities: string[]; 'City List': string[];
}; };
}) { }) {
await dashboard.grid.cell.verify({ await dashboard.grid.cell.verify({
@ -209,9 +205,9 @@ test.describe('LTAR create & update', () => {
} }
await dashboard.grid.cell.verifyVirtualCell({ await dashboard.grid.cell.verifyVirtualCell({
index: param.index, index: param.index,
columnHeader: 'Cities', columnHeader: 'City List',
count: param.value['Cities'].length, count: param.value['City List'].length,
value: param.value['Cities'], value: param.value['City List'],
}); });
if (param.value.SLT) { if (param.value.SLT) {
await dashboard.grid.cell.verify({ await dashboard.grid.cell.verify({
@ -241,14 +237,14 @@ test.describe('LTAR create & update', () => {
index: 0, index: 0,
value: { value: {
Country: 'Afghanistan', Country: 'Afghanistan',
Cities: ['Kabul'], 'City List': ['Kabul'],
}, },
}); });
await verifyRow({ await verifyRow({
index: 1, index: 1,
value: { value: {
Country: 'Algeria', Country: 'Algeria',
Cities: ['Batna', 'Bchar', 'Skikda'], 'City List': ['Batna', 'Bchar', 'Skikda'],
}, },
}); });
@ -274,7 +270,7 @@ test.describe('LTAR create & update', () => {
index: 0, index: 0,
value: { value: {
Country: 'Afghanistan', Country: 'Afghanistan',
Cities: ['Kabul'], 'City List': ['Kabul'],
SLT: 'test', SLT: 'test',
formula: 'Afghanistan test', formula: 'Afghanistan test',
}, },
@ -290,7 +286,7 @@ test.describe('LTAR create & update', () => {
index: 0, index: 0,
value: { value: {
Country: 'Afghanistan2', Country: 'Afghanistan2',
Cities: ['Kabul'], 'City List': ['Kabul'],
SLT: 'test', SLT: 'test',
formula: 'Afghanistan2 test', formula: 'Afghanistan2 test',
}, },
@ -305,7 +301,7 @@ test.describe('LTAR create & update', () => {
index: 0, index: 0,
value: { value: {
Country: 'Afghanistan2', Country: 'Afghanistan2',
Cities: ['Kabul'], 'City List': ['Kabul'],
SLT: '', SLT: '',
formula: 'Afghanistan2', formula: 'Afghanistan2',
}, },

14
tests/playwright/tests/db/columns/columnLookupRollup.spec.ts

@ -15,7 +15,7 @@ test.describe('Virtual columns', () => {
// close 'Team & Auth' tab // close 'Team & Auth' tab
// await dashboard.closeTab({ title: "Team & Auth" }); // await dashboard.closeTab({ title: "Team & Auth" });
const countryList = ['Spain', 'Saudi Arabia', 'United Arab Emirates', 'Mexico', 'Turkey']; const pinCode = ['4166', '77459', '41136', '8268', '33463'];
const cityCount = ['1', '3', '1', '2', '1']; const cityCount = ['1', '3', '1', '2', '1'];
await dashboard.treeView.openTable({ title: 'City' }); await dashboard.treeView.openTable({ title: 'City' });
@ -23,14 +23,14 @@ test.describe('Virtual columns', () => {
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Lookup', title: 'Lookup',
type: 'Lookup', type: 'Lookup',
childTable: 'Country', childTable: 'Address List',
childColumn: 'Country', childColumn: 'PostalCode',
}); });
for (let i = 0; i < countryList.length; i++) { for (let i = 0; i < pinCode.length; i++) {
await dashboard.grid.cell.verify({ await dashboard.grid.cell.verify({
index: i, index: i,
columnHeader: 'Lookup', columnHeader: 'Lookup',
value: countryList[i], value: pinCode[i],
}); });
} }
await dashboard.closeTab({ title: 'City' }); await dashboard.closeTab({ title: 'City' });
@ -40,11 +40,11 @@ test.describe('Virtual columns', () => {
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Rollup', title: 'Rollup',
type: 'Rollup', type: 'Rollup',
childTable: 'Cities', childTable: 'City List',
childColumn: 'CityId', childColumn: 'CityId',
rollupType: 'count', rollupType: 'count',
}); });
for (let i = 0; i < countryList.length; i++) { for (let i = 0; i < pinCode.length; i++) {
await dashboard.grid.cell.verify({ await dashboard.grid.cell.verify({
index: i, index: i,
columnHeader: 'Rollup', columnHeader: 'Rollup',

8
tests/playwright/tests/db/columns/columnLtarDragdrop.spec.ts

@ -64,7 +64,7 @@ test.describe('Test table', () => {
await page.reload(); await page.reload();
}); });
test('drag drop for Link, lookup creation', async () => { test('drag drop for LTAR, lookup creation', async () => {
await dashboard.treeView.openTable({ title: 'Table0' }); await dashboard.treeView.openTable({ title: 'Table0' });
const src = await dashboard.rootPage.locator(`[data-testid="tree-view-table-draggable-handle-Table1"]`); const src = await dashboard.rootPage.locator(`[data-testid="tree-view-table-draggable-handle-Table1"]`);
const dst = await dashboard.rootPage.locator(`[data-testid="grid-row-0"]`); const dst = await dashboard.rootPage.locator(`[data-testid="grid-row-0"]`);
@ -78,14 +78,14 @@ test.describe('Test table', () => {
const linkTable = await getTextExcludeIconText( const linkTable = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(3) await columnAddModal.locator(`.ant-form-item-control-input`).nth(3)
); );
expect(columnType).toContain('Links'); expect(columnType).toContain('LinkToAnotherRecord');
expect(linkTable).toContain('Table1'); expect(linkTable).toContain('Table1');
// save // save
await columnAddModal.locator(`.ant-btn-primary`).click(); await columnAddModal.locator(`.ant-btn-primary`).click();
// verify if column is created // verify if column is created
await grid.column.verify({ title: 'Table1', isVisible: true }); await grid.column.verify({ title: 'Table1List', isVisible: true });
} }
// drag drop for lookup column creation // drag drop for lookup column creation
@ -103,7 +103,7 @@ test.describe('Test table', () => {
// validate // validate
expect(columnType).toContain('Lookup'); expect(columnType).toContain('Lookup');
expect(linkField).toContain('Table1'); expect(linkField).toContain('Table1List');
expect(childColumn).toContain('Title'); expect(childColumn).toContain('Title');
// save // save

6
tests/playwright/tests/db/columns/columnMenuOperations.spec.ts

@ -79,7 +79,7 @@ test.describe('Column menu operations', () => {
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'InsertAfterColumn1', title: 'InsertAfterColumn1',
type: 'SingleLineText', type: 'SingleLineText',
insertAfterColumnTitle: 'Actors', insertAfterColumnTitle: 'Actor List',
}); });
await dashboard.closeTab({ title: 'Film' }); await dashboard.closeTab({ title: 'Film' });
@ -98,7 +98,7 @@ test.describe('Column menu operations', () => {
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'InsertBeforeColumn1', title: 'InsertBeforeColumn1',
type: 'SingleLineText', type: 'SingleLineText',
insertBeforeColumnTitle: 'Actors', insertBeforeColumnTitle: 'Actor List',
}); });
await dashboard.closeTab({ title: 'Film' }); await dashboard.closeTab({ title: 'Film' });
@ -113,7 +113,7 @@ test.describe('Column menu operations', () => {
}); });
await dashboard.grid.column.hideColumn({ await dashboard.grid.column.hideColumn({
title: 'Actors', title: 'Actor List',
}); });
await dashboard.closeTab({ title: 'Film' }); await dashboard.closeTab({ title: 'Film' });

2
tests/playwright/tests/db/columns/columnQrCode.spec.ts

@ -34,7 +34,7 @@ test.describe('Virtual Columns', () => {
// and compare the base64 encoded codes/src attributes for the first 3 rows. // and compare the base64 encoded codes/src attributes for the first 3 rows.
// Column data from City table (Sakila DB) // Column data from City table (Sakila DB)
/** /**
* City LastUpdate Addresses Country * City LastUpdate Address List Country
* A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain * A Corua (La Corua) 2006-02-15 04:45:25 939 Probolinggo Loop Spain
* Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia * Abha 2006-02-15 04:45:25 733 Mandaluyong Place Saudi Arabia
* Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates * Abu Dhabi 2006-02-15 04:45:25 535 Ahmadnagar Manor United Arab Emirates

15
tests/playwright/tests/db/columns/columnRelationalExtendedTests.spec.ts

@ -23,17 +23,16 @@ test.describe('Relational Columns', () => {
for (let i = 0; i < cityList.length; i++) { for (let i = 0; i < cityList.length; i++) {
await dashboard.grid.cell.verifyVirtualCell({ await dashboard.grid.cell.verifyVirtualCell({
index: i, index: i,
columnHeader: 'Cities', columnHeader: 'City List',
count: cityList[i].length, count: cityList[i].length,
type: 'hm', value: cityList[i],
options: { singular: 'City', plural: 'Cities' },
}); });
} }
// click on expand icon, open child list // click on expand icon, open child list
await dashboard.grid.cell.inCellExpand({ await dashboard.grid.cell.inCellExpand({
index: 0, index: 0,
columnHeader: 'Cities', columnHeader: 'City List',
}); });
await dashboard.childList.verify({ await dashboard.childList.verify({
cardTitle: ['Kabul'], cardTitle: ['Kabul'],
@ -66,7 +65,6 @@ test.describe('Relational Columns', () => {
await dashboard.grid.cell.verifyVirtualCell({ await dashboard.grid.cell.verifyVirtualCell({
index: i, index: i,
columnHeader: 'Country', columnHeader: 'Country',
type: 'bt',
count: countryList[i].length, count: countryList[i].length,
value: countryList[i], value: countryList[i],
}); });
@ -92,17 +90,16 @@ test.describe('Relational Columns', () => {
for (let i = 0; i < filmList.length; i++) { for (let i = 0; i < filmList.length; i++) {
await dashboard.grid.cell.verifyVirtualCell({ await dashboard.grid.cell.verifyVirtualCell({
index: i, index: i,
columnHeader: 'Films', columnHeader: 'Film List',
// Count hardwired to avoid verifying all 19 entries // Count hardwired to avoid verifying all 19 entries
count: 19, count: 19,
type: 'mm', value: filmList[i],
options: { singular: 'Film', plural: 'Films' },
}); });
} }
// click on expand icon, open child list // click on expand icon, open child list
await dashboard.grid.cell.inCellExpand({ await dashboard.grid.cell.inCellExpand({
index: 0, index: 0,
columnHeader: 'Films', columnHeader: 'Film List',
}); });
await dashboard.childList.verify({ await dashboard.childList.verify({
cardTitle: filmList[0], cardTitle: filmList[0],

18
tests/playwright/tests/db/features/erd.spec.ts

@ -188,33 +188,33 @@ test.describe('Erd', () => {
// Verify tables with default config // Verify tables with default config
await erd.verifyColumns({ await erd.verifyColumns({
tableName: `country`, tableName: `country`,
columns: ['country_id', 'country', 'last_update', 'cities'], columns: ['country_id', 'country', 'last_update', 'city_list'],
}); });
await erd.verifyColumns({ await erd.verifyColumns({
tableName: `city`, tableName: `city`,
columns: ['city_id', 'city', 'country_id', 'last_update', 'country', 'addresses'], columns: ['city_id', 'city', 'country_id', 'last_update', 'country', 'address_list'],
}); });
// Verify with PK/FK disabled // Verify with PK/FK disabled
await erd.clickShowPkAndFk(); await erd.clickShowPkAndFk();
await erd.verifyColumns({ await erd.verifyColumns({
tableName: `country`, tableName: `country`,
columns: ['country', 'last_update', 'cities'], columns: ['country', 'last_update', 'city_list'],
}); });
await erd.verifyColumns({ await erd.verifyColumns({
tableName: `city`, tableName: `city`,
columns: ['city', 'last_update', 'country', 'addresses'], columns: ['city', 'last_update', 'country', 'address_list'],
}); });
// Verify with all columns disabled // Verify with all columns disabled
await erd.clickShowColumnNames(); await erd.clickShowColumnNames();
await erd.verifyColumns({ tableName: `country`, columns: ['cities'] }); await erd.verifyColumns({ tableName: `country`, columns: ['city_list'] });
await erd.verifyColumns({ await erd.verifyColumns({
tableName: `city`, tableName: `city`,
columns: ['country', 'addresses'], columns: ['country', 'address_list'],
}); });
// Enable All columns // Enable All columns
@ -311,7 +311,7 @@ test.describe('Erd', () => {
}); });
}); });
const actorTableColumn = ['actor_id', 'first_name', 'last_name', 'last_update', 'films']; const actorTableColumn = ['actor_id', 'first_name', 'last_name', 'last_update', 'film_list'];
const mysqlPaymentTableColumns = [ const mysqlPaymentTableColumns = [
'payment_id', 'payment_id',
@ -338,9 +338,9 @@ const pgPaymentTableColumns = [
'staff', 'staff',
]; ];
const actorLTARColumns = ['filmactors', 'films']; const actorLTARColumns = ['filmactor_list', 'film_list'];
const actorNonPkFkColumns = ['first_name', 'last_name', 'last_update', 'films', 'filmactors']; const actorNonPkFkColumns = ['first_name', 'last_name', 'last_update', 'film_list', 'filmactor_list'];
const paymentLTARColumns = ['customer', 'rental', 'staff']; const paymentLTARColumns = ['customer', 'rental', 'staff'];

4
tests/playwright/tests/db/features/expandedFormUrl.spec.ts

@ -66,7 +66,7 @@ test.describe('Expanded form URL', () => {
title: 'CountryExpand', title: 'CountryExpand',
}); });
await viewObj.toolbar.clickFields(); await viewObj.toolbar.clickFields();
await viewObj.toolbar.fields.click({ title: 'Cities' }); await viewObj.toolbar.fields.click({ title: 'City List' });
} }
// expand row & verify URL // expand row & verify URL
@ -104,7 +104,7 @@ test.describe('Expanded form URL', () => {
url: 'rowId=1', url: 'rowId=1',
}); });
await dashboard.expandedForm.openChildCard({ await dashboard.expandedForm.openChildCard({
column: 'Cities', column: 'City List',
title: 'Kabul', title: 'Kabul',
}); });
await dashboard.rootPage.waitForTimeout(1000); await dashboard.rootPage.waitForTimeout(1000);

40
tests/playwright/tests/db/features/filters.spec.ts

@ -937,23 +937,23 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
// add filter for CityList column // add filter for CityList column
const filterList = [ const filterList = [
{ op: '=', value: '5', rowCount: 5 }, { op: 'is', value: 'Kabul', rowCount: 1 },
{ op: '!=', value: '5', rowCount: 104 }, { op: 'is not', value: 'Kabul', rowCount: 108 },
{ op: '>', value: '5', rowCount: 25 }, { op: 'is like', value: 'bad', rowCount: 2 },
{ op: '<', value: '5', rowCount: 79 }, { op: 'is not like', value: 'bad', rowCount: 107 },
{ op: '>=', value: '5', rowCount: 30 }, { op: 'is blank', value: null, rowCount: 0 },
{ op: '<=', value: '5', rowCount: 84 }, { op: 'is not blank', value: null, rowCount: 109 },
]; ];
await toolbar.clickFilter(); await toolbar.clickFilter();
await toolbar.filter.clickAddFilter(); await toolbar.filter.clickAddFilter();
for (let i = 0; i < filterList.length; i++) { for (let i = 0; i < filterList.length; i++) {
await verifyFilter({ await verifyFilter({
column: 'Cities', column: 'City List',
opType: filterList[i].op, opType: filterList[i].op,
value: filterList[i].value, value: filterList[i].value,
result: { rowCount: filterList[i].rowCount }, result: { rowCount: filterList[i].rowCount },
dataType: 'Links', dataType: 'LinkToAnotherRecord',
}); });
} }
} }
@ -965,8 +965,8 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Lookup', title: 'Lookup',
type: 'Lookup', type: 'Lookup',
childTable: 'Country', childTable: 'Address List',
childColumn: 'Country', childColumn: 'PostalCode',
}); });
// Enable NULL & EMPTY filters // Enable NULL & EMPTY filters
@ -975,12 +975,12 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
// add filter for CityList column // add filter for CityList column
const filterList = [ const filterList = [
{ op: 'is equal', value: 'Spain', rowCount: 5 }, { op: 'is equal', value: '4166', rowCount: 1 },
{ op: 'is not equal', value: 'Spain', rowCount: 595 }, { op: 'is not equal', value: '4166', rowCount: 599 },
{ op: 'is like', value: 'ca', rowCount: 28 }, { op: 'is like', value: '41', rowCount: 19 },
{ op: 'is not like', value: 'ca', rowCount: 572 }, { op: 'is not like', value: '41', rowCount: 581 },
{ op: 'is blank', value: null, rowCount: 0 }, { op: 'is blank', value: null, rowCount: 1 },
{ op: 'is not blank', value: null, rowCount: 600 }, { op: 'is not blank', value: null, rowCount: 599 },
]; ];
await toolbar.clickFilter(); await toolbar.clickFilter();
@ -1003,7 +1003,7 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
await dashboard.grid.column.create({ await dashboard.grid.column.create({
title: 'Rollup', title: 'Rollup',
type: 'Rollup', type: 'Rollup',
childTable: 'Addresses', childTable: 'Address List',
childColumn: 'PostalCode', childColumn: 'PostalCode',
rollupType: 'Sum', rollupType: 'Sum',
}); });
@ -1018,8 +1018,6 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
{ op: 'is not equal', value: '4166', rowCount: 599 }, { op: 'is not equal', value: '4166', rowCount: 599 },
{ op: 'is like', value: '41', rowCount: 19 }, { op: 'is like', value: '41', rowCount: 19 },
{ op: 'is not like', value: '41', rowCount: 581 }, { op: 'is not like', value: '41', rowCount: 581 },
{ op: 'is empty', value: '41', rowCount: 581 },
{ op: 'is not empty', value: '41', rowCount: 581 },
{ op: 'is blank', value: null, rowCount: 2 }, { op: 'is blank', value: null, rowCount: 2 },
{ op: 'is not blank', value: null, rowCount: 598 }, { op: 'is not blank', value: null, rowCount: 598 },
]; ];
@ -1028,11 +1026,11 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
await toolbar.filter.clickAddFilter(); await toolbar.filter.clickAddFilter();
for (let i = 0; i < filterList.length; i++) { for (let i = 0; i < filterList.length; i++) {
await verifyFilter({ await verifyFilter({
column: 'Rollup', column: 'Lookup',
opType: filterList[i].op, opType: filterList[i].op,
value: filterList[i].value, value: filterList[i].value,
result: { rowCount: filterList[i].rowCount }, result: { rowCount: filterList[i].rowCount },
dataType: 'Rollup', dataType: 'Lookup',
}); });
} }
} }

2
tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

@ -71,7 +71,7 @@ test.describe('Verify shortcuts', () => {
await grid.cell.click({ index: 0, columnHeader: 'Country' }); await grid.cell.click({ index: 0, columnHeader: 'Country' });
await page.waitForTimeout(1500); await page.waitForTimeout(1500);
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+ArrowRight' : 'Control+ArrowRight'); await page.keyboard.press((await grid.isMacOs()) ? 'Meta+ArrowRight' : 'Control+ArrowRight');
await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'Cities' }); await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'City List' });
// Cmd + Right arrow // Cmd + Right arrow
await page.keyboard.press((await grid.isMacOs()) ? 'Meta+ArrowLeft' : 'Control+ArrowLeft'); await page.keyboard.press((await grid.isMacOs()) ? 'Meta+ArrowLeft' : 'Control+ArrowLeft');

52
tests/playwright/tests/db/features/metaLTAR.spec.ts

@ -103,7 +103,7 @@ test.describe.serial('Test table', () => {
// Create links // Create links
// TableA <hm> TableB <hm> TableC // TableA <hm> TableB <hm> TableC
await api.dbTableColumn.create(tables[0].id, { await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `TableA:hm:TableB`, title: `TableA:hm:TableB`,
column_name: `TableA:hm:TableB`, column_name: `TableA:hm:TableB`,
parentId: tables[0].id, parentId: tables[0].id,
@ -111,7 +111,7 @@ test.describe.serial('Test table', () => {
type: 'hm', type: 'hm',
}); });
await api.dbTableColumn.create(tables[1].id, { await api.dbTableColumn.create(tables[1].id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `TableB:hm:TableC`, title: `TableB:hm:TableC`,
column_name: `TableB:hm:TableC`, column_name: `TableB:hm:TableC`,
parentId: tables[1].id, parentId: tables[1].id,
@ -121,7 +121,7 @@ test.describe.serial('Test table', () => {
// TableA <mm> TableD <mm> TableE // TableA <mm> TableD <mm> TableE
await api.dbTableColumn.create(tables[0].id, { await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `TableA:mm:TableD`, title: `TableA:mm:TableD`,
column_name: `TableA:mm:TableD`, column_name: `TableA:mm:TableD`,
parentId: tables[0].id, parentId: tables[0].id,
@ -129,7 +129,7 @@ test.describe.serial('Test table', () => {
type: 'mm', type: 'mm',
}); });
await api.dbTableColumn.create(tables[3].id, { await api.dbTableColumn.create(tables[3].id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `TableD:mm:TableE`, title: `TableD:mm:TableE`,
column_name: `TableD:mm:TableE`, column_name: `TableD:mm:TableE`,
parentId: tables[3].id, parentId: tables[3].id,
@ -139,7 +139,7 @@ test.describe.serial('Test table', () => {
// TableA <hm> TableA : self relation // TableA <hm> TableA : self relation
await api.dbTableColumn.create(tables[0].id, { await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `TableA:hm:TableA`, title: `TableA:hm:TableA`,
column_name: `TableA:hm:TableA`, column_name: `TableA:hm:TableA`,
parentId: tables[0].id, parentId: tables[0].id,
@ -149,7 +149,7 @@ test.describe.serial('Test table', () => {
// TableA <mm> TableA : self relation // TableA <mm> TableA : self relation
await api.dbTableColumn.create(tables[0].id, { await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: `TableA:mm:TableA`, title: `TableA:mm:TableA`,
column_name: `TableA:mm:TableA`, column_name: `TableA:mm:TableA`,
parentId: tables[0].id, parentId: tables[0].id,
@ -227,25 +227,27 @@ test.describe.serial('Test table', () => {
// has-many removal verification // has-many removal verification
await dashboard.treeView.openTable({ title: 'Table1' }); await dashboard.treeView.openTable({ title: 'Table1' });
await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0', count: 0, value: [], type: 'bt' }); await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0', count: 0, value: [] });
await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0', count: 0, value: [], type: 'bt' }); await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0', count: 0, value: [] });
await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0', count: 0, value: [], type: 'bt' }); await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0', count: 0, value: [] });
// many-many removal verification // many-many removal verification
await dashboard.treeView.openTable({ title: 'Table3' }); await dashboard.treeView.openTable({ title: 'Table3' });
const params = { await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0 List', count: 0, value: [] });
index: 0, await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0 List', count: 1, value: ['2'] });
columnHeader: 'Table0s', await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0 List', count: 2, value: ['2', '3'] });
count: 0, await dashboard.grid.cell.verifyVirtualCell({
value: [], index: 3,
type: 'hm', columnHeader: 'Table0 List',
options: { singular: 'Table0', plural: 'Table0s' }, count: 3,
}; value: ['2', '3', '4'],
await dashboard.grid.cell.verifyVirtualCell({ ...params }); });
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 1, index: 1, value: ['2'] }); await dashboard.grid.cell.verifyVirtualCell({
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 2, index: 2, value: ['2', '3'] }); index: 4,
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 3, index: 3, value: ['2', '3', '4'] }); columnHeader: 'Table0 List',
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 4, index: 4, value: ['2', '3', '4', '5'] }); count: 4,
value: ['2', '3', '4', '5'],
});
}); });
test('Delete record - bulk, over UI', async () => { test('Delete record - bulk, over UI', async () => {
@ -283,7 +285,7 @@ test.describe.serial('Test table', () => {
// verify // verify
await dashboard.treeView.openTable({ title: 'Table3' }); await dashboard.treeView.openTable({ title: 'Table3' });
await dashboard.grid.column.verify({ title: 'Table0s', isVisible: false }); await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false });
await dashboard.grid.column.verify({ title: 'TableD:mm:TableE', isVisible: true }); await dashboard.grid.column.verify({ title: 'TableD:mm:TableE', isVisible: true });
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
@ -303,7 +305,7 @@ test.describe.serial('Test table', () => {
await dashboard.grid.column.delete({ title: 'TableA:mm:TableA' }); await dashboard.grid.column.delete({ title: 'TableA:mm:TableA' });
// verify // verify
await dashboard.grid.column.verify({ title: 'Table0s', isVisible: false }); await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false });
await dashboard.grid.column.verify({ title: 'TableA:mm:TableA', isVisible: false }); await dashboard.grid.column.verify({ title: 'TableA:mm:TableA', isVisible: false });
}); });
@ -316,6 +318,6 @@ test.describe.serial('Test table', () => {
await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); await dashboard.grid.column.verify({ title: 'Table0', isVisible: false });
await dashboard.treeView.openTable({ title: 'Table3' }); await dashboard.treeView.openTable({ title: 'Table3' });
await dashboard.grid.column.verify({ title: 'Table0s', isVisible: false }); await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false });
}); });
}); });

4
tests/playwright/tests/db/features/mobileMode.spec.ts

@ -45,7 +45,7 @@ test.describe('Mobile Mode', () => {
// verify form-view fields order // verify form-view fields order
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['Country', 'LastUpdate', 'Cities'], fields: ['Country', 'LastUpdate', 'City List'],
}); });
// reorder & verify // reorder & verify
@ -54,7 +54,7 @@ test.describe('Mobile Mode', () => {
destinationField: 'Country', destinationField: 'Country',
}); });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'], fields: ['LastUpdate', 'Country', 'City List'],
}); });
await dashboard.treeView.openTable({ mobileMode: true, title: 'test-table-for-mobile-mode' }); await dashboard.treeView.openTable({ mobileMode: true, title: 'test-table-for-mobile-mode' });

40
tests/playwright/tests/db/features/undo-redo.spec.ts

@ -527,7 +527,7 @@ test.describe('Undo Redo - LTAR', () => {
await api.dbTableColumn.create(countryTable.id, { await api.dbTableColumn.create(countryTable.id, {
column_name: 'CityList', column_name: 'CityList',
title: 'CityList', title: 'CityList',
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
parentId: countryTable.id, parentId: countryTable.id,
childId: cityTable.id, childId: cityTable.id,
type: 'hm', type: 'hm',
@ -546,38 +546,18 @@ test.describe('Undo Redo - LTAR', () => {
// inserted values // inserted values
const expectedValues = [...values]; const expectedValues = [...values];
try { const currentRecords: Record<string, any> = await api.dbTableRow.list('noco', context.project.id, countryTable.id, {
const currentRecords: Record<string, any> = await api.dbTableRow.list(
'noco',
context.project.id,
countryTable.id,
{
fields: ['CityList'], fields: ['CityList'],
limit: 100, limit: 100,
} });
);
expect(currentRecords.list.length).toBe(4);
expect(+currentRecords.list[0].CityList).toBe(expectedValues.length);
} catch (e) {
console.log(e);
}
if (expectedValues.length > 0) {
// read nested records associated with first record
const nestedRecords: Record<string, any> = await api.dbTableRow.nestedList(
'noco',
context.project.id,
countryTable.id,
1,
'hm',
'CityList'
);
const cities = nestedRecords.list.map((record: any) => record.City);
for (let i = 0; i < expectedValues.length; i++) { // verify if expectedValues array includes all the values in currentRecords
expect(cities.includes(expectedValues[i])).toBeTruthy(); // currentRecords [ { Id: 1, City: 'Mumbai' }, { Id: 3, City: 'Delhi' } ]
} // expectedValues [ 'Mumbai', 'Delhi' ]
} currentRecords.list[0].CityList.forEach((record: any) => {
expect(expectedValues).toContain(record.City);
});
expect(currentRecords.list[0].CityList.length).toBe(expectedValues.length);
} }
async function undo({ page, values }: { page: Page; values: string[] }) { async function undo({ page, values }: { page: Page; values: string[] }) {

29
tests/playwright/tests/db/features/webhook.spec.ts

@ -611,8 +611,9 @@ test.describe.serial('Webhook', () => {
// create LTAR Country has-many City // create LTAR Country has-many City
countryTable = await api.dbTableColumn.create(countryTable.id, { countryTable = await api.dbTableColumn.create(countryTable.id, {
column_name: 'CityList',
title: 'CityList', title: 'CityList',
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
parentId: countryTable.id, parentId: countryTable.id,
childId: cityTable.id, childId: cityTable.id,
type: 'hm', type: 'hm',
@ -683,7 +684,16 @@ test.describe.serial('Webhook', () => {
CountryCode: '1', CountryCode: '1',
CityCodeRollup: '2', CityCodeRollup: '2',
CityCodeFormula: 100, CityCodeFormula: 100,
CityList: '2', CityList: [
{
Id: 1,
City: 'Mumbai',
},
{
Id: 2,
City: 'Pune',
},
],
CityCodeLookup: ['23', '33'], CityCodeLookup: ['23', '33'],
}, },
], ],
@ -694,7 +704,16 @@ test.describe.serial('Webhook', () => {
CountryCode: '1', CountryCode: '1',
CityCodeRollup: '2', CityCodeRollup: '2',
CityCodeFormula: 100, CityCodeFormula: 100,
CityList: '2', CityList: [
{
Id: 1,
City: 'Mumbai',
},
{
Id: 2,
City: 'Pune',
},
],
CityCodeLookup: ['23', '33'], CityCodeLookup: ['23', '33'],
}, },
], ],
@ -714,10 +733,6 @@ test.describe.serial('Webhook', () => {
expectedData.data.previous_rows[0].CityCodeLookup = [23, 33]; expectedData.data.previous_rows[0].CityCodeLookup = [23, 33];
// @ts-ignore // @ts-ignore
expectedData.data.rows[0].CityCodeLookup = [23, 33]; expectedData.data.rows[0].CityCodeLookup = [23, 33];
// @ts-ignore
expectedData.data.previous_rows[0].CityList = 2;
// @ts-ignore
expectedData.data.rows[0].CityList = 2;
if (isMysql(context)) { if (isMysql(context)) {
// @ts-ignore // @ts-ignore

8
tests/playwright/tests/db/general/cellSelection.spec.ts

@ -50,7 +50,7 @@ test.describe('Verify cell selection', () => {
await dashboard.treeView.openTable({ title: 'Country' }); await dashboard.treeView.openTable({ title: 'Country' });
await grid.selectRange({ await grid.selectRange({
start: { index: 0, columnHeader: 'Country' }, start: { index: 0, columnHeader: 'Country' },
end: { index: 2, columnHeader: 'Cities' }, end: { index: 2, columnHeader: 'City List' },
}); });
expect(await grid.selectedCount()).toBe(9); expect(await grid.selectedCount()).toBe(9);
await grid.cell.get({ index: 0, columnHeader: 'Country' }).click(); await grid.cell.get({ index: 0, columnHeader: 'Country' }).click();
@ -62,7 +62,7 @@ test.describe('Verify cell selection', () => {
await dashboard.treeView.openTable({ title: 'Country' }); await dashboard.treeView.openTable({ title: 'Country' });
await grid.selectRange({ await grid.selectRange({
start: { index: 0, columnHeader: 'Country' }, start: { index: 0, columnHeader: 'Country' },
end: { index: 2, columnHeader: 'Cities' }, end: { index: 2, columnHeader: 'City List' },
}); });
expect(await grid.selectedCount()).toBe(9); expect(await grid.selectedCount()).toBe(9);
await grid.cell.get({ index: 5, columnHeader: 'Country' }).click(); await grid.cell.get({ index: 5, columnHeader: 'Country' }).click();
@ -74,7 +74,7 @@ test.describe('Verify cell selection', () => {
await dashboard.treeView.openTable({ title: 'Country' }); await dashboard.treeView.openTable({ title: 'Country' });
await dashboard.grid.toolbar.fields.toggleShowSystemFields(); await dashboard.grid.toolbar.fields.toggleShowSystemFields();
await grid.selectRange({ await grid.selectRange({
start: { index: 2, columnHeader: 'Cities' }, start: { index: 2, columnHeader: 'City List' },
end: { index: 0, columnHeader: 'Country' }, end: { index: 0, columnHeader: 'Country' },
}); });
expect(await grid.selectedCount()).toBe(12); expect(await grid.selectedCount()).toBe(12);
@ -88,7 +88,7 @@ test.describe('Verify cell selection', () => {
await dashboard.treeView.openTable({ title: 'Country' }); await dashboard.treeView.openTable({ title: 'Country' });
await grid.selectRange({ await grid.selectRange({
start: { index: 0, columnHeader: 'Country' }, start: { index: 0, columnHeader: 'Country' },
end: { index: 2, columnHeader: 'Cities' }, end: { index: 2, columnHeader: 'City List' },
}); });
await page.keyboard.press('ArrowRight'); await page.keyboard.press('ArrowRight');
expect(await grid.selectedCount()).toBe(1); expect(await grid.selectedCount()).toBe(1);

6
tests/playwright/tests/db/general/megaTable.spec.ts

@ -202,9 +202,9 @@ test.describe.serial('Test table', () => {
await api.dbTableRow.bulkCreate('noco', context.project.id, table_2.id, rows); await api.dbTableRow.bulkCreate('noco', context.project.id, table_2.id, rows);
await api.dbTableColumn.create(table_2.id, { await api.dbTableColumn.create(table_2.id, {
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
title: 'Links', title: 'LinkToAnotherRecord',
column_name: 'Links', column_name: 'LinkToAnotherRecord',
parentId: table_1.id, parentId: table_1.id,
childId: table_2.id, childId: table_2.id,
type: 'hm', type: 'hm',

23
tests/playwright/tests/db/views/viewForm.spec.ts

@ -34,7 +34,7 @@ test.describe('Form view', () => {
// verify form-view fields order // verify form-view fields order
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['Country', 'LastUpdate', 'Cities'], fields: ['Country', 'LastUpdate', 'City List'],
}); });
// reorder & verify // reorder & verify
@ -43,31 +43,31 @@ test.describe('Form view', () => {
destinationField: 'Country', destinationField: 'Country',
}); });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'], fields: ['LastUpdate', 'Country', 'City List'],
}); });
// remove & verify (drag-drop) // remove & verify (drag-drop)
await form.removeField({ field: 'Cities', mode: 'dragDrop' }); await form.removeField({ field: 'City List', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country'], fields: ['LastUpdate', 'Country'],
}); });
// add & verify (drag-drop) // add & verify (drag-drop)
await form.addField({ field: 'Cities', mode: 'dragDrop' }); await form.addField({ field: 'City List', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'], fields: ['LastUpdate', 'Country', 'City List'],
}); });
// remove & verify (hide field button) // remove & verify (hide field button)
await form.removeField({ field: 'Cities', mode: 'hideField' }); await form.removeField({ field: 'City List', mode: 'hideField' });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country'], fields: ['LastUpdate', 'Country'],
}); });
// add & verify (hide field button) // add & verify (hide field button)
await form.addField({ field: 'Cities', mode: 'clickField' }); await form.addField({ field: 'City List', mode: 'clickField' });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'], fields: ['LastUpdate', 'Country', 'City List'],
}); });
// remove-all & verify // remove-all & verify
@ -81,7 +81,7 @@ test.describe('Form view', () => {
await form.addAllFields(); await form.addAllFields();
await dashboard.rootPage.waitForTimeout(2000); await dashboard.rootPage.waitForTimeout(2000);
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'], fields: ['LastUpdate', 'Country', 'City List'],
}); });
}); });
@ -310,7 +310,7 @@ test.describe('Form view with LTAR', () => {
await api.dbTableColumn.create(countryTable.id, { await api.dbTableColumn.create(countryTable.id, {
column_name: 'CityList', column_name: 'CityList',
title: 'CityList', title: 'CityList',
uidt: UITypes.Links, uidt: UITypes.LinkToAnotherRecord,
parentId: countryTable.id, parentId: countryTable.id,
childId: cityTable.id, childId: cityTable.id,
type: 'hm', type: 'hm',
@ -360,8 +360,7 @@ test.describe('Form view with LTAR', () => {
index: 3, index: 3,
columnHeader: 'CityList', columnHeader: 'CityList',
count: 1, count: 1,
type: 'hm', value: ['Atlanta'],
options: { singular: 'City', plural: 'Cities' },
}); });
}); });
}); });

2
tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

@ -65,7 +65,7 @@ test.describe('Share form', () => {
await surveyForm.validate({ await surveyForm.validate({
heading: 'Country Title', heading: 'Country Title',
subHeading: 'Country Form Subtitle', subHeading: 'Country Form Subtitle',
fieldLabel: 'Cities', fieldLabel: 'City List',
footer: '3 / 3', footer: '3 / 3',
}); });
await surveyForm.submitButton.click(); await surveyForm.submitButton.click();

30
tests/playwright/tests/db/views/viewGridShare.spec.ts

@ -32,7 +32,7 @@ test.describe('Shared view', () => {
// hide column // hide column
await dashboard.grid.toolbar.fields.toggle({ title: 'Address2' }); await dashboard.grid.toolbar.fields.toggle({ title: 'Address2' });
await dashboard.grid.toolbar.fields.toggle({ title: 'Stores' }); await dashboard.grid.toolbar.fields.toggle({ title: 'Store List' });
// sort // sort
await dashboard.grid.toolbar.sort.add({ await dashboard.grid.toolbar.sort.add({
@ -73,9 +73,9 @@ test.describe('Shared view', () => {
{ title: 'PostalCode', isVisible: true }, { title: 'PostalCode', isVisible: true },
{ title: 'Phone', isVisible: true }, { title: 'Phone', isVisible: true },
{ title: 'LastUpdate', isVisible: true }, { title: 'LastUpdate', isVisible: true },
{ title: 'Customers', isVisible: true }, { title: 'Customer List', isVisible: true },
{ title: 'Staffs', isVisible: true }, { title: 'Staff List', isVisible: true },
{ title: 'Stores', isVisible: false }, { title: 'Store List', isVisible: false },
{ title: 'City', isVisible: true }, { title: 'City', isVisible: true },
]; ];
for (const column of expectedColumns) { for (const column of expectedColumns) {
@ -94,11 +94,7 @@ test.describe('Shared view', () => {
// verify virtual records // verify virtual records
for (const record of expectedVirtualRecordsByDb) { for (const record of expectedVirtualRecordsByDb) {
await sharedPage.grid.cell.verifyVirtualCell({ await sharedPage.grid.cell.verifyVirtualCell({ ...record, verifyChildList: true });
...record,
options: { singular: 'Customer', plural: 'Customers' },
verifyChildList: true,
});
} }
/** /**
@ -281,15 +277,15 @@ const sqliteExpectedRecords2 = [
]; ];
const expectedVirtualRecords = [ const expectedVirtualRecords = [
{ index: 0, columnHeader: 'Customers', count: 1, type: 'hm' }, { index: 0, columnHeader: 'Customer List', count: 1, value: ['2'] },
{ index: 1, columnHeader: 'Customers', count: 1, type: 'hm' }, { index: 1, columnHeader: 'Customer List', count: 1, value: ['2'] },
{ index: 0, columnHeader: 'City', count: 1, type: 'bt', value: ['Kanchrapara'] }, { index: 0, columnHeader: 'City', count: 1, value: ['Kanchrapara'] },
{ index: 1, columnHeader: 'City', count: 1, type: 'bt', value: ['Tafuna'] }, { index: 1, columnHeader: 'City', count: 1, value: ['Tafuna'] },
]; ];
const sqliteExpectedVirtualRecords = [ const sqliteExpectedVirtualRecords = [
{ index: 0, columnHeader: 'Customers', count: 1, type: 'hm' }, { index: 0, columnHeader: 'Customer List', count: 1, value: ['2'] },
{ index: 1, columnHeader: 'Customers', count: 1, type: 'hm' }, { index: 1, columnHeader: 'Customer List', count: 1, value: ['1'] },
{ index: 0, columnHeader: 'City', count: 1, type: 'bt', value: ['Davao'] }, { index: 0, columnHeader: 'City', count: 1, value: ['Davao'] },
{ index: 1, columnHeader: 'City', count: 1, type: 'bt', value: ['Nagareyama'] }, { index: 1, columnHeader: 'City', count: 1, value: ['Nagareyama'] },
]; ];

Loading…
Cancel
Save