Browse Source

feat: virtual scroll for grid (#8356)

* feat: virtual scroll for grid

* feat: improve virtual scroll

* fix: remove unused expose & ref

* feat: move row ltar helpers to parent level

* fix: use shared composable for useMetas

* fix: column add issue

* fix: reload issue

* feat: move cell state to computed

* chore: lint

* fix: null check for sticky field

* fix: PR requested changes

* fix: shared views

* fix: provide row store calls

* test: avoid all rows selector

* fix: group by

* fix: include isVirtualCol in cellMeta

* fix: split colMeta and cellMeta

* chore: lint

* test: edit column flakiness

* test: renderColumn for dashboard grid

* test: user column test flakiness
fix/src-filter
Mert E 7 months ago committed by GitHub
parent
commit
fed1c7ba5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/nc-gui/components/shared-view/Calendar.vue
  2. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  3. 2
      packages/nc-gui/components/shared-view/Grid.vue
  4. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  5. 2
      packages/nc-gui/components/shared-view/Map.vue
  6. 121
      packages/nc-gui/components/smartsheet/Cell.vue
  7. 1
      packages/nc-gui/components/smartsheet/Form.vue
  8. 17
      packages/nc-gui/components/smartsheet/Row.vue
  9. 5
      packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue
  10. 4
      packages/nc-gui/components/smartsheet/TableDataCell.vue
  11. 61
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  12. 4
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  13. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  14. 1
      packages/nc-gui/components/smartsheet/grid/GroupByTable.vue
  15. 909
      packages/nc-gui/components/smartsheet/grid/Table.vue
  16. 5
      packages/nc-gui/components/tabs/Smartsheet.vue
  17. 2
      packages/nc-gui/composables/useExpandedFormStore.ts
  18. 4
      packages/nc-gui/composables/useMetas.ts
  19. 50
      packages/nc-gui/composables/useMultiSelect/index.ts
  20. 4
      packages/nc-gui/composables/useSharedFormViewStore.ts
  21. 249
      packages/nc-gui/composables/useSmartsheetLtarHelpers.ts
  22. 266
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  23. 1
      packages/nc-gui/lib/types.ts
  24. 3
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  25. 44
      tests/playwright/pages/Dashboard/Grid/index.ts
  26. 2
      tests/playwright/pages/Dashboard/common/Cell/UserOptionCell.ts
  27. 1
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  28. 1
      tests/playwright/pages/Dashboard/common/Topbar/index.ts
  29. 6
      tests/playwright/tests/db/columns/columnUserSelect.spec.ts
  30. 3
      tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

2
packages/nc-gui/components/shared-view/Calendar.vue

@ -28,6 +28,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView) useProvideKanbanViewStore(meta, sharedView)

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -32,6 +32,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView) useProvideKanbanViewStore(meta, sharedView)

2
packages/nc-gui/components/shared-view/Grid.vue

