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. 83
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  13. 36
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  14. 38
      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. 52
      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. 91
      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. 44
      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 {
@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">
import type { Card as AntCard } from 'ant-design-vue'
import {

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

@ -2,7 +2,7 @@
import type { NodeProps } from '@vue-flow/core'
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { NodeData } from './utils'
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'"
>
<div
v-if="isLinksOrLTAR(col)"
v-if="col.uidt === UITypes.LinkToAnotherRecord"
class="flex w-full"
: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">
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isLinksOrLTAR } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { ERDConfig } from './utils'
import { reactive, ref, storeToRefs, useMetas, useProject, watch } from '#imports'
@ -42,7 +42,9 @@ const populateTables = async () => {
(t) =>
t.id === props.table?.id ||
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 {

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

@ -1,5 +1,5 @@
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre'
import type { Edge, EdgeMarker, Elements, Node } from '@vue-flow/core'
import type { MaybeRef } from '@vueuse/core'
@ -73,7 +73,8 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ER
const relations = computed(() =>
erdTables.value.reduce((acc, table) => {
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) => {
const colOptions = column.colOptions as LinkToAnotherRecordType
@ -174,7 +175,7 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ER
const columns =
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)

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

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

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

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

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

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

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

@ -14,7 +14,6 @@ import {
isCount,
isFormula,
isHm,
isLink,
isLookup,
isMm,
isQrCode,
@ -96,8 +95,7 @@ onUnmounted(() => {
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
>
<template v-if="intersected">
<LazyVirtualCellLinks v-if="isLink(column)" />
<LazyVirtualCellHasMany v-else-if="isHm(column)" />
<LazyVirtualCellHasMany v-if="isHm(column)" />
<LazyVirtualCellManyToMany v-else-if="isMm(column)" />
<LazyVirtualCellBelongsTo v-else-if="isBt(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 onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup, UITypes.Links]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const geoDataToggleCondition = (t: { name: UITypes }) => {
return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData)
}
const showDeprecated = $ref(false)
const uiTypesOptions = computed<typeof uiTypes>(() => {
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)
? [
{
@ -189,13 +187,11 @@ useEventListener('keydown', (e: KeyboardEvent) => {
:disabled="isKanban"
dropdown-class-name="nc-dropdown-column-type"
@change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
>
<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">
<component :is="opt.icon" class="text-grey" />
{{ opt.name }}
<span v-if="opt.deprecated" class="!text-xs !text-gray-300">(Deprecated)</span>
</div>
</a-select-option>
</a-select>
@ -213,10 +209,9 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && (formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links)"
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState"
/>
<LazySmartsheetColumnLinkOptions v-if="isEdit && formState.uidt === UITypes.Links" v-model:value="formState" />
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<LazySmartsheetColumnSelectOptions
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>

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

@ -1,5 +1,5 @@
<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 MdiPlusIcon from '~icons/mdi/plus-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 isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</script>
<template>
@ -88,7 +86,7 @@ const isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</a-select>
</a-form-item>
</div>
<template v-if="!isXcdbBase || isLinks">
<template v-if="!isXcdbBase">
<div
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
@ -99,46 +97,43 @@ const isLinks = $computed(() => vModel.value.uidt === UITypes.Links)
</div>
<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">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
v-model:value="vModel.onUpdate"
:disabled="vModel.virtual"
name="onUpdate"
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select
v-model:value="vModel.onDelete"
:disabled="vModel.virtual"
name="onDelete"
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="vModel.virtual" :disabled="appInfo.isCloud" name="virtual" @change="onDataTypeChange"
>Virtual Relation
</a-checkbox>
</a-form-item>
</div>
</template>
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
v-model:value="vModel.onUpdate"
:disabled="vModel.virtual"
name="onUpdate"
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select
v-model:value="vModel.onDelete"
:disabled="vModel.virtual"
name="onDelete"
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="vModel.virtual" :disabled="appInfo.isCloud" name="virtual" @change="onDataTypeChange"
>Virtual Relation
</a-checkbox>
</a-form-item>
</div>
</div>
</template>
</div>

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

@ -1,7 +1,8 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
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'
const props = defineProps<{
@ -34,7 +35,7 @@ const refTables = $computed(() => {
}
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) => ({
col: column.colOptions,
column,
@ -49,9 +50,7 @@ const columns = $computed<ColumnType[]>(() => {
if (!selectedTable?.id) {
return []
}
return metas[selectedTable.id].columns.filter(
(c: ColumnType) => !isSystemColumn(c) && c.id !== vModel.value.id && c.uidt !== UITypes.Links,
)
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isSystemColumn(c) && c.id !== vModel.value.id)
})
onMounted(() => {
@ -65,28 +64,22 @@ const onRelationColChange = () => {
vModel.value.fk_lookup_column_id = columns?.[0]?.id
onDataTypeChange()
}
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
})
</script>
<template>
<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">
<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
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onRelationColChange"
>
<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="font-semibold text-xs flex-shrink flex-grow-0 truncate">{{ table.column.title }}</div>
<div class="flex-grow"></div>
<div class="text-[0.65rem] text-gray-600 nc-relation-details">
<span class="uppercase">{{ table.col.type }}</span> {{ table.title || table.table_name }}
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600">
{{ getRelationName(table.col.type) }} {{ table.title || table.table_name }}
</div>
</div>
</a-select-option>
@ -101,10 +94,7 @@ const cellIcon = (column: ColumnType) =>
@change="onDataTypeChange"
>
<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 }}
</div>
{{ column.title }}
</a-select-option>
</a-select>
</a-form-item>
@ -112,8 +102,4 @@ const cellIcon = (column: ColumnType) =>
</div>
</template>
<style scoped>
:deep(.ant-select-selector .ant-select-selection-item .nc-relation-details) {
@apply hidden;
}
</style>
<style scoped></style>

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

@ -1,7 +1,8 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UITypes } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } 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'
const props = defineProps<{
@ -49,7 +50,10 @@ const refTables = $computed(() => {
const _refTables = meta.columns
.filter(
(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) => ({
col: c.colOptions,
@ -83,28 +87,22 @@ const onRelationColChange = () => {
vModel.value.fk_rollup_column_id = columns?.[0]?.id
onDataTypeChange()
}
const cellIcon = (column: ColumnType) =>
h(isVirtualCol(column) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: column,
})
</script>
<template>
<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">
<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
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onRelationColChange"
>
<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="font-semibold text-xs flex-shrink flex-grow-0 truncate">{{ table.column.title }}</div>
<div class="flex-grow"></div>
<div class="text-[0.65rem] text-gray-600 nc-relation-details">
<span class="uppercase">{{ table.col.type }}</span> {{ table.title || table.table_name }}
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600">
({{ getRelationName(table.col.type) }} {{ table.title || table.table_name }})
</div>
</div>
</a-select-option>
@ -119,11 +117,7 @@ const cellIcon = (column: ColumnType) =>
@change="onDataTypeChange"
>
<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 }}
</div>
{{ column.title }}
</a-select-option>
</a-select>
</a-form-item>
@ -142,9 +136,3 @@ const cellIcon = (column: ColumnType) =>
</a-form-item>
</div>
</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">
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 {
CellClickHookInj,
@ -324,7 +324,7 @@ export default {
<div
v-for="(col, i) of fields"
v-else
v-show="!isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
v-show="!isVirtualCol(col) || !isNew || col.uidt === UITypes.LinkToAnotherRecord"
:key="col.title"
class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`"

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnReqType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
ColumnInj,
@ -66,7 +66,7 @@ const deleteColumn = () =>
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 */
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)
// reload tables if deleted column is mm and include m2m is true
@ -181,7 +181,6 @@ const duplicateColumn = async () => {
// construct column create payload
switch (column?.value.uidt) {
case UITypes.LinkToAnotherRecord:
case UITypes.Links:
case UITypes.Lookup:
case UITypes.Rollup:
case UITypes.Formula:
@ -316,7 +315,7 @@ const hideField = async () => {
{{ $t('general.edit') }}
</div>
</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-menu-item @click="sortByColumn('asc')">
<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>
{{ tooltipMsg }}
</template>
<span class="name" :class="{ 'truncate': !isForm, 'whitespace-pre-line': isForm }" :title="column.title">
{{ column.title }}
</span>
<span class="name" style="white-space: pre-line" :title="column.title"> {{ column.title }}</span>
</a-tooltip>
<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) => {
switch (column.uidt) {
case UITypes.LinkToAnotherRecord:
case UITypes.Links:
switch ((column.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return { icon: iconMap.mm, color: 'text-accent' }

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

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
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'
const { modelValue, isSort } = defineProps<{
@ -25,9 +25,6 @@ const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const options = computed<SelectProps['options']>(() =>
meta.value?.columns
?.filter((c: ColumnType) => {
if (c.uidt === UITypes.Links) {
return true
}
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
return (
/** if the field is used in filter, then show it anyway */
@ -39,7 +36,9 @@ const options = computed<SelectProps['options']>(() =>
return false
} else if (isSort) {
/** 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 */
} else {
const isVirtualSystemField = c.colOptions && c.system

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

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

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
FieldsInj,
@ -71,13 +71,8 @@ const grid = ref()
const onDrop = async (event: DragEvent) => {
event.preventDefault()
try {
// extract the data from the event's data transfer object
const textData = event.dataTransfer?.getData('text/json')
if (!textData) return
// parse the data
const data = JSON.parse(textData)
// Access the dropped data
const data = JSON.parse(event.dataTransfer?.getData('text/json')!)
// Do something with the received data
// 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
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
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) {
const lookupCol = childMeta.columns?.find((c) => c.pv) ?? childMeta.columns?.[0]
grid.value?.openColumnCreate({
uidt: UITypes.Lookup,
title: `${relationCol.title}Lookup`,
title: `${data.title}Lookup`,
fk_relation_column_id: relationCol.id,
fk_lookup_column_id: lookupCol?.id,
})
} else {
grid.value?.openColumnCreate({
uidt: UITypes.Links,
title: `${data.title}`,
uidt: UITypes.LinkToAnotherRecord,
title: `${data.title}List`,
parentId: parentMeta.id,
childId: childMeta.id,
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,
isAttachment,
ref,
renderValue,
useExpandedFormDetached,
useLTARStoreOrThrow,
} from '#imports'
@ -18,11 +19,9 @@ interface Props {
item?: any
column: any
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'])
@ -41,7 +40,7 @@ const isLocked = inject(IsLockedInj, ref(false))
const { open } = useExpandedFormDetached()
function openExpandedForm() {
if (!readOnly.value && !isLocked.value && !readonlyProp) {
if (!readOnly && !isLocked.value) {
open({
isOpen: true,
row: { row: item, rowMeta: {}, oldRow: { ...item } },
@ -85,8 +84,9 @@ export default {
class="min-w-max"
:class="{
'px-1 rounded-full flex-1': !isAttachment(column),
'border-gray-200 rounded border-1':
border && ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(column.uidt),
'border-gray-200 rounded border-1': ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(
column.uidt,
),
}"
>
<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,
inject,
ref,
renderValue,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
@ -39,6 +40,7 @@ const {
childrenListPagination,
relatedTableDisplayValueProp,
unlink,
getRelatedTableRowId,
relatedTableMeta,
} = useLTARStoreOrThrow()
@ -107,7 +109,7 @@ const onClick = (row: Row) => {
:body-style="{ padding: 0 }"
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-1" />
<component
@ -141,29 +143,26 @@ const onClick = (row: Row) => {
<a-card
v-for="(row, i) of childrenList?.list ?? state?.[colTitle] ?? []"
: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)"
>
<div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0">
<VirtualCellComponentsItemChip
:border="false"
:value="row[relatedTableDisplayValueProp]"
:column="props.column"
/>
<VirtualCellComponentsItemChip :value="row[relatedTableDisplayValueProp]" :column="props.column" />
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>
<div v-if="!readonly" class="flex gap-2">
<component
: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"
@click.stop="unlinkRow(row)"
/>
<component
:is="iconMap.delete"
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"
@click.stop="deleteRelatedRow(row, unlinkIfNewRow)"
/>
@ -209,8 +208,4 @@ const onClick = (row: Row) => {
:deep(.ant-pagination-item a) {
line-height: 21px !important;
}
:deep(.nc-nested-list-item .ant-card-body) {
@apply !px-1 !py-0;
}
</style>

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

@ -12,6 +12,7 @@ import {
inject,
isDrawerExist,
ref,
renderValue,
useLTARStoreOrThrow,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
@ -34,6 +35,7 @@ const {
childrenExcludedListPagination,
relatedTableDisplayValueProp,
link,
getRelatedTableRowId,
relatedTableMeta,
meta,
row,
@ -50,7 +52,6 @@ const selectedRowIndex = ref(0)
const isAltKeyDown = ref(false)
const linkRow = async (row: Record<string, any>) => {
childrenExcludedList.value?.list?.splice(selectedRowIndex.value, 1)
if (isNew.value) {
addLTARRef(row, column?.value as ColumnType)
saveRow!()
@ -58,7 +59,7 @@ const linkRow = async (row: Record<string, any>) => {
await link(row)
}
if (isAltKeyDown.value) {
if (!isNew.value) loadChildrenExcludedList()
loadChildrenExcludedList()
} else {
vModel.value = false
}
@ -197,7 +198,7 @@ watch(vModel, (nextVal) => {
:body-style="{ padding: 0 }"
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">
<a-input
ref="filterQueryRef"
@ -224,7 +225,7 @@ watch(vModel, (nextVal) => {
v-for="(refRow, i) in childrenExcludedList?.list ?? []"
:key="i"
: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 }"
@click="linkRow(refRow)"
>
@ -232,9 +233,10 @@ watch(vModel, (nextVal) => {
:value="refRow[relatedTableDisplayValueProp]"
:column="props.column"
: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>
</div>
@ -279,8 +281,4 @@ watch(vModel, (nextVal) => {
:deep(.nc-selected-row) {
@apply !ring;
}
:deep(.nc-nested-list-item .ant-card-body) {
@apply !px-1 !py-0;
}
</style>

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

@ -1,6 +1,6 @@
import rfdc from 'rfdc'
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 { RuleObject } from 'ant-design-vue/es/form'
import {
@ -31,8 +31,8 @@ interface ValidationsObj {
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const projectStore = useProject()
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc, getBaseType } = projectStore
const { sqlUis } = storeToRefs(projectStore)
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc } = projectStore
const { project, sqlUis } = storeToRefs(projectStore)
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 baseType = computed(() => getBaseType(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const idType = null
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: [
{
@ -254,7 +252,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
await $api.dbTableColumn.create(meta.value?.id as string, { ...formState.value, ...columnPosition })
/** 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(() => {})
}

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

@ -26,6 +26,7 @@ import {
useI18n,
useMetas,
useProject,
useUIPermission,
} from '#imports'
const MAIN_MOUSE_PRESSED = 0
@ -79,6 +80,9 @@ export function useMultiSelect(
() => !(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) {
if (activeCell.row === row && activeCell.col === col) {
return

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

@ -11,7 +11,7 @@ import type {
TableType,
ViewType,
} 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 {
SharedViewPasswordInj,
@ -74,7 +74,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const fieldRequired = (fieldName = 'Value') => helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required)
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 () => {
@ -151,7 +151,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
) {
obj.localState[column.title!] = { required: fieldRequired(column.label || column.title) }
} else if (
isLinksOrLTAR(column) &&
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions &&
(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 { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
@ -80,11 +80,11 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
try {
await $api.dbTableRow.nestedAdd(
NOCO,
project.value.id as string,
metaValue?.id as string,
project.value.title as string,
metaValue?.title as string,
rowId,
type as 'mm' | 'hm',
column.id as string,
column.title as string,
relatedRowId,
)
} catch (e: any) {
@ -96,7 +96,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
const syncLTARRefs = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}) => {
const id = extractPkFromRow(row, metaValue?.columns as ColumnType[])
for (const column of metaValue?.columns ?? []) {
if (!isLinksOrLTAR(column)) continue
if (column.uidt !== UITypes.LinkToAnotherRecord) continue
const colOptions = column.colOptions as LinkToAnotherRecordType
@ -132,7 +132,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
// clear LTAR cell
const clearLTARCell = async (column: ColumnType) => {
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]
@ -143,11 +143,11 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
if (!currentRow.value.row[column.title!]) return
await $api.dbTableRow.nestedRemove(
NOCO,
project.value.id as string,
meta.value?.id as string,
project.value.title as string,
meta.value?.title as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
'bt' as any,
column.id as string,
column.title as string,
extractPkFromRow(currentRow.value.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
)
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>[]) || []) {
await $api.dbTableRow.nestedRemove(
NOCO,
project.value.id as string,
meta.value?.id as string,
project.value.title as string,
meta.value?.title as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
(<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm',
column.id as string,
column.title as string,
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 { UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import {
Modal,
SYSTEM_COLUMNS,
@ -86,7 +86,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?
async onOk() {
try {
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
// skip for xcdb base

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

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

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

@ -227,6 +227,5 @@ export const useProject = defineStore('projectStore', () => {
isXcdbBase,
hasEmptyOrNullFilters,
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'
const uiTypes = [
{
name: UITypes.Links,
icon: iconMap.link,
virtual: 1,
},
{
name: UITypes.LinkToAnotherRecord,
icon: iconMap.link,
virtual: 1,
deprecated: 1,
},
{
name: UITypes.Lookup,

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

@ -261,13 +261,13 @@ export const comparisonOpList = (
text: 'is blank',
value: 'blank',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Links, UITypes.Rollup],
excludedTypes: [UITypes.Checkbox],
},
{
text: 'is not blank',
value: 'notblank',
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 { 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) =>
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 isBarcode = (column: ColumnType) => column.uidt === UITypes.Barcode
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
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

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

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

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

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

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

@ -38,7 +38,6 @@ enum UITypes {
Barcode = 'Barcode',
QrCode = 'QrCode',
Button = 'Button',
Links = 'Links',
}
export const numericUITypes = [
@ -50,7 +49,6 @@ export const numericUITypes = [
UITypes.Rating,
UITypes.Rollup,
UITypes.Year,
UITypes.Links,
];
export function isNumericCol(
@ -81,17 +79,8 @@ export function isVirtualCol(
UITypes.Barcode,
UITypes.Rollup,
UITypes.Lookup,
UITypes.Links,
// UITypes.Count,
].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;

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

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

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

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

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

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

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

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

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

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

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

@ -1,11 +1,8 @@
import { customAlphabet } from 'nanoid';
import { UITypes } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import { GridViewColumn } from '../models';
import Column from '../models/Column';
import { getUniqueColumnAliasName } from '../helpers/getUniqueName';
import validateParams from '../helpers/validateParams';
import type { RollupColumn } from '../models';
import type {
BoolType,
ColumnReqType,
@ -33,14 +30,12 @@ export async function createHmAndBtColumn(
fkColName?: string,
virtual: BoolType = false,
isSystemCol = false,
columnMeta = null,
isLinks = false,
) {
// save bt column
{
const title = getUniqueColumnAliasName(
await child.getColumns(),
(type === 'bt' && alias) || `${parent.title}`,
type === 'bt' ? alias : `${parent.title}`,
);
await Column.insert<LinkToAnotherRecordColumn>({
title,
@ -55,8 +50,7 @@ export async function createHmAndBtColumn(
fk_parent_column_id: parent.primaryKey.id,
fk_related_model_id: parent.id,
virtual,
// if self referencing treat it as system field to hide from ui
system: isSystemCol || parent.id === child.id,
system: isSystemCol,
fk_col_name: fkColName,
fk_index_name: fkColName,
});
@ -65,17 +59,12 @@ export async function createHmAndBtColumn(
{
const title = getUniqueColumnAliasName(
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({
title,
fk_model_id: parent.id,
uidt: isLinks ? UITypes.Links : UITypes.LinkToAnotherRecord,
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
fk_child_column_id: childColumn.id,
fk_parent_column_id: parent.primaryKey.id,
@ -84,7 +73,6 @@ export async function createHmAndBtColumn(
system: isSystemCol,
fk_col_name: fkColName,
fk_index_name: fkColName,
meta,
});
}
}
@ -218,47 +206,3 @@ export const generateFkName = (parent: TableType, child: TableType) => {
.slice(0, 10)}_${randomID(15)}`;
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;
// todo: include field relative to the relation => pk / fk
} else if (col.uidt === UITypes.Links) {
value = 1;
} else {
value = (
Array.isArray(nestedFields) ? nestedFields : nestedFields.split(',')
).reduce((o, f) => ({ ...o, [f]: 1 }), {});
value = (Array.isArray(fields) ? fields : fields.split(',')).reduce(
(o, f) => ({ ...o, [f]: 1 }),
{},
);
}
} else if (col.uidt === UITypes.LinkToAnotherRecord) {
const model = await col

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

@ -1,8 +1,5 @@
import { ModelTypes, UITypes, ViewTypes } 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 Model from '../models/Model';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
@ -12,7 +9,6 @@ import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import { getUniqueColumnAliasName } from './getUniqueName';
import type { RollupColumn } from '../models';
import type LinkToAnotherRecordColumn from '../models/LinkToAnotherRecordColumn';
import type Base from '../models/Base';
import type Project from '../models/Project';
@ -114,7 +110,7 @@ export async function extractAndGenerateManyToManyRelations(
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelA.columns,
pluralize(modelB.title),
`${modelB.title} List`,
),
fk_model_id: modelA.id,
fk_related_model_id: modelB.id,
@ -125,18 +121,14 @@ export async function extractAndGenerateManyToManyRelations(
fk_mm_parent_column_id:
belongsToCols[1].colOptions.fk_child_column_id,
type: RelationTypes.MANY_TO_MANY,
uidt: UITypes.Links,
meta: {
plural: pluralize(modelB.title),
singular: singularize(modelB.title),
},
uidt: UITypes.LinkToAnotherRecord,
});
}
if (!isRelationAvailInB) {
await Column.insert<LinkToAnotherRecordColumn>({
title: getUniqueColumnAliasName(
modelB.columns,
pluralize(modelA.title),
`${modelA.title} List`,
),
fk_model_id: modelB.id,
fk_related_model_id: modelA.id,
@ -147,11 +139,7 @@ export async function extractAndGenerateManyToManyRelations(
fk_mm_parent_column_id:
belongsToCols[0].colOptions.fk_child_column_id,
type: RelationTypes.MANY_TO_MANY,
uidt: UITypes.Links,
meta: {
plural: pluralize(modelA.title),
singular: singularize(modelA.title),
},
uidt: UITypes.LinkToAnotherRecord,
});
}
@ -163,7 +151,7 @@ export async function extractAndGenerateManyToManyRelations(
const model = await colOpt.getRelatedTable();
for (const col of await model.getColumns()) {
if (!isLinksOrLTAR(col.uidt)) continue;
if (col.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt1 = await col.getColOptions<LinkToAnotherRecordColumn>();
if (!colOpt1 || colOpt1.type !== RelationTypes.HAS_MANY) continue;
@ -265,14 +253,10 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
const virtualColumns = [
...hasMany.map((hm) => {
return {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: pluralize(hm.title),
meta: {
plural: pluralize(hm.title),
singular: singularize(hm.title),
},
title: `${hm.title} List`,
};
}),
...belongsTo.map((bt) => {
@ -360,7 +344,6 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
order: colOrder++,
fk_related_model_id: column.hm ? tnId : rtnId,
system: column.system,
meta: column.meta,
});
// nested relations data apis
@ -452,53 +435,3 @@ export async function populateMeta(base: Base, project: Project): Promise<any> {
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 {
CacheDelDirection,
@ -10,7 +10,6 @@ import Noco from '../Noco';
import addFormulaErrorIfMissingColumn from '../helpers/addFormulaErrorIfMissingColumn';
import { NcError } from '../helpers/catchError';
import { extractProps } from '../helpers/extractProps';
import { stringifyMetaProp } from '../utils/modelUtils';
import FormulaColumn from './FormulaColumn';
import LinkToAnotherRecordColumn from './LinkToAnotherRecordColumn';
import LookupColumn from './LookupColumn';
@ -22,7 +21,6 @@ import Sort from './Sort';
import Filter from './Filter';
import QrCodeColumn from './QrCodeColumn';
import BarcodeColumn from './BarcodeColumn';
import { LinksColumn } from './index';
import type { ColumnReqType, ColumnType } from 'nocodb-sdk';
export default class Column<T = any> implements ColumnType {
@ -208,7 +206,6 @@ export default class Column<T = any> implements ColumnType {
);
break;
}
case UITypes.Links:
case UITypes.LinkToAnotherRecord: {
await LinkToAnotherRecordColumn.insert(
{
@ -425,9 +422,6 @@ export default class Column<T = any> implements ColumnType {
case UITypes.LinkToAnotherRecord:
res = await LinkToAnotherRecordColumn.read(this.id, ncMeta);
break;
case UITypes.Links:
res = await LinksColumn.read(this.id, ncMeta);
break;
case UITypes.MultiSelect:
res = await SelectOption.read(this.id, ncMeta);
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]);
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 (isLinksOrLTAR(col.uidt)) {
if (col.uidt === UITypes.LinkToAnotherRecord) {
{
// get lookup columns using relation and delete
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;
break;
case UITypes.LinkToAnotherRecord:
case UITypes.Links:
colOptionTableName = MetaTable.COL_RELATIONS;
cacheScopeName = CacheScope.COL_RELATION;
break;
@ -1213,29 +1206,4 @@ export default class Column<T = any> implements ColumnType {
}
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';
export default class LinkToAnotherRecordColumn {
id: string;
fk_column_id: string;
fk_column_id?: string;
fk_child_column_id?: string;
fk_parent_column_id?: string;
fk_mm_model_id?: string;
@ -127,4 +126,6 @@ export default class LinkToAnotherRecordColumn {
}
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);
if (sortObj.push_to_top) {
// todo: delete cache
const sortList = await ncMeta.metaList2(null, null, MetaTable.SORT, {
condition: { fk_view_id: sortObj.fk_view_id },
orderBy: {
order: 'asc',
},
});
await NocoCache.delAll(CacheScope.SORT, `${sortObj.fk_view_id}:*`);
await NocoCache.setList(CacheScope.SORT, [sortObj.fk_view_id], sortList);
} else {
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 Noco from '../Noco';
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 User } from './User';
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 { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { isLinksOrLTAR } from 'nocodb-sdk';
import extractRolesObj from '../../../../utils/extractRolesObj';
import { AttachmentsService } from '../../../../services/attachments.service';
import { ColumnsService } from '../../../../services/columns.service';
@ -271,7 +270,7 @@ export class AtImportProcessor {
// base mapping table
const aTblNcTypeMap = {
foreignKey: UITypes.Links,
foreignKey: UITypes.LinkToAnotherRecord,
text: UITypes.SingleLineText,
multilineText: UITypes.LongText,
richText: UITypes.LongText,
@ -786,7 +785,7 @@ export class AtImportProcessor {
const ncTbl: any = await this.columnsService.columnAdd({
tableId: srcTableId,
column: {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: ncName.title,
column_name: ncName.column_name,
parentId: srcTableId,
@ -868,14 +867,14 @@ export class AtImportProcessor {
updateMigrationSkipLog(
parentTblSchema?.title,
ncLinkMappingTable[x].nc.title,
UITypes.Links,
UITypes.LinkToAnotherRecord,
'Link error',
);
continue;
}
// hack // fix me
if (!isLinksOrLTAR(parentLinkColumn)) {
if (parentLinkColumn.uidt !== 'LinkToAnotherRecord') {
parentLinkColumn = parentTblSchema.columns.find(
(col) => col.title === ncLinkMappingTable[x].nc.title + '_2',
);
@ -889,7 +888,7 @@ export class AtImportProcessor {
//
childLinkColumn = childTblSchema.columns.find(
(col) =>
isLinksOrLTAR(col) &&
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions.fk_child_column_id ===
parentLinkColumn.colOptions.fk_child_column_id &&
col.colOptions.fk_parent_column_id ===
@ -901,7 +900,7 @@ export class AtImportProcessor {
//
childLinkColumn = childTblSchema.columns.find(
(col) =>
isLinksOrLTAR(col) &&
col.uidt === UITypes.LinkToAnotherRecord &&
col.colOptions.fk_child_column_id ===
parentLinkColumn.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
if (
!value &&
!isLinksOrLTAR(dt) &&
dt !== UITypes.LinkToAnotherRecord &&
dt !== UITypes.Lookup &&
dt !== UITypes.Rollup
) {
@ -1451,7 +1450,6 @@ export class AtImportProcessor {
// we will pick up LTAR once all table data's are in place
case UITypes.LinkToAnotherRecord:
case UITypes.Links:
if (storeLinks) {
if (ncLinkDataStore[table.title][record.id] === undefined)
ncLinkDataStore[table.title][record.id] = {
@ -1972,7 +1970,9 @@ export class AtImportProcessor {
migrationStatsObj.aTbl.rollup = aTblRollup.length;
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 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 { Job } from 'bull';
import papaparse from 'papaparse';
import { UITypes } from 'nocodb-sdk';
import { Logger } from '@nestjs/common';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { Base, Column, Model, Project } from '../../../../models';
import { ProjectsService } from '../../../../services/projects.service';
import { findWithIdentifier } from '../../../../helpers/exportImportHelpers';
@ -137,7 +137,7 @@ export class DuplicateProcessor {
await sourceModel.getColumns();
const relatedModelIds = sourceModel.columns
.filter((col) => isLinksOrLTAR(col))
.filter((col) => col.uidt === UITypes.LinkToAnotherRecord)
.map((col) => col.colOptions.fk_related_model_id)
.filter((id) => id);
@ -186,7 +186,7 @@ export class DuplicateProcessor {
const bts = md.columns
.filter(
(c) =>
isLinksOrLTAR(c) &&
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type === 'bt' &&
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 { isLinksOrLTAR, UITypes, ViewTypes } from 'nocodb-sdk';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { unparse } from 'papaparse';
import { Injectable, Logger } from '@nestjs/common';
import NcConnectionMgrv2 from '../../../../utils/common/NcConnectionMgrv2';
@ -352,12 +352,14 @@ export class ExportService {
.map((c) => c.title)
.join(',')
: model.columns
.filter((c) => !isLinksOrLTAR(c))
.filter((c) => c.uidt !== UITypes.LinkToAnotherRecord)
.map((c) => c.title)
.join(',');
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;

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -3,7 +3,6 @@ import DOMPurify from 'isomorphic-dompurify';
import {
AuditOperationSubTypes,
AuditOperationTypes,
isLinksOrLTAR,
isVirtualCol,
ModelTypes,
UITypes,
@ -15,7 +14,13 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import getColumnUiType from '../helpers/getColumnUiType';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
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 NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { validatePayload } from '../helpers';
@ -151,7 +156,9 @@ export class TablesService {
const project = await Project.getWithInfo(table.project_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) {
const referredTables = await Promise.all(

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

@ -1,13 +1,13 @@
import { UITypes } from 'nocodb-sdk';
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 { isPg, isSqlite } from '../init/db';
import type Column from '../../../src/models/Column';
import type FormViewColumn from '../../../src/models/FormViewColumn';
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';
import Project from '../../../src/models/Project';
import View from '../../../src/models/View';
import { isSqlite, isPg } from '../init/db';
const defaultColumns = function (context) {
return [
@ -55,7 +55,7 @@ const createColumn = async (context, table, columnAttr) => {
});
const column: Column = (await table.getColumns()).find(
(column) => column.title === columnAttr.title,
(column) => column.title === columnAttr.title
);
return column;
};
@ -76,7 +76,7 @@ const createRollupColumn = async (
table: Model;
relatedTableName: string;
relatedTableColumnTitle: string;
},
}
) => {
const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({
@ -86,14 +86,13 @@ const createRollupColumn = async (
});
const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle,
(column) => column.title === relatedTableColumnTitle
);
const ltarColumn = (await table.getColumns()).find(
(column) =>
(column.uidt === UITypes.Links ||
column.uidt === UITypes.LinkToAnotherRecord) &&
column.colOptions?.fk_related_model_id === childTable.id,
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions?.fk_related_model_id === childTable.id
);
const rollupColumn = await createColumn(context, table, {
@ -123,7 +122,7 @@ const createLookupColumn = async (
table: Model;
relatedTableName: string;
relatedTableColumnTitle: string;
},
}
) => {
const childBases = await project.getBases();
const childTable = await Model.getByIdOrName({
@ -133,20 +132,19 @@ const createLookupColumn = async (
});
const childTableColumns = await childTable.getColumns();
const childTableColumn = await childTableColumns.find(
(column) => column.title === relatedTableColumnTitle,
(column) => column.title === relatedTableColumnTitle
);
if (!childTableColumn) {
throw new Error(
`Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`,
`Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`
);
}
const ltarColumn = (await table.getColumns()).find(
(column) =>
(column.uidt === UITypes.Links ||
column.uidt === UITypes.LinkToAnotherRecord) &&
column.colOptions?.fk_related_model_id === childTable.id,
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions?.fk_related_model_id === childTable.id
);
const lookupColumn = await createColumn(context, table, {
title: title,
@ -170,15 +168,15 @@ const createQrCodeColumn = async (
title: string;
table: Model;
referencedQrValueTableColumnTitle: string;
},
}
) => {
const referencedQrValueTableColumnId = await table
.getColumns()
.then(
(cols) =>
cols.find(
(column) => column.title == referencedQrValueTableColumnTitle,
)['id'],
(column) => column.title == referencedQrValueTableColumnTitle
)['id']
);
const qrCodeColumn = await createColumn(context, table, {
@ -200,15 +198,15 @@ const createBarcodeColumn = async (
title: string;
table: Model;
referencedBarcodeValueTableColumnTitle: string;
},
}
) => {
const referencedBarcodeValueTableColumnId = await table
.getColumns()
.then(
(cols) =>
cols.find(
(column) => column.title == referencedBarcodeValueTableColumnTitle,
)['id'],
(column) => column.title == referencedBarcodeValueTableColumnTitle
)['id']
);
const barcodeColumn = await createColumn(context, table, {
@ -232,12 +230,12 @@ const createLtarColumn = async (
parentTable: Model;
childTable: Model;
type: string;
},
}
) => {
const ltarColumn = await createColumn(context, parentTable, {
title: title,
column_name: title,
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
parentId: parentTable.id,
childId: childTable.id,
type: type,
@ -248,7 +246,7 @@ const createLtarColumn = async (
const updateViewColumn = async (
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)
.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',
});
const paymentListColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const nestedFilter = {
is_group: true,
status: 'create',
@ -439,6 +443,12 @@ function tableTest() {
comparison_op: 'like',
value: '%a%',
},
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
],
};
@ -503,6 +513,10 @@ function tableTest() {
relatedTableColumnTitle: 'FirstName',
});
const paymentListColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const returnDateColumn = (await rentalTable.getColumns()).find(
(c) => c.title === 'ReturnDate',
);
@ -519,6 +533,12 @@ function tableTest() {
comparison_op: 'like',
value: '%a%',
},
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{
is_group: true,
status: 'create',
@ -611,7 +631,7 @@ function tableTest() {
});
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payments',
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find(
@ -766,12 +786,16 @@ function tableTest() {
relatedTableColumnTitle: 'RentalDate',
});
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Active',
);
const nestedFields = {
Rentals: { fields: ['RentalDate', 'ReturnDate'] },
'Rental List': { fields: ['RentalDate', 'ReturnDate'] },
};
const nestedFilter = [
@ -794,6 +818,12 @@ function tableTest() {
comparison_op: 'lte',
value: 30,
},
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{
is_group: true,
status: 'create',
@ -837,7 +867,7 @@ function tableTest() {
}
const nestedRentalResponse = Object.keys(
ascResponse.body.list[0]['Rentals'],
ascResponse.body.list[0]['Rental List'],
);
if (
nestedRentalResponse.includes('ReturnDate') &&
@ -1067,12 +1097,16 @@ function tableTest() {
relatedTableColumnTitle: 'RentalDate',
});
const paymentListColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Payment List',
);
const activeColumn = (await customerTable.getColumns()).find(
(c) => c.title === 'Active',
);
const nestedFields = {
Rentals: {
'Rental List': {
f: 'RentalDate,ReturnDate',
},
};
@ -1097,6 +1131,12 @@ function tableTest() {
comparison_op: 'lte',
value: 30,
},
{
fk_column_id: paymentListColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'notblank',
},
{
is_group: true,
status: 'create',
@ -1132,7 +1172,7 @@ function tableTest() {
throw new Error('Wrong filter');
}
const nestedRentalResponse = Object.keys(ascResponse.body['Rentals']);
const nestedRentalResponse = Object.keys(ascResponse.body['Rental List']);
if (
nestedRentalResponse.includes('RentalId') &&
nestedRentalResponse.includes('RentalDate') &&
@ -1250,15 +1290,24 @@ function tableTest() {
)
.set('xc-auth', context.token)
.query({
'nested[Films][fields]': 'Title,ReleaseYear,Language',
'nested[Film List][fields]': 'Title,ReleaseYear,Language',
})
.expect(200);
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)) {
expect(record['Films']).to.equal('19');
} else {
expect(record['Films']).to.equal(19);
expect(record['Film List'][0]['Language']).to.have.all.keys(
'Name',
'LanguageId',
);
}
});
@ -1634,7 +1683,7 @@ function tableTest() {
it('Nested row list hm', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const response = await request(context.app)
.get(
@ -1653,7 +1702,7 @@ function tableTest() {
it('Nested row list hm with limit and offset', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const response = await request(context.app)
.get(
@ -1679,7 +1728,7 @@ function tableTest() {
it('Row list hm with invalid table id', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const response = await request(context.app)
.get(
@ -1703,7 +1752,7 @@ function tableTest() {
// const visibleColumns = [firstNameColumn];
// const rentalListColumn = (await customerTable.getColumns()).find(
// (column) => column.title === 'Rentals'
// (column) => column.title === 'Rental List'
// )!;
// const response = await request(context.app)
// .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 filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const response = await request(context.app)
.get(
@ -1755,7 +1804,7 @@ function tableTest() {
});
const filmTable = await getTable({ project: sakilaProject, name: 'film' });
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const response = await request(context.app)
.get(
@ -1786,7 +1835,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const response = await request(context.app)
.get(
@ -1804,7 +1853,7 @@ function tableTest() {
it('Create hm relation with invalid table id', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const refId = 1;
const response = await request(context.app)
@ -1861,7 +1910,7 @@ function tableTest() {
// it.only('Create list mm existing ref row id', async () => {
// const rowId = 1;
// const rentalListColumn = (await customerTable.getColumns()).find(
// (column) => column.title === 'Rentals'
// (column) => column.title === 'Rental List'
// )!;
// const refId = 1;
@ -1879,7 +1928,7 @@ function tableTest() {
it('Create list hm', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const refId = 1;
@ -1966,7 +2015,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const refId = 1;
@ -1988,7 +2037,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const refId = 2;
@ -2057,7 +2106,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const refId = 1;
@ -2097,7 +2146,7 @@ function tableTest() {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const refId = 76;
@ -2146,9 +2195,8 @@ function tableTest() {
column: ltarColumn,
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)
.delete(
`/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'] });
// LTAR now returns rollup count
if (!(updatedRow['Ltar'] === 0 || updatedRow['Ltar'] === '0')) {
if (updatedRow['Ltar'].length !== 0) {
throw new Error('Was not deleted');
}
@ -2173,7 +2220,7 @@ function tableTest() {
it('Exclude list hm', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const response = await request(context.app)
@ -2192,7 +2239,7 @@ function tableTest() {
it('Exclude list hm with limit and offset', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const response = await request(context.app)
@ -2224,7 +2271,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const response = await request(context.app)
@ -2247,7 +2294,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const response = await request(context.app)
@ -2321,7 +2368,7 @@ function tableTest() {
it('Create nested hm relation with invalid table id', async () => {
const rowId = 1;
const rentalListColumn = (await customerTable.getColumns()).find(
(column) => column.title === 'Rentals',
(column) => column.title === 'Rental List',
)!;
const refId = 1;
const response = await request(context.app)
@ -2344,7 +2391,7 @@ function tableTest() {
name: 'actor',
});
const filmListColumn = (await actorTable.getColumns()).find(
(column) => column.title === 'Films',
(column) => column.title === 'Film List',
)!;
const response = await request(context.app)
.post(

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

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

220
tests/playwright/fixtures/expectedBaseDownloadData.txt

@ -1,110 +1,110 @@
Country,Cities
Afghanistan,1
Algeria,3
American Samoa,1
Angola,2
Anguilla,1
Argentina,13
Armenia,1
Australia,1
Austria,3
Azerbaijan,2
Bahrain,1
Bangladesh,3
Belarus,2
Bolivia,2
Brazil,28
Brunei,1
Bulgaria,2
Cambodia,2
Cameroon,2
Canada,7
Chad,1
Chile,3
China,53
Colombia,6
"Congo, The Democratic Republic of the",2
Czech Republic,1
Dominican Republic,3
Ecuador,3
Egypt,6
Estonia,1
Ethiopia,1
Faroe Islands,1
Finland,1
France,4
French Guiana,1
French Polynesia,2
Gambia,1
Germany,7
Greece,2
Greenland,1
Holy See (Vatican City State),1
Hong Kong,1
Hungary,1
India,60
Indonesia,14
Iran,8
Iraq,1
Israel,4
Italy,7
Japan,31
Kazakstan,2
Kenya,2
Kuwait,1
Latvia,2
Liechtenstein,1
Lithuania,1
Madagascar,1
Malawi,1
Malaysia,3
Mexico,30
Moldova,1
Morocco,3
Mozambique,3
Myanmar,2
Nauru,1
Nepal,1
Netherlands,5
New Zealand,1
Nigeria,13
North Korea,1
Oman,2
Pakistan,5
Paraguay,3
Peru,4
Philippines,20
Poland,8
Puerto Rico,2
Romania,2
Runion,1
Russian Federation,28
Saint Vincent and the Grenadines,1
Saudi Arabia,5
Senegal,1
Slovakia,1
South Africa,11
South Korea,5
Spain,5
Sri Lanka,1
Sudan,2
Sweden,1
Switzerland,3
Taiwan,10
Tanzania,3
Thailand,3
Tonga,1
Tunisia,1
Turkey,15
Turkmenistan,1
Tuvalu,1
Ukraine,6
United Arab Emirates,3
United Kingdom,8
United States,35
Venezuela,7
Vietnam,6
"Virgin Islands, U.S.",1
Yemen,4
Yugoslavia,2
Zambia,1
Country,City List
Afghanistan,Kabul
Algeria,"Batna, Bchar, Skikda"
American Samoa,Tafuna
Angola,"Benguela, Namibe"
Anguilla,South Hill
Argentina,"Almirante Brown, Avellaneda, Baha Blanca, Crdoba, Escobar, Ezeiza, La Plata, Merlo, Quilmes, San Miguel de Tucumn, Santa F, Tandil, Vicente Lpez"
Armenia,Yerevan
Australia,Woodridge
Austria,"Graz, Linz, Salzburg"
Azerbaijan,"Baku, Sumqayit"
Bahrain,al-Manama
Bangladesh,"Dhaka, Jamalpur, Tangail"
Belarus,"Mogiljov, Molodetno"
Bolivia,"El Alto, Sucre"
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,Bandar Seri Begawan
Bulgaria,"Ruse, Stara Zagora"
Cambodia,"Battambang, Phnom Penh"
Cameroon,"Bamenda, Yaound"
Canada,"Gatineau, Halifax, Lethbridge, London, Oshawa, Richmond Hill, Vancouver"
Chad,NDjamna
Chile,"Antofagasta, Coquimbo, Rancagua"
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,"Buenaventura, Dos Quebradas, Florencia, Pereira, Sincelejo, Sogamoso"
"Congo, The Democratic Republic of the","Lubumbashi, Mwene-Ditu"
Czech Republic,Olomouc
Dominican Republic,"La Romana, San Felipe de Puerto Plata, Santiago de los Caballeros"
Ecuador,"Loja, Portoviejo, Robamba"
Egypt,"Bilbays, Idfu, Mit Ghamr, Qalyub, Sawhaj, Shubra al-Khayma"
Estonia,Tartu
Ethiopia,Addis Abeba
Faroe Islands,Trshavn
Finland,Oulu
France,"Brest, Le Mans, Toulon, Toulouse"
French Guiana,Cayenne
French Polynesia,"Faaa, Papeete"
Gambia,Banjul
Germany,"Duisburg, Erlangen, Halle/Saale, Mannheim, Saarbrcken, Siegen, Witten"
Greece,"Athenai, Patras"
Greenland,Nuuk
Holy See (Vatican City State),Citt del Vaticano
Hong Kong,Kowloon and New Kowloon
Hungary,Szkesfehrvr
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,"Cianjur, Ciomas, Ciparay, Gorontalo, Jakarta, Lhokseumawe, Madiun, Pangkal Pinang, Pemalang, Pontianak, Probolinggo, Purwakarta, Surakarta, Tegal"
Iran,"Arak, Esfahan, Kermanshah, Najafabad, Qomsheh, Shahr-e Kord, Sirjan, Tabriz"
Iraq,Mosul
Israel,"Ashdod, Ashqelon, Bat Yam, Tel Aviv-Jaffa"
Italy,"Alessandria, Bergamo, Brescia, Brindisi, Livorno, Syrakusa, Udine"
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,"Pavlodar, Zhezqazghan"
Kenya,"Kisumu, Nyeri"
Kuwait,Jalib al-Shuyukh
Latvia,"Daugavpils, Liepaja"
Liechtenstein,Vaduz
Lithuania,Vilnius
Madagascar,Mahajanga
Malawi,Lilongwe
Malaysia,"Ipoh, Kuching, Sungai Petani"
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,Chisinau
Morocco,"Beni-Mellal, Nador, Sal"
Mozambique,"Beira, Naala-Porto, Tete"
Myanmar,"Monywa, Myingyan"
Nauru,Yangor
Nepal,Birgunj
Netherlands,"Amersfoort, Apeldoorn, Ede, Emmen, s-Hertogenbosch"
New Zealand,Hamilton
Nigeria,"Benin City, Deba Habe, Effon-Alaiye, Ife, Ikerre, Ilorin, Kaduna, Ogbomosho, Ondo, Owo, Oyo, Sokoto, Zaria"
North Korea,Pyongyang
Oman,"Masqat, Salala"
Pakistan,"Dadu, Mandi Bahauddin, Mardan, Okara, Shikarpur"
Paraguay,"Asuncin, Ciudad del Este, San Lorenzo"
Peru,"Callao, Hunuco, Lima, Sullana"
Philippines,"Baybay, Bayugan, Bislig, Cabuyao, Cavite, Davao, Gingoog, Hagonoy, Iligan, Imus, Lapu-Lapu, Mandaluyong, Ozamis, Santa Rosa, Taguig, Talavera, Tanauan, Tanza, Tarlac, Tuguegarao"
Poland,"Bydgoszcz, Czestochowa, Jastrzebie-Zdrj, Kalisz, Lublin, Plock, Tychy, Wroclaw"
Puerto Rico,"Arecibo, Ponce"
Romania,"Botosani, Bucuresti"
Runion,Saint-Denis
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,Kingstown
Saudi Arabia,"Abha, al-Hawiya, al-Qatif, Jedda, Tabuk"
Senegal,Ziguinchor
Slovakia,Bratislava
South Africa,"Boksburg, Botshabelo, Chatsworth, Johannesburg, Kimberley, Klerksdorp, Newcastle, Paarl, Rustenburg, Soshanguve, Springs"
South Korea,"Cheju, Kimchon, Naju, Tonghae, Uijongbu"
Spain,"A Corua (La Corua), Donostia-San Sebastin, Gijn, Ourense (Orense), Santiago de Compostela"
Sri Lanka,Jaffna
Sudan,"al-Qadarif, Omdurman"
Sweden,Malm
Switzerland,"Basel, Bern, Lausanne"
Taiwan,"Changhwa, Chiayi, Chungho, Fengshan, Hsichuh, Lungtan, Nantou, Tanshui, Touliu, Tsaotun"
Tanzania,"Mwanza, Tabora, Zanzibar"
Thailand,"Nakhon Sawan, Pak Kret, Songkhla"
Tonga,Nukualofa
Tunisia,Sousse
Turkey,"Adana, Balikesir, Batman, Denizli, Eskisehir, Gaziantep, Inegl, Kilis, Ktahya, Osmaniye, Sivas, Sultanbeyli, Tarsus, Tokat, Usak"
Turkmenistan,Ashgabat
Tuvalu,Funafuti
Ukraine,"Kamjanets-Podilskyi, Konotop, Mukateve, ostka, Simferopol, Sumy"
United Arab Emirates,"Abu Dhabi, al-Ayn, Sharja"
United Kingdom,"Bradford, Dundee, London, Southampton, Southend-on-Sea, Southport, Stockport, York"
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,"Barcelona, Caracas, Cuman, Maracabo, Ocumare del Tuy, Valencia, Valle de la Pascua"
Vietnam,"Cam Ranh, Haiphong, Hanoi, Nam Dinh, Nha Trang, Vinh"
"Virgin Islands, U.S.",Charlotte Amalie
Yemen,"Aden, Hodeida, Sanaa, Taizz"
Yugoslavia,"Kragujevac, Novi Sad"
Zambia,Kitwe

220
tests/playwright/fixtures/expectedBaseDownloadDataPg.txt

@ -1,110 +1,110 @@
Country,Cities
Afghanistan,1
Algeria,3
American Samoa,1
Angola,2
Anguilla,1
Argentina,13
Armenia,1
Australia,1
Austria,3
Azerbaijan,2
Bahrain,1
Bangladesh,3
Belarus,2
Bolivia,2
Brazil,28
Brunei,1
Bulgaria,2
Cambodia,2
Cameroon,2
Canada,7
Chad,1
Chile,3
China,53
Colombia,6
"Congo, The Democratic Republic of the",2
Czech Republic,1
Dominican Republic,3
Ecuador,3
Egypt,6
Estonia,1
Ethiopia,1
Faroe Islands,1
Finland,1
France,4
French Guiana,1
French Polynesia,2
Gambia,1
Germany,7
Greece,2
Greenland,1
Holy See (Vatican City State),1
Hong Kong,1
Hungary,1
India,60
Indonesia,14
Iran,8
Iraq,1
Israel,4
Italy,7
Japan,31
Kazakstan,2
Kenya,2
Kuwait,1
Latvia,2
Liechtenstein,1
Lithuania,1
Madagascar,1
Malawi,1
Malaysia,3
Mexico,30
Moldova,1
Morocco,3
Mozambique,3
Myanmar,2
Nauru,1
Nepal,1
Netherlands,5
New Zealand,1
Nigeria,13
North Korea,1
Oman,2
Pakistan,5
Paraguay,3
Peru,4
Philippines,20
Poland,8
Puerto Rico,2
Romania,2
Runion,1
Russian Federation,28
Saint Vincent and the Grenadines,1
Saudi Arabia,5
Senegal,1
Slovakia,1
South Africa,11
South Korea,5
Spain,5
Sri Lanka,1
Sudan,2
Sweden,1
Switzerland,3
Taiwan,10
Tanzania,3
Thailand,3
Tonga,1
Tunisia,1
Turkey,15
Turkmenistan,1
Tuvalu,1
Ukraine,6
United Arab Emirates,3
United Kingdom,8
United States,35
Venezuela,7
Vietnam,6
"Virgin Islands, U.S.",1
Yemen,4
Yugoslavia,2
Zambia,1
Country,City List
Afghanistan,Kabul
Algeria,"Batna, Bchar, Skikda"
American Samoa,Tafuna
Angola,"Benguela, Namibe"
Anguilla,South Hill
Argentina,"Almirante Brown, Avellaneda, Baha Blanca, Crdoba, Escobar, Ezeiza, La Plata, Merlo, Quilmes, San Miguel de Tucumn, Santa F, Tandil, Vicente Lpez"
Armenia,Yerevan
Australia,Woodridge
Austria,"Graz, Linz, Salzburg"
Azerbaijan,"Baku, Sumqayit"
Bahrain,al-Manama
Bangladesh,"Dhaka, Jamalpur, Tangail"
Belarus,"Mogiljov, Molodetno"
Bolivia,"El Alto, Sucre"
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,Bandar Seri Begawan
Bulgaria,"Ruse, Stara Zagora"
Cambodia,"Battambang, Phnom Penh"
Cameroon,"Bamenda, Yaound"
Canada,"Gatineau, Halifax, Lethbridge, London, Oshawa, Richmond Hill, Vancouver"
Chad,NDjamna
Chile,"Antofagasta, Coquimbo, Rancagua"
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,"Buenaventura, Dos Quebradas, Florencia, Pereira, Sincelejo, Sogamoso"
"Congo, The Democratic Republic of the","Lubumbashi, Mwene-Ditu"
Czech Republic,Olomouc
Dominican Republic,"La Romana, San Felipe de Puerto Plata, Santiago de los Caballeros"
Ecuador,"Loja, Portoviejo, Robamba"
Egypt,"Bilbays, Idfu, Mit Ghamr, Qalyub, Sawhaj, Shubra al-Khayma"
Estonia,Tartu
Ethiopia,Addis Abeba
Faroe Islands,Trshavn
Finland,Oulu
France,"Brest, Le Mans, Toulon, Toulouse"
French Guiana,Cayenne
French Polynesia,"Faaa, Papeete"
Gambia,Banjul
Germany,"Duisburg, Erlangen, Halle/Saale, Mannheim, Saarbrcken, Siegen, Witten"
Greece,"Athenai, Patras"
Greenland,Nuuk
Holy See (Vatican City State),Citt del Vaticano
Hong Kong,Kowloon and New Kowloon
Hungary,Szkesfehrvr
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,"Cianjur, Ciomas, Ciparay, Gorontalo, Jakarta, Lhokseumawe, Madiun, Pangkal Pinang, Pemalang, Pontianak, Probolinggo, Purwakarta, Surakarta, Tegal"
Iran,"Arak, Esfahan, Kermanshah, Najafabad, Qomsheh, Shahr-e Kord, Sirjan, Tabriz"
Iraq,Mosul
Israel,"Ashdod, Ashqelon, Bat Yam, Tel Aviv-Jaffa"
Italy,"Alessandria, Bergamo, Brescia, Brindisi, Livorno, Syrakusa, Udine"
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,"Pavlodar, Zhezqazghan"
Kenya,"Kisumu, Nyeri"
Kuwait,Jalib al-Shuyukh
Latvia,"Daugavpils, Liepaja"
Liechtenstein,Vaduz
Lithuania,Vilnius
Madagascar,Mahajanga
Malawi,Lilongwe
Malaysia,"Ipoh, Kuching, Sungai Petani"
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,Chisinau
Morocco,"Beni-Mellal, Nador, Sal"
Mozambique,"Beira, Naala-Porto, Tete"
Myanmar,"Monywa, Myingyan"
Nauru,Yangor
Nepal,Birgunj
Netherlands,"Amersfoort, Apeldoorn, Ede, Emmen, s-Hertogenbosch"
New Zealand,Hamilton
Nigeria,"Benin City, Deba Habe, Effon-Alaiye, Ife, Ikerre, Ilorin, Kaduna, Ogbomosho, Ondo, Owo, Oyo, Sokoto, Zaria"
North Korea,Pyongyang
Oman,"Masqat, Salala"
Pakistan,"Dadu, Mandi Bahauddin, Mardan, Okara, Shikarpur"
Paraguay,"Asuncin, Ciudad del Este, San Lorenzo"
Peru,"Callao, Hunuco, Lima, Sullana"
Philippines,"Baybay, Bayugan, Bislig, Cabuyao, Cavite, Davao, Gingoog, Hagonoy, Iligan, Imus, Lapu-Lapu, Mandaluyong, Ozamis, Santa Rosa, Taguig, Talavera, Tanauan, Tanza, Tarlac, Tuguegarao"
Poland,"Bydgoszcz, Czestochowa, Jastrzebie-Zdrj, Kalisz, Lublin, Plock, Tychy, Wroclaw"
Puerto Rico,"Arecibo, Ponce"
Romania,"Botosani, Bucuresti"
Runion,Saint-Denis
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,Kingstown
Saudi Arabia,"Abha, al-Hawiya, al-Qatif, Jedda, Tabuk"
Senegal,Ziguinchor
Slovakia,Bratislava
South Africa,"Boksburg, Botshabelo, Chatsworth, Johannesburg, Kimberley, Klerksdorp, Newcastle, Paarl, Rustenburg, Soshanguve, Springs"
South Korea,"Cheju, Kimchon, Naju, Tonghae, Uijongbu"
Spain,"A Corua (La Corua), Donostia-San Sebastin, Gijn, Ourense (Orense), Santiago de Compostela"
Sri Lanka,Jaffna
Sudan,"al-Qadarif, Omdurman"
Sweden,Malm
Switzerland,"Basel, Bern, Lausanne"
Taiwan,"Changhwa, Chiayi, Chungho, Fengshan, Hsichuh, Lungtan, Nantou, Tanshui, Touliu, Tsaotun"
Tanzania,"Mwanza, Tabora, Zanzibar"
Thailand,"Nakhon Sawan, Pak Kret, Songkhla"
Tonga,Nukualofa
Tunisia,Sousse
Turkey,"Adana, Balikesir, Batman, Denizli, Eskisehir, Gaziantep, Inegl, Kilis, Ktahya, Osmaniye, Sivas, Sultanbeyli, Tarsus, Tokat, Usak"
Turkmenistan,Ashgabat
Tuvalu,Funafuti
Ukraine,"Kamjanets-Podilskyi, Konotop, Mukateve, ostka, Simferopol, Sumy"
United Arab Emirates,"Abu Dhabi, al-Ayn, Sharja"
United Kingdom,"Bradford, Dundee, London, Southampton, Southend-on-Sea, Southport, Stockport, York"
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,"Barcelona, Caracas, Cuman, Maracabo, Ocumare del Tuy, Valencia, Valle de la Pascua"
Vietnam,"Cam Ranh, Haiphong, Hanoi, Nam Dinh, Nha Trang, Vinh"
"Virgin Islands, U.S.",Charlotte Amalie
Yemen,"Aden, Hodeida, Sanaa, Taizz"
Yugoslavia,"Kragujevac, Novi Sad"
Zambia,Kitwe

8
tests/playwright/fixtures/expectedData.txt

@ -1,4 +1,4 @@
Address,District,PostalCode,Phone,Location,Customers,Staffs,City
1661 Abha Drive,Tamil Nadu,14400,270456873752,"{""x"":78.8214191,""y"":10.3812871}",1,0,Pudukkottai
1993 Tabuk Lane,Tamil Nadu,64221,648482415405,"{""x"":80.1270701,""y"":12.9246028}",1,0,Tambaram
381 Kabul Way,Taipei,87272,55477302294,"{""x"":0,""y"":0}",1,0,Hsichuh
Address,District,PostalCode,Phone,Location,Customer List,Staff List,City
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}",2,,Tambaram
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
1013 Tabuk Boulevard," ",96203," ",1,0,Kanchrapara
1168 Najafabad Parkway," ",40301," ",1,0,Kabul
1294 Firozabad Drive," ",70618," ",1,0,Pingxiang
1342 Abha Boulevard," ",10714," ",1,0,Bucuresti
1368 Maracabo Boulevard," ",32716," ",1,0,South Hill
1427 Tabuk Place," ",31342," ",1,0,Cape Coral
1519 Santiago de los Caballeros Loop," ",22025," ",1,0,Mwene-Ditu
1661 Abha Drive," ",14400," ",1,0,Pudukkottai
17 Kabul Boulevard," ",38594," ",1,0,Nagareyama
1838 Tabriz Lane," ",1195," ",1,0,Dhaka
1888 Kabul Drive," ",20936," ",1,0,Ife
1892 Nabereznyje Telny Lane," ",28396," ",1,0,Tafuna
1993 Tabuk Lane," ",64221," ",1,0,Tambaram
217 Botshabelo Place," ",49521," ",1,0,Davao
381 Kabul Way," ",87272," ",1,0,Hsichuh
44 Najafabad Way," ",61391," ",1,0,Donostia-San Sebastin
48 Maracabo Place," ",1570," ",1,0,Talavera
669 Firozabad Loop," ",92265," ",1,0,al-Ayn
Address,District,PostalCode,Phone,Customer List,Staff List,City
1013 Tabuk Boulevard," ",96203," ",2,,Kanchrapara
1168 Najafabad Parkway," ",40301," ",1,,Kabul
1294 Firozabad Drive," ",70618," ",2,,Pingxiang
1342 Abha Boulevard," ",10714," ",2,,Bucuresti
1368 Maracabo Boulevard," ",32716," ",2,,South Hill
1427 Tabuk Place," ",31342," ",2,,Cape Coral
1519 Santiago de los Caballeros Loop," ",22025," ",2,,Mwene-Ditu
1661 Abha Drive," ",14400," ",1,,Pudukkottai
17 Kabul Boulevard," ",38594," ",1,,Nagareyama
1838 Tabriz Lane," ",1195," ",1,,Dhaka
1888 Kabul Drive," ",20936," ",1,,Ife
1892 Nabereznyje Telny Lane," ",28396," ",2,,Tafuna
1993 Tabuk Lane," ",64221," ",2,,Tambaram
217 Botshabelo Place," ",49521," ",2,,Davao
381 Kabul Way," ",87272," ",2,,Hsichuh
44 Najafabad Way," ",61391," ",2,,Donostia-San Sebastin
48 Maracabo Place," ",1570," ",1,,Talavera
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`, {
hasText: childColumn,
})
.last()
.click();
break;
case 'Rollup':
@ -163,7 +162,7 @@ export class ColumnPageObject extends BasePage {
.nth(0)
.click();
break;
case 'Links':
case 'LinkToAnotherRecord':
await this.get()
.locator('.nc-ltar-relation-type >> .ant-radio')
.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();
// 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 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();
// expand row
await this.cell.get({ index: 0, columnHeader: 'Cities' }).hover();
await this.cell.get({ index: 0, columnHeader: 'City List' }).hover();
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();
}
@ -327,9 +327,15 @@ export class GridPage extends BasePage {
await expect(await this.rootPage.locator('text=Insert New Row')).toBeVisible();
// 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 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();
}

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

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

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
// 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
* 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
@ -131,8 +131,8 @@ const formulaDataByDbType = (context: NcContext) => [
result: isPg(context) ? ['false', 'false', 'false', 'false', 'false'] : ['0', '0', '0', '0', '0'],
},
{
formula: `IF((SEARCH({City}, "Ad") != 0), "2.0","WRONG")`,
result: ['WRONG', 'WRONG', 'WRONG', 'WRONG', '2.0'],
formula: `IF((SEARCH({Address List}, "Parkway") != 0), "2.0","WRONG")`,
result: ['WRONG', 'WRONG', 'WRONG', '2.0', '2.0'],
},
// 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
await dashboard.grid.column.create({
title: 'Link1-2hm',
type: 'Links',
type: 'LinkToAnotherRecord',
childTable: 'Sheet2',
relationType: 'Has Many',
});
await dashboard.grid.column.create({
title: 'Link1-2mm',
type: 'Links',
type: 'LinkToAnotherRecord',
childTable: 'Sheet2',
relationType: 'Many To many',
});
@ -46,7 +46,7 @@ test.describe('LTAR create & update', () => {
await dashboard.treeView.openTable({ title: 'Sheet2', networkResponse: false });
await dashboard.grid.column.create({
title: 'Link2-1hm',
type: 'Links',
type: 'LinkToAnotherRecord',
childTable: 'Sheet1',
relationType: 'Has Many',
});
@ -67,7 +67,7 @@ test.describe('LTAR create & update', () => {
type: 'belongsTo',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1s',
columnTitle: 'Sheet1 List',
value: '1a',
type: 'manyToMany',
});
@ -84,7 +84,7 @@ test.describe('LTAR create & update', () => {
await dashboard.linkRecord.select('1b');
await dashboard.grid.cell.inCellAdd({
index: 1,
columnHeader: 'Sheet1s',
columnHeader: 'Sheet1 List',
});
await dashboard.linkRecord.select('1b');
await dashboard.grid.cell.inCellAdd({
@ -102,7 +102,7 @@ test.describe('LTAR create & update', () => {
type: 'belongsTo',
});
await dashboard.expandedForm.fillField({
columnTitle: 'Sheet1s',
columnTitle: 'Sheet1 List',
value: '1c',
type: 'manyToMany',
});
@ -123,10 +123,10 @@ test.describe('LTAR create & update', () => {
const expected = [
[['1a'], ['1b'], ['1c']],
[['1 Sheet1'], ['1 Sheet1'], ['1 Sheet1']],
[['1 Sheet1'], ['1 Sheet1'], ['1 Sheet1']],
[['1a'], ['1b'], ['1c']],
[['1a'], ['1b'], ['1c']],
];
const colHeaders = ['Sheet1', 'Sheet1s', 'Link2-1hm'];
const colHeaders = ['Sheet1', 'Sheet1 List', 'Link2-1hm'];
// verify LTAR cell values
for (let i = 0; i < expected.length; i++) {
@ -136,8 +136,6 @@ test.describe('LTAR create & update', () => {
columnHeader: colHeaders[i],
count: 1,
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' });
const expected2 = [
[['1 Sheet2'], ['1 Sheet2'], ['1 Sheet2']],
[['1 Sheet2'], ['1 Sheet2'], ['1 Sheet2']],
[['2a'], ['2b'], ['2c']],
[['2a'], ['2b'], ['2c']],
[['2a'], ['2b'], ['2c']],
];
const colHeaders2 = ['Link1-2hm', 'Link1-2mm', 'Sheet2'];
@ -160,13 +158,11 @@ test.describe('LTAR create & update', () => {
columnHeader: colHeaders2[i],
count: 1,
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 j = 0; j < expected2[i].length; j++) {
await dashboard.grid.cell.unlinkVirtualCell({
@ -192,7 +188,7 @@ test.describe('LTAR create & update', () => {
Country: string;
formula?: string;
SLT?: string;
Cities: string[];
'City List': string[];
};
}) {
await dashboard.grid.cell.verify({
@ -209,9 +205,9 @@ test.describe('LTAR create & update', () => {
}
await dashboard.grid.cell.verifyVirtualCell({
index: param.index,
columnHeader: 'Cities',
count: param.value['Cities'].length,
value: param.value['Cities'],
columnHeader: 'City List',
count: param.value['City List'].length,
value: param.value['City List'],
});
if (param.value.SLT) {
await dashboard.grid.cell.verify({
@ -241,14 +237,14 @@ test.describe('LTAR create & update', () => {
index: 0,
value: {
Country: 'Afghanistan',
Cities: ['Kabul'],
'City List': ['Kabul'],
},
});
await verifyRow({
index: 1,
value: {
Country: 'Algeria',
Cities: ['Batna', 'Bchar', 'Skikda'],
'City List': ['Batna', 'Bchar', 'Skikda'],
},
});
@ -274,7 +270,7 @@ test.describe('LTAR create & update', () => {
index: 0,
value: {
Country: 'Afghanistan',
Cities: ['Kabul'],
'City List': ['Kabul'],
SLT: 'test',
formula: 'Afghanistan test',
},
@ -290,7 +286,7 @@ test.describe('LTAR create & update', () => {
index: 0,
value: {
Country: 'Afghanistan2',
Cities: ['Kabul'],
'City List': ['Kabul'],
SLT: 'test',
formula: 'Afghanistan2 test',
},
@ -305,7 +301,7 @@ test.describe('LTAR create & update', () => {
index: 0,
value: {
Country: 'Afghanistan2',
Cities: ['Kabul'],
'City List': ['Kabul'],
SLT: '',
formula: 'Afghanistan2',
},

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

@ -15,7 +15,7 @@ test.describe('Virtual columns', () => {
// close 'Team & Auth' tab
// 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'];
await dashboard.treeView.openTable({ title: 'City' });
@ -23,14 +23,14 @@ test.describe('Virtual columns', () => {
await dashboard.grid.column.create({
title: 'Lookup',
type: 'Lookup',
childTable: 'Country',
childColumn: 'Country',
childTable: 'Address List',
childColumn: 'PostalCode',
});
for (let i = 0; i < countryList.length; i++) {
for (let i = 0; i < pinCode.length; i++) {
await dashboard.grid.cell.verify({
index: i,
columnHeader: 'Lookup',
value: countryList[i],
value: pinCode[i],
});
}
await dashboard.closeTab({ title: 'City' });
@ -40,11 +40,11 @@ test.describe('Virtual columns', () => {
await dashboard.grid.column.create({
title: 'Rollup',
type: 'Rollup',
childTable: 'Cities',
childTable: 'City List',
childColumn: 'CityId',
rollupType: 'count',
});
for (let i = 0; i < countryList.length; i++) {
for (let i = 0; i < pinCode.length; i++) {
await dashboard.grid.cell.verify({
index: i,
columnHeader: 'Rollup',

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

@ -64,7 +64,7 @@ test.describe('Test table', () => {
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' });
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"]`);
@ -78,14 +78,14 @@ test.describe('Test table', () => {
const linkTable = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(3)
);
expect(columnType).toContain('Links');
expect(columnType).toContain('LinkToAnotherRecord');
expect(linkTable).toContain('Table1');
// save
await columnAddModal.locator(`.ant-btn-primary`).click();
// 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
@ -103,7 +103,7 @@ test.describe('Test table', () => {
// validate
expect(columnType).toContain('Lookup');
expect(linkField).toContain('Table1');
expect(linkField).toContain('Table1List');
expect(childColumn).toContain('Title');
// save

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

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

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

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

@ -188,33 +188,33 @@ test.describe('Erd', () => {
// Verify tables with default config
await erd.verifyColumns({
tableName: `country`,
columns: ['country_id', 'country', 'last_update', 'cities'],
columns: ['country_id', 'country', 'last_update', 'city_list'],
});
await erd.verifyColumns({
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
await erd.clickShowPkAndFk();
await erd.verifyColumns({
tableName: `country`,
columns: ['country', 'last_update', 'cities'],
columns: ['country', 'last_update', 'city_list'],
});
await erd.verifyColumns({
tableName: `city`,
columns: ['city', 'last_update', 'country', 'addresses'],
columns: ['city', 'last_update', 'country', 'address_list'],
});
// Verify with all columns disabled
await erd.clickShowColumnNames();
await erd.verifyColumns({ tableName: `country`, columns: ['cities'] });
await erd.verifyColumns({ tableName: `country`, columns: ['city_list'] });
await erd.verifyColumns({
tableName: `city`,
columns: ['country', 'addresses'],
columns: ['country', 'address_list'],
});
// 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 = [
'payment_id',
@ -338,9 +338,9 @@ const pgPaymentTableColumns = [
'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'];

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

@ -66,7 +66,7 @@ test.describe('Expanded form URL', () => {
title: 'CountryExpand',
});
await viewObj.toolbar.clickFields();
await viewObj.toolbar.fields.click({ title: 'Cities' });
await viewObj.toolbar.fields.click({ title: 'City List' });
}
// expand row & verify URL
@ -104,7 +104,7 @@ test.describe('Expanded form URL', () => {
url: 'rowId=1',
});
await dashboard.expandedForm.openChildCard({
column: 'Cities',
column: 'City List',
title: 'Kabul',
});
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
const filterList = [
{ op: '=', value: '5', rowCount: 5 },
{ op: '!=', value: '5', rowCount: 104 },
{ op: '>', value: '5', rowCount: 25 },
{ op: '<', value: '5', rowCount: 79 },
{ op: '>=', value: '5', rowCount: 30 },
{ op: '<=', value: '5', rowCount: 84 },
{ op: 'is', value: 'Kabul', rowCount: 1 },
{ op: 'is not', value: 'Kabul', rowCount: 108 },
{ op: 'is like', value: 'bad', rowCount: 2 },
{ op: 'is not like', value: 'bad', rowCount: 107 },
{ op: 'is blank', value: null, rowCount: 0 },
{ op: 'is not blank', value: null, rowCount: 109 },
];
await toolbar.clickFilter();
await toolbar.filter.clickAddFilter();
for (let i = 0; i < filterList.length; i++) {
await verifyFilter({
column: 'Cities',
column: 'City List',
opType: filterList[i].op,
value: filterList[i].value,
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({
title: 'Lookup',
type: 'Lookup',
childTable: 'Country',
childColumn: 'Country',
childTable: 'Address List',
childColumn: 'PostalCode',
});
// Enable NULL & EMPTY filters
@ -975,12 +975,12 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
// add filter for CityList column
const filterList = [
{ op: 'is equal', value: 'Spain', rowCount: 5 },
{ op: 'is not equal', value: 'Spain', rowCount: 595 },
{ op: 'is like', value: 'ca', rowCount: 28 },
{ op: 'is not like', value: 'ca', rowCount: 572 },
{ op: 'is blank', value: null, rowCount: 0 },
{ op: 'is not blank', value: null, rowCount: 600 },
{ op: 'is equal', value: '4166', rowCount: 1 },
{ op: 'is not equal', value: '4166', rowCount: 599 },
{ op: 'is like', value: '41', rowCount: 19 },
{ op: 'is not like', value: '41', rowCount: 581 },
{ op: 'is blank', value: null, rowCount: 1 },
{ op: 'is not blank', value: null, rowCount: 599 },
];
await toolbar.clickFilter();
@ -1003,7 +1003,7 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
await dashboard.grid.column.create({
title: 'Rollup',
type: 'Rollup',
childTable: 'Addresses',
childTable: 'Address List',
childColumn: 'PostalCode',
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 like', value: '41', rowCount: 19 },
{ 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 not blank', value: null, rowCount: 598 },
];
@ -1028,11 +1026,11 @@ test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
await toolbar.filter.clickAddFilter();
for (let i = 0; i < filterList.length; i++) {
await verifyFilter({
column: 'Rollup',
column: 'Lookup',
opType: filterList[i].op,
value: filterList[i].value,
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 page.waitForTimeout(1500);
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
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
// TableA <hm> TableB <hm> TableC
await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: `TableA:hm:TableB`,
column_name: `TableA:hm:TableB`,
parentId: tables[0].id,
@ -111,7 +111,7 @@ test.describe.serial('Test table', () => {
type: 'hm',
});
await api.dbTableColumn.create(tables[1].id, {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: `TableB:hm:TableC`,
column_name: `TableB:hm:TableC`,
parentId: tables[1].id,
@ -121,7 +121,7 @@ test.describe.serial('Test table', () => {
// TableA <mm> TableD <mm> TableE
await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: `TableA:mm:TableD`,
column_name: `TableA:mm:TableD`,
parentId: tables[0].id,
@ -129,7 +129,7 @@ test.describe.serial('Test table', () => {
type: 'mm',
});
await api.dbTableColumn.create(tables[3].id, {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: `TableD:mm:TableE`,
column_name: `TableD:mm:TableE`,
parentId: tables[3].id,
@ -139,7 +139,7 @@ test.describe.serial('Test table', () => {
// TableA <hm> TableA : self relation
await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: `TableA:hm:TableA`,
column_name: `TableA:hm:TableA`,
parentId: tables[0].id,
@ -149,7 +149,7 @@ test.describe.serial('Test table', () => {
// TableA <mm> TableA : self relation
await api.dbTableColumn.create(tables[0].id, {
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
title: `TableA:mm:TableA`,
column_name: `TableA:mm:TableA`,
parentId: tables[0].id,
@ -227,25 +227,27 @@ test.describe.serial('Test table', () => {
// has-many removal verification
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: 1, columnHeader: 'Table0', count: 0, value: [], type: 'bt' });
await dashboard.grid.cell.verifyVirtualCell({ index: 2, 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: [] });
await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0', count: 0, value: [] });
// many-many removal verification
await dashboard.treeView.openTable({ title: 'Table3' });
const params = {
index: 0,
columnHeader: 'Table0s',
count: 0,
value: [],
type: 'hm',
options: { singular: 'Table0', plural: 'Table0s' },
};
await dashboard.grid.cell.verifyVirtualCell({ ...params });
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 1, index: 1, value: ['2'] });
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 2, index: 2, value: ['2', '3'] });
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 3, index: 3, value: ['2', '3', '4'] });
await dashboard.grid.cell.verifyVirtualCell({ ...params, count: 4, index: 4, value: ['2', '3', '4', '5'] });
await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0 List', count: 0, value: [] });
await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0 List', count: 1, value: ['2'] });
await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0 List', count: 2, value: ['2', '3'] });
await dashboard.grid.cell.verifyVirtualCell({
index: 3,
columnHeader: 'Table0 List',
count: 3,
value: ['2', '3', '4'],
});
await dashboard.grid.cell.verifyVirtualCell({
index: 4,
columnHeader: 'Table0 List',
count: 4,
value: ['2', '3', '4', '5'],
});
});
test('Delete record - bulk, over UI', async () => {
@ -283,7 +285,7 @@ test.describe.serial('Test table', () => {
// verify
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 });
///////////////////////////////////////////////////////////////////////////////////////////////
@ -303,7 +305,7 @@ test.describe.serial('Test table', () => {
await dashboard.grid.column.delete({ title: 'TableA:mm:TableA' });
// 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 });
});
@ -316,6 +318,6 @@ test.describe.serial('Test table', () => {
await dashboard.grid.column.verify({ title: 'Table0', isVisible: false });
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
await form.verifyFormViewFieldsOrder({
fields: ['Country', 'LastUpdate', 'Cities'],
fields: ['Country', 'LastUpdate', 'City List'],
});
// reorder & verify
@ -54,7 +54,7 @@ test.describe('Mobile Mode', () => {
destinationField: 'Country',
});
await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'],
fields: ['LastUpdate', 'Country', 'City List'],
});
await dashboard.treeView.openTable({ mobileMode: true, title: 'test-table-for-mobile-mode' });

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

@ -527,7 +527,7 @@ test.describe('Undo Redo - LTAR', () => {
await api.dbTableColumn.create(countryTable.id, {
column_name: 'CityList',
title: 'CityList',
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
parentId: countryTable.id,
childId: cityTable.id,
type: 'hm',
@ -546,38 +546,18 @@ test.describe('Undo Redo - LTAR', () => {
// inserted values
const expectedValues = [...values];
try {
const currentRecords: Record<string, any> = await api.dbTableRow.list(
'noco',
context.project.id,
countryTable.id,
{
fields: ['CityList'],
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);
const currentRecords: Record<string, any> = await api.dbTableRow.list('noco', context.project.id, countryTable.id, {
fields: ['CityList'],
limit: 100,
});
for (let i = 0; i < expectedValues.length; i++) {
expect(cities.includes(expectedValues[i])).toBeTruthy();
}
}
// verify if expectedValues array includes all the values in currentRecords
// 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[] }) {

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

@ -611,8 +611,9 @@ test.describe.serial('Webhook', () => {
// create LTAR Country has-many City
countryTable = await api.dbTableColumn.create(countryTable.id, {
column_name: 'CityList',
title: 'CityList',
uidt: UITypes.Links,
uidt: UITypes.LinkToAnotherRecord,
parentId: countryTable.id,
childId: cityTable.id,
type: 'hm',
@ -683,7 +684,16 @@ test.describe.serial('Webhook', () => {
CountryCode: '1',
CityCodeRollup: '2',
CityCodeFormula: 100,
CityList: '2',
CityList: [
{
Id: 1,
City: 'Mumbai',
},
{
Id: 2,
City: 'Pune',
},
],
CityCodeLookup: ['23', '33'],
},
],
@ -694,7 +704,16 @@ test.describe.serial('Webhook', () => {
CountryCode: '1',
CityCodeRollup: '2',
CityCodeFormula: 100,
CityList: '2',
CityList: [
{
Id: 1,
City: 'Mumbai',
},
{
Id: 2,
City: 'Pune',
},
],
CityCodeLookup: ['23', '33'],
},
],
@ -714,10 +733,6 @@ test.describe.serial('Webhook', () => {
expectedData.data.previous_rows[0].CityCodeLookup = [23, 33];
// @ts-ignore
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)) {
// @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 grid.selectRange({
start: { index: 0, columnHeader: 'Country' },
end: { index: 2, columnHeader: 'Cities' },
end: { index: 2, columnHeader: 'City List' },
});
expect(await grid.selectedCount()).toBe(9);
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 grid.selectRange({
start: { index: 0, columnHeader: 'Country' },
end: { index: 2, columnHeader: 'Cities' },
end: { index: 2, columnHeader: 'City List' },
});
expect(await grid.selectedCount()).toBe(9);
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.grid.toolbar.fields.toggleShowSystemFields();
await grid.selectRange({
start: { index: 2, columnHeader: 'Cities' },
start: { index: 2, columnHeader: 'City List' },
end: { index: 0, columnHeader: 'Country' },
});
expect(await grid.selectedCount()).toBe(12);
@ -88,7 +88,7 @@ test.describe('Verify cell selection', () => {
await dashboard.treeView.openTable({ title: 'Country' });
await grid.selectRange({
start: { index: 0, columnHeader: 'Country' },
end: { index: 2, columnHeader: 'Cities' },
end: { index: 2, columnHeader: 'City List' },
});
await page.keyboard.press('ArrowRight');
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.dbTableColumn.create(table_2.id, {
uidt: UITypes.Links,
title: 'Links',
column_name: 'Links',
uidt: UITypes.LinkToAnotherRecord,
title: 'LinkToAnotherRecord',
column_name: 'LinkToAnotherRecord',
parentId: table_1.id,
childId: table_2.id,
type: 'hm',

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

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

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

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

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

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

Loading…
Cancel
Save