Browse Source

Merge branch 'develop' into feat/ltar-improvements

merge develop
pull/5816/head
Raju Udava 1 year ago
parent
commit
f1ea40044e
  1. 6
      lerna.json
  2. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  3. 20
      packages/nc-gui/components/smartsheet/Form.vue
  4. 27
      packages/nc-gui/components/smartsheet/Grid.vue
  5. 5
      packages/nc-gui/components/smartsheet/header/Cell.vue
  6. 5
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  7. 17
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  8. 12
      packages/nc-gui/components/virtual-cell/HasMany.vue
  9. 12
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  10. 49
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  11. 4
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  12. 8
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  13. 4
      packages/nc-gui/composables/useMultiSelect/cellRange.ts
  14. 1
      packages/nc-gui/composables/useMultiSelect/index.ts
  15. 78
      packages/nc-gui/composables/useViewData.ts
  16. 28
      packages/nc-gui/package-lock.json
  17. 17
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  18. 14
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue
  19. 6
      packages/noco-docs/content/en/setup-and-usages/column-types.md
  20. 12
      packages/nocodb/src/models/Model.ts
  21. 10
      packages/nocodb/src/models/Project.ts
  22. 29
      packages/nocodb/src/models/View.ts
  23. 1
      packages/nocodb/src/modules/datas/helpers.ts
  24. 2
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts
  25. 65
      renovate.json
  26. 4
      tests/playwright/package-lock.json
  27. 7
      tests/playwright/pages/Dashboard/Form/index.ts
  28. 5
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts
  29. 4
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts
  30. 2
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  31. 1
      tests/playwright/tests/db/columnRelationalExtendedTests.spec.ts
  32. 2
      tests/playwright/tests/db/viewForm.spec.ts

6
lerna.json

@ -1,6 +1,10 @@
{
"packages": [
"packages/*"
"packages/nc-cli",
"packages/nc-gui",
"packages/nc-plugin",
"packages/nocodb",
"packages/nocodb-sdk"
],
"version": "independent"
}

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

@ -193,7 +193,7 @@ onUnmounted(() => {
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ 'text-blue-600': isPrimary(column) && !props.virtual && !isForm },
{ 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField },
{ 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm && !isAttachment(column) },
{ 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm && !isAttachment(column) && !props.virtual },
]"
@keydown.enter.exact="navigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"

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