@ -43,6 +43,8 @@ provide(IsLockedInj, isLocked)
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideViewGroupBy(sharedView, meta, xWhere, true) useProvideViewGroupBy(sharedView, meta, xWhere, true)
useProvideSmartsheetLtarHelpers(meta)
if (signedIn.value) { if (signedIn.value) {
try { try {
await loadProject() await loadProject()

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -27,6 +27,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView, true) useProvideKanbanViewStore(meta, sharedView, true)

2
packages/nc-gui/components/shared-view/Map.vue

@ -27,6 +27,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideMapViewStore(meta, sharedView, true) useProvideMapViewStore(meta, sharedView, true)

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

@ -154,43 +154,10 @@ const onContextmenu = (e: MouseEvent) => {
e.stopPropagation() e.stopPropagation()
} }
} }
// Todo: move intersection logic to a separate component or a vue directive
const intersected = ref(false)
const intersectionObserver = ref<IntersectionObserver>()
const elementToObserve = ref<Element>()
// load the cell only when it is in the viewport
function initIntersectionObserver() {
intersectionObserver.value = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// if the cell is in the viewport, load the cell and disconnect the observer
if (entry.isIntersecting) {
intersected.value = true
intersectionObserver.value?.disconnect()
intersectionObserver.value = undefined
}
})
})
}
// observe the cell when it is mounted
onMounted(() => {
initIntersectionObserver()
intersectionObserver.value?.observe(elementToObserve.value!)
})
// disconnect the observer when the cell is unmounted
onUnmounted(() => {
intersectionObserver.value?.disconnect()
})
</script> </script>
<template> <template>
<div <div
ref="elementToObserve"
:class="[ :class="[
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`, `nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ {
@ -214,51 +181,49 @@ onUnmounted(() => {
@keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"
> >
<template v-if="column"> <template v-if="column">
<template v-if="intersected"> <LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" />
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" /> <LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" /> <LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" /> <LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" /> <LazyCellSingleSelect
<LazyCellSingleSelect v-else-if="isSingleSelect(column)"
v-else-if="isSingleSelect(column)" v-model="vModel"
v-model="vModel" :disable-option-creation="!!isEditColumnMenu"
:disable-option-creation="!!isEditColumnMenu" :row-index="props.rowIndex"
:row-index="props.rowIndex" />
/> <LazyCellMultiSelect
<LazyCellMultiSelect v-else-if="isMultiSelect(column)"
v-else-if="isMultiSelect(column)" v-model="vModel"
v-model="vModel" :disable-option-creation="!!isEditColumnMenu"
:disable-option-creation="!!isEditColumnMenu" :row-index="props.rowIndex"
:row-index="props.rowIndex" />
/> <LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" /> <LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" /> <LazyCellDateTimePicker
<LazyCellDateTimePicker v-else-if="isDateTime(column, abstractType)"
v-else-if="isDateTime(column, abstractType)" v-model="vModel"
v-model="vModel" :is-pk="isPrimaryKey(column)"
:is-pk="isPrimaryKey(column)" :is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste" />
/> <LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" /> <LazyCellRating v-else-if="isRating(column)" v-model="vModel" />
<LazyCellRating v-else-if="isRating(column)" v-model="vModel" /> <LazyCellDuration v-else-if="isDuration(column)" v-model="vModel" />
<LazyCellDuration v-else-if="isDuration(column)" v-model="vModel" /> <LazyCellEmail v-else-if="isEmail(column)" v-model="vModel" />
<LazyCellEmail v-else-if="isEmail(column)" v-model="vModel" /> <LazyCellUrl v-else-if="isURL(column)" v-model="vModel" />
<LazyCellUrl v-else-if="isURL(column)" v-model="vModel" /> <LazyCellPhoneNumber v-else-if="isPhoneNumber(column)" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="isPhoneNumber(column)" v-model="vModel" /> <LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" />
<LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" /> <LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" />
<LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" /> <LazyCellUser v-else-if="isUser(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellUser v-else-if="isUser(column)" v-model="vModel" :row-index="props.rowIndex" /> <LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" />
<LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" /> <LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" /> <LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" />
<LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" /> <LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" />
<LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" /> <LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" /> <LazyCellText v-else v-model="vModel" />
<LazyCellText v-else v-model="vModel" /> <div
<div v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)"
v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)" class="nc-locked-overlay"
class="nc-locked-overlay" />
/>
</template>
</template> </template>
</div> </div>
</template> </template>

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

@ -120,7 +120,6 @@ reloadEventHook.on(async () => {
const { fields, showAll, hideAll } = useViewColumnsOrThrow() const { fields, showAll, hideAll } = useViewColumnsOrThrow()
const { state, row } = useProvideSmartsheetRowStore( const { state, row } = useProvideSmartsheetRowStore(
meta,
ref({ ref({
row: formState.value, row: formState.value,
oldRow: {}, oldRow: {},

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

@ -1,6 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import type { Row } from '#imports' import type { Row } from '#imports'
import { import {
ReloadRowDataHookInj, ReloadRowDataHookInj,
@ -10,7 +8,6 @@ import {
provide, provide,
toRef, toRef,
useProvideSmartsheetRowStore, useProvideSmartsheetRowStore,
useSmartsheetStoreOrThrow,
} from '#imports' } from '#imports'
const props = defineProps<{ const props = defineProps<{
@ -19,12 +16,7 @@ const props = defineProps<{
const currentRow = toRef(props, 'row') const currentRow = toRef(props, 'row')
const { meta } = useSmartsheetStoreOrThrow() const { isNew, state } = useProvideSmartsheetRowStore(currentRow)
const { isNew, state, syncLTARRefs, clearLTARCell, addLTARRef, cleaMMCell } = useProvideSmartsheetRowStore(
meta as Ref<TableType>,
currentRow,
)
const reloadViewDataTrigger = inject(ReloadViewDataHookInj)! const reloadViewDataTrigger = inject(ReloadViewDataHookInj)!
@ -39,13 +31,6 @@ reloadHook.on((params) => {
}) })
provide(ReloadRowDataHookInj, reloadHook) provide(ReloadRowDataHookInj, reloadHook)
defineExpose({
syncLTARRefs,
clearLTARCell,
addLTARRef,
cleaMMCell,
})
</script> </script>
<template> <template>

5
packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue

@ -1,7 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { isVirtualCol } from 'nocodb-sdk' import { isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -52,7 +49,7 @@ provide(ReloadRowDataHookInj, reloadViewDataHook!)
const currentRow = toRef(props, 'row') const currentRow = toRef(props, 'row')
useProvideSmartsheetRowStore(meta as Ref<TableType>, currentRow) useProvideSmartsheetRowStore(currentRow)
</script> </script>
<template> <template>

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

@ -1,15 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { CellClickHookInj, CurrentCellInj, createEventHook, ref } from '#imports' import { CellClickHookInj, CurrentCellInj, createEventHook, ref } from '#imports'
const el = ref<HTMLTableDataCellElement>() const el = ref<HTMLElement>()
const cellClickHook = createEventHook() const cellClickHook = createEventHook()
provide(CellClickHookInj, cellClickHook) provide(CellClickHookInj, cellClickHook)
provide(CurrentCellInj, el) provide(CurrentCellInj, el)
defineExpose({ el })
</script> </script>
<template> <template>

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

@ -62,43 +62,10 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
if (!isForm.value) e.stopImmediatePropagation() if (!isForm.value) e.stopImmediatePropagation()
} }
// Todo: move intersection logic to a separate component or a vue directive
const intersected = ref(false)
const intersectionObserver = ref<IntersectionObserver>()
const elementToObserve = ref<Element>()
// load the cell only when it is in the viewport
function initIntersectionObserver() {
intersectionObserver.value = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// if the cell is in the viewport, load the cell and disconnect the observer
if (entry.isIntersecting) {
intersected.value = true
intersectionObserver.value?.disconnect()
intersectionObserver.value = undefined
}
})
})
}
// observe the cell when it is mounted
onMounted(() => {
initIntersectionObserver()
intersectionObserver.value?.observe(elementToObserve.value!)
})
// disconnect the observer when the cell is unmounted
onUnmounted(() => {
intersectionObserver.value?.disconnect()
})
</script> </script>
<template> <template>
<div <div
ref="elementToObserve"
class="nc-virtual-cell w-full flex items-center" class="nc-virtual-cell w-full flex items-center"
:class="{ :class="{
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm, 'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
@ -107,21 +74,19 @@ onUnmounted(() => {
@keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
> >
<template v-if="intersected"> <LazyVirtualCellLinks v-if="isLink(column)" />
<LazyVirtualCellLinks v-if="isLink(column)" /> <LazyVirtualCellHasMany v-else-if="isHm(column)" />
<LazyVirtualCellHasMany v-else-if="isHm(column)" /> <LazyVirtualCellManyToMany v-else-if="isMm(column)" />
<LazyVirtualCellManyToMany v-else-if="isMm(column)" /> <LazyVirtualCellBelongsTo v-else-if="isBt(column)" />
<LazyVirtualCellBelongsTo v-else-if="isBt(column)" /> <LazyVirtualCellOneToOne v-else-if="isOo(column)" />
<LazyVirtualCellOneToOne v-else-if="isOo(column)" /> <LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellRollup v-else-if="isRollup(column)" /> <LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" /> <LazyVirtualCellQrCode v-else-if="isQrCode(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" /> <LazyVirtualCellBarcode v-else-if="isBarcode(column)" />
<LazyVirtualCellBarcode v-else-if="isBarcode(column)" /> <LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" /> <LazyVirtualCellLookup v-else-if="isLookup(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" /> <LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" />
<LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" /> <LazyCellReadOnlyUser v-else-if="isCreatedOrLastModifiedByCol(column)" :model-value="modelValue" />
<LazyCellReadOnlyUser v-else-if="isCreatedOrLastModifiedByCol(column)" :model-value="modelValue" />
</template>
</div> </div>
</template> </template>

4
packages/nc-gui/components/smartsheet/column/DefaultValue.vue

@ -7,8 +7,6 @@ const props = defineProps<{
}>() }>()
const emits = defineEmits(['update:value']) const emits = defineEmits(['update:value'])
const meta = inject(MetaInj, ref())
provide(EditColumnInj, ref(true)) provide(EditColumnInj, ref(true))
const vModel = useVModel(props, 'value', emits) const vModel = useVModel(props, 'value', emits)
@ -20,7 +18,7 @@ const rowRef = ref({
}, },
}) })
useProvideSmartsheetRowStore(meta, rowRef) useProvideSmartsheetRowStore(rowRef)
const cdfValue = ref<string | null>(null) const cdfValue = ref<string | null>(null)

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

@ -157,6 +157,8 @@ const duplicatingRowInProgress = ref(false)
useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta) useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta)
useProvideSmartsheetLtarHelpers(meta)
watch( watch(
state, state,
() => { () => {

1
packages/nc-gui/components/smartsheet/grid/GroupByTable.vue

@ -148,6 +148,7 @@ const pagination = computed(() => {
:hide-header="true" :hide-header="true"
:pagination="pagination" :pagination="pagination"
:disable-skeleton="true" :disable-skeleton="true"
:disable-virtual-y="true"
/> />
</template> </template>

909
packages/nc-gui/components/smartsheet/grid/Table.vue

File diff suppressed because it is too large Load Diff

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

@ -24,6 +24,7 @@ import {
useMetas, useMetas,
useProvideCalendarViewStore, useProvideCalendarViewStore,
useProvideKanbanViewStore, useProvideKanbanViewStore,
useProvideSmartsheetLtarHelpers,
useProvideSmartsheetStore, useProvideSmartsheetStore,
useRoles, useRoles,
useSqlEditor, useSqlEditor,
@ -58,8 +59,6 @@ const { isGallery, isGrid, isForm, isKanban, isLocked, isMap, isCalendar, xWhere
useSqlEditor() useSqlEditor()
const { isPanelExpanded } = useExtensions()
const reloadViewDataEventHook = createEventHook() const reloadViewDataEventHook = createEventHook()
const reloadViewMetaEventHook = createEventHook<void | boolean>() const reloadViewMetaEventHook = createEventHook<void | boolean>()
@ -88,6 +87,8 @@ provide(
useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger()) useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger())
useProvideViewGroupBy(activeView, meta, xWhere) useProvideViewGroupBy(activeView, meta, xWhere)
useProvideSmartsheetLtarHelpers(meta)
const grid = ref() const grid = ref()
const onDrop = async (event: DragEvent) => { const onDrop = async (event: DragEvent) => {

2
packages/nc-gui/composables/useExpandedFormStore.ts

@ -56,7 +56,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
: ({ row: {}, oldRow: {}, rowMeta: {} } as Row), : ({ row: {}, oldRow: {}, rowMeta: {} } as Row),
) )
const rowStore = useProvideSmartsheetRowStore(meta, row) const rowStore = useProvideSmartsheetRowStore(row)
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())

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

@ -3,7 +3,7 @@ import type { WatchStopHandle } from 'vue'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, storeToRefs, useBase, useNuxtApp, useState, watch } from '#imports' import { extractSdkResponseErrorMsg, storeToRefs, useBase, useNuxtApp, useState, watch } from '#imports'
export function useMetas() { export const useMetas = createSharedComposable(() => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { tables: _tables } = storeToRefs(useBase()) const { tables: _tables } = storeToRefs(useBase())
@ -118,4 +118,4 @@ export function useMetas() {
} }
return { getMeta, clearAllMeta, metas, metasWithIdAsKey, removeMeta, setMeta } return { getMeta, clearAllMeta, metas, metasWithIdAsKey, removeMeta, setMeta }
} })

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

@ -335,25 +335,43 @@ export function useMultiSelect(
} }
} }
function isCellSelected(row: number, col: number) { const fillRangeMap = computed(() => {
if (activeCell.col === col && activeCell.row === row) { /*
return true `${rowIndex}-${colIndex}`: true | false
*/
const map: Record<string, boolean> = {}
if (fillRange._start === null || fillRange._end === null) {
return map
} }
return selectedRange.isCellInRange({ row, col }) for (let row = fillRange.start.row; row <= fillRange.end.row; row++) {
} for (let col = fillRange.start.col; col <= fillRange.end.col; col++) {
map[`${row}-${col}`] = true
}
}
function isCellInFillRange(row: number, col: number) { return map
if (fillRange._start === null || fillRange._end === null) { })
return false
const selectRangeMap = computed(() => {
/*
`${rowIndex}-${colIndex}`: true | false
*/
const map: Record<string, boolean> = {}
if (selectedRange._start === null || selectedRange._end === null) {
return map
} }
if (selectedRange.isCellInRange({ row, col })) { for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
return false for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
map[`${row}-${col}`] = true
}
} }
return fillRange.isCellInRange({ row, col }) return map
} })
const isPasteable = (row?: Row, col?: ColumnType, showInfo = false) => { const isPasteable = (row?: Row, col?: ColumnType, showInfo = false) => {
if (!row || !col) { if (!row || !col) {
@ -420,7 +438,7 @@ export function useMultiSelect(
// if there was a right click on selected range, don't restart the selection // if there was a right click on selected range, don't restart the selection
if ( if (
(event?.button !== MAIN_MOUSE_PRESSED || (event?.button === MAIN_MOUSE_PRESSED && event.ctrlKey)) && (event?.button !== MAIN_MOUSE_PRESSED || (event?.button === MAIN_MOUSE_PRESSED && event.ctrlKey)) &&
isCellSelected(row, col) selectRangeMap.value[`${row}-${col}`]
) { ) {
return return
} }
@ -489,7 +507,7 @@ export function useMultiSelect(
fillDirection === 1 ? row <= fillRange._end.row : row >= fillRange._end.row; fillDirection === 1 ? row <= fillRange._end.row : row >= fillRange._end.row;
row += fillDirection row += fillDirection
) { ) {
if (isCellSelected(row, selectedRange.start.col)) { if (selectRangeMap.value[`${row}-${selectedRange.start.col}`]) {
continue continue
} }
@ -1298,14 +1316,14 @@ export function useMultiSelect(
handleMouseOver, handleMouseOver,
clearSelectedRange, clearSelectedRange,
copyValue, copyValue,
isCellSelected,
activeCell, activeCell,
handleCellClick, handleCellClick,
resetSelectedRange, resetSelectedRange,
selectedRange, selectedRange,
makeActive, makeActive,
isCellInFillRange,
isMouseDown, isMouseDown,
isFillMode, isFillMode,
selectRangeMap,
fillRangeMap,
} }
} }

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

@ -1,7 +1,6 @@
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { helpers, minLength, required, sameAs } from '@vuelidate/validators' import { helpers, minLength, required, sameAs } from '@vuelidate/validators'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type { import type {
BoolType, BoolType,
ColumnType, ColumnType,
@ -91,8 +90,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const preFilledDefaultValueformState = ref<Record<string, any>>({}) const preFilledDefaultValueformState = ref<Record<string, any>>({})
useProvideSmartsheetLtarHelpers(meta)
const { state: additionalState } = useProvideSmartsheetRowStore( const { state: additionalState } = useProvideSmartsheetRowStore(
meta as Ref<TableType>,
ref({ ref({
row: formState, row: formState,
rowMeta: { new: true }, rowMeta: { new: true },

249
packages/nc-gui/composables/useSmartsheetLtarHelpers.ts

@ -0,0 +1,249 @@
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
NOCO,
deepCompare,
extractPkFromRow,
extractSdkResponseErrorMsg,
isBt,
isHm,
isMm,
isOo,
message,
storeToRefs,
unref,
useBase,
useI18n,
useInjectionState,
useMetas,
useNuxtApp,
} from '#imports'
import type { Row } from '#imports'
const [useProvideSmartsheetLtarHelpers, useSmartsheetLtarHelpers] = useInjectionState(
(meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>) => {
const { $api } = useNuxtApp()
const { t } = useI18n()
const { base } = storeToRefs(useBase())
const { metas } = useMetas()
const getRowLtarHelpers = (row: Row) => {
if (!row.rowMeta) {
row.rowMeta = {}
}
if (!row.rowMeta.ltarState) {
row.rowMeta.ltarState = {}
}
return row.rowMeta.ltarState
}
// actions
const addLTARRef = async (row: Row, value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
if (!getRowLtarHelpers(row)[column.title!]) getRowLtarHelpers(row)[column.title!] = []
if (getRowLtarHelpers(row)[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) {
// This value is already in the list
return message.info(t('msg.info.valueAlreadyInList'))
}
if (Array.isArray(value)) {
getRowLtarHelpers(row)[column.title!]!.push(...value)
} else {
getRowLtarHelpers(row)[column.title!]!.push(value)
}
} else if (isBt(column) || isOo(column)) {
getRowLtarHelpers(row)[column.title!] = value
}
}
// actions
const removeLTARRef = async (row: Row, value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
getRowLtarHelpers(row)[column.title!]?.splice(getRowLtarHelpers(row)[column.title!]?.indexOf(value), 1)
} else if (isBt(column) || isOo(column)) {
getRowLtarHelpers(row)[column.title!] = null
}
}
const linkRecord = async (
rowId: string,
relatedRowId: string,
column: ColumnType,
type: RelationTypes,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
) => {
try {
await $api.dbTableRow.nestedAdd(
NOCO,
base.value.id as string,
metaValue?.id as string,
encodeURIComponent(rowId),
type,
column.id as string,
encodeURIComponent(relatedRowId),
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
/** sync LTAR relations kept in local state */
const syncLTARRefs = async (
row: Row,
rowData: Record<string, any>,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
) => {
const id = extractPkFromRow(rowData, metaValue?.columns as ColumnType[])
for (const column of metaValue?.columns ?? []) {
if (!isLinksOrLTAR(column)) continue
const colOptions = column.colOptions as LinkToAnotherRecordType
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string]
if (isHm(column) || isMm(column)) {
const relatedRows = (getRowLtarHelpers(row)?.[column.title!] ?? []) as Record<string, any>[]
for (const relatedRow of relatedRows) {
await linkRecord(
id,
extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
} else if ((isBt(column) || isOo(column)) && getRowLtarHelpers(row)?.[column.title!]) {
await linkRecord(
id,
extractPkFromRow(
getRowLtarHelpers(row)?.[column.title!] as Record<string, any>,
relatedTableMeta.columns as ColumnType[],
),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
// clear LTAR refs after sync
getRowLtarHelpers(row)[column.title!] = null
}
}
// clear LTAR cell
const clearLTARCell = async (row: Row, column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string]
if (row.rowMeta.new) {
getRowLtarHelpers(row)[column.title!] = null
} else {
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((<LinkToAnotherRecordType>column.colOptions)?.type)) {
if (!row.row[column.title!]) return
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
extractPkFromRow(row.row, meta.value?.columns as ColumnType[]),
(<LinkToAnotherRecordType>column.colOptions)?.type as any,
column.id as string,
extractPkFromRow(row.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
)
row.row[column.title!] = null
} else {
for (const link of (row.row[column.title!] as Record<string, any>[]) || []) {
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
encodeURIComponent(extractPkFromRow(row.row, meta.value?.columns as ColumnType[])),
(<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm',
column.id as string,
encodeURIComponent(extractPkFromRow(link, relatedTableMeta?.columns as ColumnType[])),
)
}
row.row[column.title!] = []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const loadRow = async (row: Row) => {
const record = await $api.dbTableRow.read(
NOCO,
base.value?.id as string,
meta.value?.title as string,
encodeURIComponent(extractPkFromRow(row.row, meta.value?.columns as ColumnType[])),
)
Object.assign(unref(row), {
row: record,
oldRow: { ...record },
rowMeta: {},
})
}
// clear MM cell
const cleaMMCell = async (row: Row, column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
if (row.rowMeta.new) {
getRowLtarHelpers(row)[column.title!] = null
} else {
if ((<LinkToAnotherRecordType>column.colOptions)?.type === RelationTypes.MANY_TO_MANY) {
if (!row.row[column.title!]) return
const result = await $api.dbDataTableRow.nestedListCopyPasteOrDeleteAll(
meta.value?.id as string,
column.id as string,
[
{
operation: 'deleteAll',
rowId: extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) as string,
columnId: column.id as string,
fk_related_model_id: (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
},
],
)
row.row[column.title!] = null
return Array.isArray(result.unlink) ? result.unlink : []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
return {
addLTARRef,
removeLTARRef,
syncLTARRefs,
loadRow,
clearLTARCell,
cleaMMCell,
}
},
'smartsheet-ltar-helpers',
)
export { useProvideSmartsheetLtarHelpers }
export function useSmartsheetLtarHelpersOrThrow() {
const smartsheetLtarHelpers = useSmartsheetLtarHelpers()
if (smartsheetLtarHelpers == null) throw new Error('Please call `useSmartsheetLtarHelpers` on the appropriate parent component')
return smartsheetLtarHelpers
}

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

@ -1,242 +1,42 @@
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import { import { computed, ref, unref, useInjectionState } from '#imports'
NOCO,
computed,
deepCompare,
extractPkFromRow,
extractSdkResponseErrorMsg,
isBt,
isHm,
isMm,
isOo,
message,
ref,
storeToRefs,
unref,
useBase,
useI18n,
useInjectionState,
useMetas,
useNuxtApp,
} from '#imports'
import type { Row } from '#imports' import type { Row } from '#imports'
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState( const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((row: MaybeRef<Row>) => {
(meta: Ref<TableType | undefined>, row: MaybeRef<Row>) => { const currentRow = ref(row)
const { $api } = useNuxtApp()
const { t } = useI18n() // state
const state = computed({
const { base } = storeToRefs(useBase()) get: () => currentRow.value?.rowMeta?.ltarState ?? {},
set: (value) => {
const { metas } = useMetas() if (currentRow.value) {
if (!currentRow.value.rowMeta) {
const currentRow = ref(row) currentRow.value.rowMeta = {}
// state
const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({})
// getters
const isNew = computed(() => unref(row).rowMeta.new ?? false)
// actions
const addLTARRef = async (value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
if (!state.value[column.title!]) state.value[column.title!] = []
if (state.value[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) {
// This value is already in the list
return message.info(t('msg.info.valueAlreadyInList'))
}
if (Array.isArray(value)) {
state.value[column.title!]!.push(...value)
} else {
state.value[column.title!]!.push(value)
} }
} else if (isBt(column) || isOo(column)) { currentRow.value.rowMeta.ltarState = value
state.value[column.title!] = value
} }
} },
})
// actions
const removeLTARRef = async (value: Record<string, any>, column: ColumnType) => { // getters
if (isHm(column) || isMm(column)) { const isNew = computed(() => unref(row).rowMeta?.new ?? false)
state.value[column.title!]?.splice(state.value[column.title!]?.indexOf(value), 1)
} else if (isBt(column) || isOo(column)) { const { addLTARRef, removeLTARRef, syncLTARRefs, loadRow, clearLTARCell, cleaMMCell } = useSmartsheetLtarHelpersOrThrow()
state.value[column.title!] = null
} return {
} row,
state,
const linkRecord = async ( isNew,
rowId: string, // todo: use better name
relatedRowId: string, addLTARRef: (...args: any) => addLTARRef(currentRow.value, ...args),
column: ColumnType, removeLTARRef: (...args: any) => removeLTARRef(currentRow.value, ...args),
type: RelationTypes, syncLTARRefs: (...args: any) => syncLTARRefs(currentRow.value, ...args),
{ metaValue = meta.value }: { metaValue?: TableType } = {}, loadRow: (...args: any) => loadRow(currentRow.value, ...args),
) => { currentRow,
try { clearLTARCell: (...args: any) => clearLTARCell(currentRow.value, ...args),
await $api.dbTableRow.nestedAdd( cleaMMCell: (...args: any) => cleaMMCell(currentRow.value, ...args),
NOCO, }
base.value.id as string, }, 'smartsheet-row-store')
metaValue?.id as string,
encodeURIComponent(rowId),
type,
column.id as string,
encodeURIComponent(relatedRowId),
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
/** sync LTAR relations kept in local state */
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
const colOptions = column.colOptions as LinkToAnotherRecordType
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string]
if (isHm(column) || isMm(column)) {
const relatedRows = (state.value?.[column.title!] ?? []) as Record<string, any>[]
for (const relatedRow of relatedRows) {
await linkRecord(
id,
extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
} else if ((isBt(column) || isOo(column)) && state.value?.[column.title!]) {
await linkRecord(
id,
extractPkFromRow(state.value?.[column.title!] as Record<string, any>, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
// clear LTAR refs after sync
state.value[column.title!] = null
}
}
// clear LTAR cell
const clearLTARCell = async (column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string]
if (isNew.value) {
state.value[column.title!] = null
} else if (currentRow.value) {
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((<LinkToAnotherRecordType>column.colOptions)?.type)) {
if (!currentRow.value.row[column.title!]) return
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
(<LinkToAnotherRecordType>column.colOptions)?.type as any,
column.id as string,
extractPkFromRow(currentRow.value.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
)
currentRow.value.row[column.title!] = null
} else {
for (const link of (currentRow.value.row[column.title!] as Record<string, any>[]) || []) {
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
encodeURIComponent(extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[])),
(<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm',
column.id as string,
encodeURIComponent(extractPkFromRow(link, relatedTableMeta?.columns as ColumnType[])),
)
}
currentRow.value.row[column.title!] = []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const loadRow = async () => {
const record = await $api.dbTableRow.read(
NOCO,
base.value?.id as string,
meta.value?.title as string,
encodeURIComponent(extractPkFromRow(unref(row)?.row, meta.value?.columns as ColumnType[])),
)
Object.assign(unref(row), {
row: record,
oldRow: { ...record },
rowMeta: {},
})
}
// clear MM cell
const cleaMMCell = async (column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
if (isNew.value) {
state.value[column.title!] = null
} else if (currentRow.value) {
if ((<LinkToAnotherRecordType>column.colOptions)?.type === RelationTypes.MANY_TO_MANY) {
if (!currentRow.value.row[column.title!]) return
console.log('currentRow.value.row, meta.value?.columns', currentRow.value.row, meta.value?.columns)
const result = await $api.dbDataTableRow.nestedListCopyPasteOrDeleteAll(
meta.value?.id as string,
column.id as string,
[
{
operation: 'deleteAll',
rowId: extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]) as string,
columnId: column.id as string,
fk_related_model_id: (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
},
],
)
currentRow.value.row[column.title!] = null
return Array.isArray(result.unlink) ? result.unlink : []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
return {
row,
state,
isNew,
// todo: use better name
addLTARRef,
removeLTARRef,
syncLTARRefs,
loadRow,
currentRow,
clearLTARCell,
cleaMMCell,
}
},
'smartsheet-row-store',
)
export { useProvideSmartsheetRowStore } export { useProvideSmartsheetRowStore }

1
packages/nc-gui/lib/types.ts

@ -64,6 +64,7 @@ interface Row {
commentCount?: number commentCount?: number
changed?: boolean changed?: boolean
saving?: boolean saving?: boolean
ltarState?: Record<string, Record<string, any> | Record<string, any>[] | null>
// use in datetime picker component // use in datetime picker component
isUpdatedFromCopyNPaste?: Record<string, boolean> isUpdatedFromCopyNPaste?: Record<string, boolean>
// Used in Calendar view // Used in Calendar view

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

@ -296,7 +296,8 @@ export class ColumnPageObject extends BasePage {
// when clicked on the dropdown cell header // when clicked on the dropdown cell header
await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').scrollIntoViewIfNeeded(); await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').scrollIntoViewIfNeeded();
await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').click(); await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').click();
await this.rootPage.locator('li[role="menuitem"]:has-text("Edit")').last().click(); await expect(await this.rootPage.locator('li[role="menuitem"]:has-text("Edit"):visible').last()).toBeVisible();
await this.rootPage.locator('li[role="menuitem"]:has-text("Edit"):visible').last().click();
await this.get().waitFor({ state: 'visible' }); await this.get().waitFor({ state: 'visible' });

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

@ -79,6 +79,43 @@ export class GridPage extends BasePage {
return this.get().locator(`tr[data-testid="grid-row-${index}"]`); return this.get().locator(`tr[data-testid="grid-row-${index}"]`);
} }
async renderColumn(columnHeader: string) {
// we have virtual grid, so we need to make sure the column is rendered
const headerRow = this.get().locator('.nc-grid-header').first();
let column = headerRow.locator(`[data-title="${columnHeader}"]`);
let lastScrolledColumn: Locator = null;
let direction = 'right';
while (headerRow) {
try {
await column.elementHandle({ timeout: 1000 });
break;
} catch {}
const lastColumn =
direction === 'right'
? headerRow.locator('th.nc-grid-column-header').last()
: headerRow.locator('th.nc-grid-column-header').nth(1);
if (lastScrolledColumn) {
if ((await lastScrolledColumn.innerText()) === (await lastColumn.innerText())) {
if (direction === 'right') {
direction = 'left';
lastScrolledColumn = null;
} else {
throw new Error(`Column with header "${columnHeader}" not found`);
}
}
}
await lastColumn.scrollIntoViewIfNeeded();
lastScrolledColumn = lastColumn;
column = headerRow.locator(`[data-title="${columnHeader}"]`);
}
}
async rowCount() { async rowCount() {
return await this.get().locator('.nc-grid-row').count(); return await this.get().locator('.nc-grid-row').count();
} }
@ -89,7 +126,8 @@ export class GridPage extends BasePage {
private async _fillRow({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) { private async _fillRow({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
const cell = this.cell.get({ index, columnHeader }); const cell = this.cell.get({ index, columnHeader });
await cell.waitFor({ state: 'visible' }); await expect(cell).toBeVisible();
await this.rootPage.waitForTimeout(500);
await this.cell.dblclick({ await this.cell.dblclick({
index, index,
columnHeader, columnHeader,
@ -132,7 +170,9 @@ export class GridPage extends BasePage {
// fallback // fallback
await this.rootPage.waitForTimeout(400); await this.rootPage.waitForTimeout(400);
await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount); await expect(this.get().locator(`[data-testid="grid-row-${rowCount - 1}"]`)).toHaveCount(1);
await this.rootPage.waitForLoadState('networkidle');
await this._fillRow({ index, columnHeader, value: rowValue }); await this._fillRow({ index, columnHeader, value: rowValue });

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

@ -100,7 +100,7 @@ export class UserOptionCellPageObject extends BasePage {
} }
const locator = this.cell.get({ index, columnHeader }).locator('.ant-tag'); const locator = this.cell.get({ index, columnHeader }).locator('.ant-tag');
await locator.waitFor({ state: 'visible' }); await expect(locator).toBeVisible();
return expect(locator).toContainText(option); return expect(locator).toContainText(option);
} }

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

@ -410,6 +410,7 @@ export class CellPageObject extends BasePage {
} }
async copyCellToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) { async copyCellToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) {
if (this.parent instanceof GridPage) await this.parent.renderColumn(columnHeader);
await this.get({ index, columnHeader }).scrollIntoViewIfNeeded(); await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.get({ index, columnHeader }).click(...clickOptions); await this.get({ index, columnHeader }).click(...clickOptions);
await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable'); await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable');

1
tests/playwright/pages/Dashboard/common/Topbar/index.ts

@ -82,5 +82,6 @@ export class TopbarPage extends BasePage {
async clickRefresh() { async clickRefresh() {
await this.get().locator(`.nc-icon-reload`).waitFor({ state: 'visible' }); await this.get().locator(`.nc-icon-reload`).waitFor({ state: 'visible' });
await this.get().locator(`.nc-icon-reload`).click(); await this.get().locator(`.nc-icon-reload`).click();
await this.rootPage.waitForLoadState('networkidle');
} }
} }

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

@ -214,9 +214,11 @@ test.describe('User single select', () => {
await grid.cell.click({ index: 3, columnHeader: 'User' }); await grid.cell.click({ index: 3, columnHeader: 'User' });
await dashboard.rootPage.keyboard.press('Shift+ArrowDown'); await dashboard.rootPage.keyboard.press('Shift+ArrowDown');
await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c'); await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+C' : 'Control+C');
await grid.cell.click({ index: 0, columnHeader: 'User' }); await grid.cell.click({ index: 0, columnHeader: 'User' });
await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v'); await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+V' : 'Control+V');
await dashboard.rootPage.waitForTimeout(500);
// refresh // refresh
await topbar.clickRefresh(); await topbar.clickRefresh();

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

@ -272,6 +272,8 @@ test.describe('Clipboard support', () => {
// ######################################## // ########################################
await dashboard.grid.renderColumn('Attachment');
await dashboard.grid.cell.attachment.addFile({ await dashboard.grid.cell.attachment.addFile({
index: 0, index: 0,
columnHeader: 'Attachment', columnHeader: 'Attachment',
@ -304,6 +306,7 @@ test.describe('Clipboard support', () => {
]; ];
for (const { type, value } of responseTable) { for (const { type, value } of responseTable) {
await dashboard.grid.renderColumn(type);
if (type === 'Rating') { if (type === 'Rating') {
await dashboard.grid.cell.rating.verify({ await dashboard.grid.cell.rating.verify({
index: rowIndex, index: rowIndex,

Loading…
Cancel
Save