@ -532,10 +532,10 @@ watch(view, (nextView) => {
<!-- Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item v-if="isEditable">
<a-input
<a-textarea
v-model:value="formViewData.heading"
class="w-full !font-bold !text-4xl !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{ borderRightWidth: '0px !important' }"
:style="{ borderRightWidth: '0px !important', 'height': '54px', 'min-height': '54px', resize: 'vertical' }"
size="large"
hide-details
placeholder="Form Title"
@ -551,10 +551,10 @@ watch(view, (nextView) => {
<!-- Sub Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item>
<a-input
<a-textarea
v-model:value="formViewData.subheading"
class="w-full !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{ borderRightWidth: '0px !important' }"
:style="{ borderRightWidth: '0px !important', height: '40px', 'min-height': '40px', resize: 'vertical' }"
size="large"
hide-details
:placeholder="$t('msg.info.formDesc')"
@ -697,7 +697,7 @@ watch(view, (nextView) => {
<a-form-item
v-if="isVirtualCol(element)"
:name="element.title"
class="!mb-0"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
@ -719,7 +719,7 @@ watch(view, (nextView) => {
<a-form-item
v-else
:name="element.title"
class="!mb-0"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
@ -743,7 +743,7 @@ watch(view, (nextView) => {
</LazySmartsheetDivDataCell>
</a-form-item>
<div class="text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div>
<div class="nc-form-help-text text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div>
</div>
</template>
@ -861,6 +861,12 @@ watch(view, (nextView) => {
}
}
.nc-form-help-text, .nc-input-required-error {
max-width: 100%;
word-break: break-all;
white-space: pre-line;
}
:deep(.nc-cell-attachment) {
@apply p-0;

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

@ -124,6 +124,7 @@ const {
removeRowIfNew,
navigateToSiblingRow,
getExpandedRowIndex,
deleteRangeOfRows,
} = useViewData(meta, view, xWhere)
const { getMeta } = useMetas()
@ -199,6 +200,7 @@ const {
isCellActive,
tbodyEl,
resetSelectedRange,
selectedRange,
} = useMultiSelect(
meta,
fields,
@ -794,6 +796,14 @@ const confirmDeleteRow = (row: number) => {
},
})
}
const deleteSelectedRangeOfRows = () => {
deleteRangeOfRows(selectedRange).then(() => {
clearSelectedRange()
activeCell.row = null
activeCell.col = null
})
}
</script>
<template>
@ -1015,14 +1025,24 @@ const confirmDeleteRow = (row: number) => {
<template v-if="!isLocked && hasEditPermission" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="confirmDeleteRow(contextMenuTarget.row)">
<a-menu-item
v-if="contextMenuTarget && (selectedRange.isSingleCell() || selectedRange.isSingleRow())"
@click="confirmDeleteRow(contextMenuTarget.row)"
>
<div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Row -->
{{ $t('activity.deleteRow') }}
</div>
</a-menu-item>
<a-menu-item @click="deleteSelectedRows">
<a-menu-item v-else-if="contextMenuTarget" @click="deleteSelectedRangeOfRows">
<div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Rows -->
Delete Rows
</div>
</a-menu-item>
<a-menu-item v-if="data.some((r) => r.rowMeta.selected)" @click="deleteSelectedRows">
<div v-e="['a:row:delete-bulk']" class="nc-project-menu-item">
<!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }}
@ -1033,6 +1053,7 @@ const confirmDeleteRow = (row: number) => {
<a-menu-item
v-if="
contextMenuTarget &&
selectedRange.isSingleCell() &&
(fields[contextMenuTarget.col].uidt === UITypes.LinkToAnotherRecord ||
!isVirtualCol(fields[contextMenuTarget.col]))
"
@ -1041,7 +1062,7 @@ const confirmDeleteRow = (row: number) => {
<div v-e="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)">
<a-menu-item v-if="contextMenuTarget && selectedRange.isSingleCell()" @click="addEmptyRow(contextMenuTarget.row + 1)">
<div v-e="['a:row:insert']" class="nc-project-menu-item">
<!-- Insert New Row -->
{{ $t('activity.insertRow') }}

5
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -53,7 +53,7 @@ const openHeaderMenu = () => {
v-if="column"
class="name"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') && !hideMenu }"
style="white-space: nowrap"
style="white-space: pre-line"
:title="column.title"
@dblclick="openHeaderMenu"
>{{ column.title }}</span
@ -95,7 +95,6 @@ const openHeaderMenu = () => {
<style scoped>
.name {
max-width: calc(100% - 40px);
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
</style>

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

@ -121,7 +121,7 @@ const closeAddColumnDropdown = () => {
<template #title>
{{ tooltipMsg }}
</template>
<span class="name" style="white-space: nowrap" :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>
@ -164,7 +164,6 @@ const closeAddColumnDropdown = () => {
<style scoped>
.name {
max-width: calc(100% - 40px);
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
</style>

17
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -45,7 +45,7 @@ const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -81,13 +81,24 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
break
}
})
const belongsToColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
</script>
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips flex items-center flex-1">
<template v-if="value && relatedTableDisplayValueProp">
<VirtualCellComponentsItemChip :item="value" :value="value[relatedTableDisplayValueProp]" @unlink="unlinkRef(value)" />
<VirtualCellComponentsItemChip
:item="value"
:value="value[relatedTableDisplayValueProp]"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
/>
</template>
</div>
@ -102,7 +113,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="belongsToColumn" @attach-record="listItemsDlg = true" />
</div>
</template>

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

@ -43,7 +43,7 @@ const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -81,6 +81,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
}
}
const hasManyColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
@ -106,6 +111,8 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
:key="i"
:item="cell.item"
:value="cell.value"
:column="hasManyColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)"
/>
@ -131,11 +138,12 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</div>
</template>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" />
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="hasManyColumn" />
<LazyVirtualCellComponentsListChildItems
v-model="childListDlg"
:cell-value="localCellValue"
:column="hasManyColumn"
@attach-record="onAttachRecord"
/>
</div>

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

@ -45,7 +45,7 @@ const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
@ -96,6 +96,11 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
break
}
})
const m2mColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
</script>
<template>
@ -108,6 +113,8 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
:key="i"
:item="cell.item"
:value="cell.value"
:column="m2mColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)"
/>
@ -133,11 +140,12 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</div>
</template>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" />
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="m2mColumn" />
<LazyVirtualCellComponentsListChildItems
v-model="childListDlg"
:cell-value="localCellValue"
:column="m2mColumn"
@attach-record="onAttachRecord"
/>
</div>

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

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import {
ActiveCellInj,
IsFormInj,
@ -6,6 +7,7 @@ import {
ReadonlyInj,
iconMap,
inject,
isAttachment,
ref,
renderValue,
useExpandedFormDetached,
@ -15,9 +17,11 @@ import {
interface Props {
value?: string | number | boolean
item?: any
column: any
showUnlinkButton: boolean
}
const { value, item } = defineProps<Props>()
const { value, item, column, showUnlinkButton } = defineProps<Props>()
const emit = defineEmits(['unlink'])
@ -56,13 +60,46 @@ export default {
<template>
<div
class="chip group py-1 px-2 mr-1 my-1 flex items-center bg-blue-100/60 hover:bg-blue-100/40 rounded-[2px]"
:class="{ active }"
class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row"
:class="{ active, 'border-1 py-1 px-2': isAttachment(column) }"
@click="openExpandedForm"
>
<span class="name">{{ renderValue(value) }}</span>
<div v-show="active || isForm" v-if="!readOnly && !isLocked && isUIAllowed('xcDatatableEditable')" class="flex items-center">
<span class="name">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(column)">
<template v-if="column.uidt === UITypes.LinkToAnotherRecord">
<LazySmartsheetVirtualCell :edit-enabled="false" :model-value="value" :column="column" :read-only="true" />
</template>
<LazySmartsheetVirtualCell v-else :edit-enabled="false" :read-only="true" :model-value="value" :column="column" />
</div>
<!-- Render normal cell -->
<template v-else>
<div v-if="isAttachment(column) && value && !Array.isArray(value) && typeof value === 'object'">
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :read-only="true" />
</div>
<!-- For attachment cell avoid adding chip style -->
<template v-else>
<div
class="min-w-max"
:class="{
'px-1 rounded-full flex-1': !isAttachment(column),
'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" />
</div>
</template>
</template>
</span>
<div
v-show="active || isForm"
v-if="showUnlinkButton && !readOnly && !isLocked && isUIAllowed('xcDatatableEditable')"
class="flex items-center"
>
<component
:is="iconMap.closeThick"
class="nc-icon unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500"

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

@ -19,7 +19,7 @@ import {
useVModel,
} from '#imports'
const props = defineProps<{ modelValue?: boolean; cellValue: any }>()
const props = defineProps<{ modelValue?: boolean; cellValue: any; column: any }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
@ -148,7 +148,7 @@ const onClick = (row: Row) => {
>
<div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0">
{{ renderValue(row[relatedTableDisplayValueProp]) }}
<VirtualCellComponentsItemChip :value="row[relatedTableDisplayValueProp]" :column="props.column" />
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>

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

@ -19,7 +19,7 @@ import {
useVModel,
} from '#imports'
const props = defineProps<{ modelValue: boolean }>()
const props = defineProps<{ modelValue: boolean; column: any }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
@ -229,7 +229,11 @@ watch(vModel, (nextVal) => {
:class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)"
>
{{ renderValue(refRow[relatedTableDisplayValueProp]) }}
<VirtualCellComponentsItemChip
:value="refRow[relatedTableDisplayValueProp]"
:column="props.column"
:show-unlink-button="false"
/>
<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1">
({{ $t('labels.primaryKey') }} : {{ getRelatedTableRowId(refRow) }})
</span>

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

@ -20,6 +20,10 @@ export class CellRange {
return !this.isEmpty() && this._start?.col === this._end?.col && this._start?.row === this._end?.row
}
isSingleRow() {
return !this.isEmpty() && this._start?.row === this._end?.row
}
get start(): Cell {
return {
row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN),

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

@ -445,5 +445,6 @@ export function useMultiSelect(
handleCellClick,
tbodyEl,
resetSelectedRange,
selectedRange,
}
}

78
packages/nc-gui/composables/useViewData.ts

@ -1,6 +1,7 @@
import { UITypes, ViewTypes } from 'nocodb-sdk'
import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { CellRange } from '#imports'
import {
IsPublicInj,
NOCO,
@ -615,6 +616,82 @@ export function useViewData(
await syncPagination()
}
async function deleteRangeOfRows(cellRange: CellRange) {
if (!cellRange._start || !cellRange._end) return
const start = Math.max(cellRange._start.row, cellRange._end.row)
const end = Math.min(cellRange._start.row, cellRange._end.row)
// plus one because we want to include the end row
let row = start + 1
const removedRowsData: { id?: string; row: Row; rowIndex: number }[] = []
while (row--) {
try {
const { row: rowObj, rowMeta } = formattedData.value[row] as Record<string, any>
if (!rowMeta.new) {
const id = meta?.value?.columns
?.filter((c) => c.pk)
.map((c) => rowObj[c.title as string])
.join('___')
const successfulDeletion = await deleteRowById(id as string)
if (!successfulDeletion) {
continue
}
removedRowsData.push({ id, row: clone(formattedData.value[row]), rowIndex: row })
}
formattedData.value.splice(row, 1)
} catch (e: any) {
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
if (row === end) break
}
addUndo({
redo: {
fn: async function redo(this: UndoRedoAction, removedRowsData: { id?: string; row: Row; rowIndex: number }[]) {
for (const { id, row } of removedRowsData) {
await deleteRowById(id as string)
const pk: Record<string, string> = rowPkData(row.row, meta?.value?.columns as ColumnType[])
const rowIndex = findIndexByPk(pk)
if (rowIndex !== -1) formattedData.value.splice(rowIndex, 1)
paginationData.value.totalRows = paginationData.value.totalRows! - 1
}
await syncPagination()
},
args: [removedRowsData],
},
undo: {
fn: async function undo(
this: UndoRedoAction,
removedRowsData: { id?: string; row: Row; rowIndex: number }[],
pg: { page: number; pageSize: number },
) {
for (const { row, rowIndex } of removedRowsData.slice().reverse()) {
const pkData = rowPkData(row.row, meta.value?.columns as ColumnType[])
row.row = { ...pkData, ...row.row }
await insertRow(row, {}, {}, true)
if (rowIndex !== -1 && pg.pageSize === paginationData.value.pageSize) {
if (pg.page === paginationData.value.page) {
formattedData.value.splice(rowIndex, 0, row)
} else {
await changePage(pg.page)
}
} else {
await loadData()
}
}
},
args: [removedRowsData, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }],
},
scope: defineViewScope({ view: viewMeta.value }),
})
await syncCount()
await syncPagination()
}
async function loadFormView() {
if (!viewMeta?.value?.id) return
try {
@ -728,6 +805,7 @@ export function useViewData(
deleteRow,
deleteRowById,
deleteSelectedRows,
deleteRangeOfRows,
updateOrSaveRow,
selectedAllRecords,
syncCount,

28
packages/nc-gui/package-lock.json generated

@ -16612,9 +16612,9 @@
}
},
"node_modules/vite": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz",
"integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==",
"version": "2.9.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz",
"integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==",
"dev": true,
"dependencies": {
"esbuild": "^0.14.27",
@ -17033,9 +17033,9 @@
"dev": true
},
"node_modules/vite-node/node_modules/vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
"integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
@ -17053,6 +17053,7 @@
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"sass": "*",
"stylus": "*",
@ -17060,6 +17061,9 @@
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
@ -30556,9 +30560,9 @@
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
},
"vite": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz",
"integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==",
"version": "2.9.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz",
"integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==",
"dev": true,
"requires": {
"esbuild": "^0.14.27",
@ -30768,9 +30772,9 @@
"dev": true
},
"vite": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
"integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
"dev": true,
"requires": {
"esbuild": "^0.15.9",

17
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -75,9 +75,18 @@ const onDecode = async (scannedCodeValue: string) => {
class="color-transition relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl)"
>
<template v-if="sharedFormView">
<h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView.heading }}</h1>
<h2 v-if="sharedFormView.subheading" class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6">
<h1
class="prose-2xl font-bold self-center my-4"
style="word-break: break-all"
>
{{ sharedFormView.heading }}
</h1>
<h2
v-if="sharedFormView.subheading"
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6"
style="word-break: break-all"
>
{{ sharedFormView.subheading }}
</h2>
@ -180,7 +189,7 @@ const onDecode = async (scannedCodeValue: string) => {
</a-button>
</LazySmartsheetDivDataCell>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1" style="word-break: break-all">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>

14
packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

@ -244,13 +244,17 @@ onMounted(() => {
class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end"
>
<div class="px-4 md:px-0 flex flex-col justify-end">
<h1 class="prose-2xl font-bold self-center my-4" data-testid="nc-survey-form__heading">
<h1
class="prose-2xl font-bold self-center my-4"
data-testid="nc-survey-form__heading"
style="word-break: break-all">
{{ sharedFormView.heading }}
</h1>
<h2
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''"
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6"
style="word-break: break-all"
data-testid="nc-survey-form__sub-heading"
>
{{ sharedFormView?.subheading }}
@ -287,7 +291,7 @@ onMounted(() => {
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
v-model="formState[field.title]"
class="mt-0 nc-input"
class="mt-0 nc-input h-auto"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
@ -296,7 +300,7 @@ onMounted(() => {
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input"
class="nc-input h-auto"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="editEnabled[index]"
@ -305,11 +309,10 @@ onMounted(() => {
@update:edit-enabled="editEnabled[index] = $event"
/>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1" style="word-break: break-all">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>
<div
class="block text-[14px]"
:class="field.uidt === UITypes.Checkbox ? 'text-center' : ''"
@ -350,6 +353,7 @@ onMounted(() => {
:mouse-enter-delay="0.25"
:mouse-leave-delay="0"
>
<!-- Ok button for question -->
<button
class="bg-opacity-100 scaling-btn flex items-center gap-1"
data-testid="nc-survey-form__btn-next"

6
packages/noco-docs/content/en/setup-and-usages/column-types.md

@ -54,7 +54,7 @@ menuTitle: 'Column Types'
### LinkToAnotherRecord
For more about Link To Another Record, please visit [here](./link-to-another-record).
For more about Link To Another Record, please visit <NuxtLink to="/setup-and-usages/link-to-another-record" target="_blank">here</NuxtLink>.
<!-- ### ForeignKey
#### Available Database Types
@ -261,7 +261,7 @@ For more about Link To Another Record, please visit [here](./link-to-another-rec
### Formula
For more about formula, please visit [here](./formulas).
For more about Formulas, please visit <NuxtLink to="/setup-and-usages/formulas" target="_blank">here</NuxtLink>.
### QR-Code
@ -289,7 +289,7 @@ Since it's a virtual column, the cell content (Barcode) cannot be changed direct
### Rollup
For more about rollup, please visit [here](./rollup).
For more about Rollup, please visit <NuxtLink to="/setup-and-usages/rollup" target="_blank">here</NuxtLink>.
### DateTime

12
packages/nocodb/src/models/Model.ts

@ -570,13 +570,25 @@ export default class Model implements TableType {
// get existing cache
const key = `${CacheScope.MODEL}:${tableId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let oldModel = { ...o };
// update alias
if (o) {
o.title = title;
o.table_name = table_name;
// set cache
await NocoCache.set(key, o);
} else {
oldModel = await this.get(tableId);
}
// delete alias cache
await NocoCache.del(
`${CacheScope.MODEL}:${oldModel.project_id}:${oldModel.base_id}:${oldModel.title}`,
);
await NocoCache.del(
`${CacheScope.MODEL}:${oldModel.project_id}:${oldModel.title}`,
);
// set meta
return await ncMeta.metaUpdate(
null,

10
packages/nocodb/src/models/Project.ts

@ -8,6 +8,7 @@ import {
import { extractProps } from '../helpers/extractProps';
import NocoCache from '../cache/NocoCache';
import Base from './/Base';
import { ProjectUser } from './index';
import type { BoolType, MetaType, ProjectType } from 'nocodb-sdk';
import type { DB_TYPES } from './/Base';
@ -276,6 +277,15 @@ export default class Project implements ProjectType {
// Todo: Remove the project entry from the connection pool in NcConnectionMgrv2
static async delete(projectId, ncMeta = Noco.ncMeta): Promise<any> {
const users = await ProjectUser.getUsersList({
project_id: projectId,
offset: 0,
limit: 1000,
});
for (const user of users) {
await ProjectUser.delete(projectId, user.id);
}
const bases = await Base.list({ projectId });
for (const base of bases) {
await base.delete(ncMeta);

29
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 {
@ -165,13 +166,19 @@ export default class View implements ViewType {
],
},
);
view.meta = parseMetaProp(view);
// todo: cache - titleOrId can be viewId so we need a different scope here
await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`,
view.id,
);
await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:${view.id}`, view);
if (view) {
await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${view.id}`,
view,
);
view.meta = parseMetaProp(view);
// todo: cache - titleOrId can be viewId so we need a different scope here
await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`,
view.id,
);
}
return view && new View(view);
}
return viewId && this.get(viewId?.id || viewId);
@ -952,6 +959,7 @@ export default class View implements ViewType {
// get existing cache
const key = `${CacheScope.VIEW}:${viewId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let oldView = { ...o };
if (o) {
// update data
o = {
@ -963,8 +971,15 @@ export default class View implements ViewType {
}
// set cache
await NocoCache.set(key, o);
} else {
oldView = await this.get(viewId);
}
// reset alias cache
await NocoCache.del(
`${CacheScope.VIEW}:${oldView.fk_model_id}:${oldView.title}`,
);
// if meta data defined then stringify it
if ('meta' in updateObj) {
updateObj.meta = stringifyMetaProp(updateObj);

1
packages/nocodb/src/modules/datas/helpers.ts

@ -43,6 +43,7 @@ export async function getViewAndModelByAliasOrId(param: {
fk_model_id: model.id,
}));
if (!model) NcError.notFound('Table not found');
if (param.viewName && !view) NcError.notFound('View not found');
return { model, view };
}

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

@ -780,7 +780,7 @@ function viewRowTests() {
.send({
title: 'Test',
})
.expect(400);
.expect(404);
};
it('Create table row grid wrong grid id', async function () {

65
renovate.json

@ -0,0 +1,65 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":dependencyDashboard",
":onlyNpm",
":prConcurrentLimit20",
":autodetectPinVersions",
":label(renovate)",
":rebaseStalePrs",
":semanticPrefixFixDepsChoreOthers",
":separatePatchReleases",
"group:monorepos",
"group:recommended"
],
"baseBranches": [
"develop"
],
"vulnerabilityAlerts": {
"commitMessagePrefix": "chore(renovate): Security update"
},
"schedule": "at any time",
"rangeStrategy": "bump",
"packageRules": [
{
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["major"],
"reviewersFromCodeOwners": true,
"commitMessagePrefix": "chore(renovate):",
"groupName": "major"
},
{
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["minor"],
"commitMessagePrefix": "chore(renovate):",
"groupName": "minor"
},
{
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["patch"],
"commitMessagePrefix": "chore(renovate):",
"groupName": "patch"
},
{
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["pin", "digest"],
"commitMessagePrefix": "chore(renovate):",
"groupName": "pin"
}
],
"ignorePaths": [
"**/node_modules/**",
"**/nc-cli/**",
"**/nc-lib-gui/**",
"**/nc-plugin/**",
"**/nocodb-legacy/**",
"**/test/**",
"**/tests/**",
"**/workflows/**",
"**/charts/**"
],
"assignees": [
"wingkwong"
]
}

4
tests/playwright/package-lock.json generated

@ -39,7 +39,7 @@
}
},
"../../packages/nocodb-sdk": {
"version": "0.107.0-beta.1",
"version": "0.107.5",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -10702,4 +10702,4 @@
"dev": true
}
}
}
}

7
tests/playwright/pages/Dashboard/Form/index.ts

@ -124,8 +124,11 @@ export class FormPage extends BasePage {
async addField({ field, mode }: { mode: string; field: string }) {
if (mode === 'dragDrop') {
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`);
const dst = await this.get().locator(`.nc-form-drag-Country`);
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"] > div.ant-card-body`);
const dst = await this.get().locator(`[data-testid="nc-form-input-Country"]`);
await src.waitFor({ state: 'visible' });
await dst.waitFor({ state: 'visible' });
await src.dragTo(dst, { trial: true });
await src.dragTo(dst);
} else if (mode === 'clickField') {
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`);

5
tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts

@ -31,7 +31,10 @@ export class ChildList extends BasePage {
const childCards = await childList.count();
await expect(childCards).toEqual(cardCount);
for (let i = 0; i < cardCount; i++) {
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]);
await childList.nth(i).locator('.name').waitFor({ state: 'visible' });
await childList.nth(i).locator('.name').scrollIntoViewIfNeeded();
await this.rootPage.waitForTimeout(100);
await expect(await childList.nth(i).locator('.name').textContent()).toContain(cardTitle[i]);
// icon: unlink
// icon: delete
await expect(

4
tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts

@ -29,7 +29,9 @@ export class LinkRecord extends BasePage {
const childCards = await childList.count();
await expect(childCards).toEqual(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) {
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]);
await childList.nth(i).locator('.name').scrollIntoViewIfNeeded();
await childList.nth(i).locator('.name').waitFor({ state: 'visible' });
await expect(await childList.nth(i).locator('.name').textContent()).toContain(cardTitle[i]);
}
}
}

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

@ -286,6 +286,8 @@ export class CellPageObject extends BasePage {
// 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 expect(await chips.nth(i).locator('.name')).toHaveText(value[i]);
}

1
tests/playwright/tests/db/columnRelationalExtendedTests.spec.ts

@ -1,7 +1,6 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard';
import setup from '../../setup';
import { isPg } from '../../setup/db';
test.describe('Relational Columns', () => {
let dashboard: DashboardPage;

2
tests/playwright/tests/db/viewForm.spec.ts

@ -55,7 +55,7 @@ test.describe('Form view', () => {
// add & verify (drag-drop)
await form.addField({ field: 'City List', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'City List', 'Country'],
fields: ['LastUpdate', 'Country', 'City List'],
});
// remove & verify (hide field button)

Loading…
Cancel
Save