Browse Source

Merge branch 'develop' into feat/pnpm

pull/5903/head
Wing-Kam Wong 1 year ago
parent
commit
f71298f5bb
  1. 4
      README.md
  2. 2
      packages/nc-gui/components/cell/Currency.vue
  3. 2
      packages/nc-gui/components/cell/DatePicker.vue
  4. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 2
      packages/nc-gui/components/cell/Decimal.vue
  6. 2
      packages/nc-gui/components/cell/Duration.vue
  7. 2
      packages/nc-gui/components/cell/Email.vue
  8. 2
      packages/nc-gui/components/cell/Float.vue
  9. 9
      packages/nc-gui/components/cell/Integer.vue
  10. 2
      packages/nc-gui/components/cell/Percent.vue
  11. 2
      packages/nc-gui/components/cell/Text.vue
  12. 2
      packages/nc-gui/components/cell/TextArea.vue
  13. 2
      packages/nc-gui/components/cell/TimePicker.vue
  14. 2
      packages/nc-gui/components/cell/Url.vue
  15. 2
      packages/nc-gui/components/cell/YearPicker.vue
  16. 23
      packages/nc-gui/components/cell/attachment/index.vue
  17. 543
      packages/nc-gui/components/smartsheet/Grid.vue
  18. 2
      packages/nc-gui/components/smartsheet/TableDataCell.vue
  19. 3
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  20. 11
      packages/nc-gui/components/smartsheet/header/Icon.vue
  21. 24
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  22. 6
      packages/nc-gui/composables/useExpandedFormStore.ts
  23. 18
      packages/nc-gui/composables/useMultiSelect/cellRange.ts
  24. 281
      packages/nc-gui/composables/useMultiSelect/index.ts
  25. 2
      packages/nc-gui/composables/useViewData.ts
  26. 1
      tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
  27. 1
      tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts
  28. 4
      tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts
  29. 32
      tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts
  30. 10
      tests/playwright/tests/db/columns/columnAttachments.spec.ts
  31. 6
      tests/playwright/tests/db/columns/columnBarcode.spec.ts
  32. 6
      tests/playwright/tests/db/columns/columnCheckbox.spec.ts
  33. 4
      tests/playwright/tests/db/columns/columnDateTime.spec.ts
  34. 4
      tests/playwright/tests/db/columns/columnDuration.spec.ts
  35. 6
      tests/playwright/tests/db/columns/columnFormula.spec.ts
  36. 6
      tests/playwright/tests/db/columns/columnGeoData.spec.ts
  37. 4
      tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts
  38. 4
      tests/playwright/tests/db/columns/columnLookupRollup.spec.ts
  39. 8
      tests/playwright/tests/db/columns/columnLtarDragdrop.spec.ts
  40. 4
      tests/playwright/tests/db/columns/columnMenuOperations.spec.ts
  41. 8
      tests/playwright/tests/db/columns/columnMultiSelect.spec.ts
  42. 6
      tests/playwright/tests/db/columns/columnQrCode.spec.ts
  43. 6
      tests/playwright/tests/db/columns/columnRating.spec.ts
  44. 4
      tests/playwright/tests/db/columns/columnRelationalExtendedTests.spec.ts
  45. 8
      tests/playwright/tests/db/columns/columnSingleSelect.spec.ts
  46. 12
      tests/playwright/tests/db/features/baseShare.spec.ts
  47. 14
      tests/playwright/tests/db/features/erd.spec.ts
  48. 10
      tests/playwright/tests/db/features/expandedFormUrl.spec.ts
  49. 12
      tests/playwright/tests/db/features/filters.spec.ts
  50. 8
      tests/playwright/tests/db/features/findRowByScanner.spec.ts
  51. 10
      tests/playwright/tests/db/features/import.spec.ts
  52. 6
      tests/playwright/tests/db/features/keyboardShortcuts.spec.ts
  53. 8
      tests/playwright/tests/db/features/language.spec.ts
  54. 11
      tests/playwright/tests/db/features/metaLTAR.spec.ts
  55. 8
      tests/playwright/tests/db/features/metaSync.spec.ts
  56. 8
      tests/playwright/tests/db/features/mobileMode.spec.ts
  57. 4
      tests/playwright/tests/db/features/pagination.spec.ts
  58. 6
      tests/playwright/tests/db/features/swagger.spec.ts
  59. 26
      tests/playwright/tests/db/features/timezone.spec.ts
  60. 12
      tests/playwright/tests/db/features/undo-redo.spec.ts
  61. 12
      tests/playwright/tests/db/features/updateBulk.ts
  62. 258
      tests/playwright/tests/db/features/verticalFillHandle.spec.ts
  63. 12
      tests/playwright/tests/db/features/webhook.spec.ts
  64. 6
      tests/playwright/tests/db/general/cellSelection.spec.ts
  65. 2
      tests/playwright/tests/db/general/megaTable.spec.ts
  66. 16
      tests/playwright/tests/db/general/projectOperations.spec.ts
  67. 6
      tests/playwright/tests/db/general/tableColumnOperation.spec.ts
  68. 10
      tests/playwright/tests/db/general/tableOperations.spec.ts
  69. 6
      tests/playwright/tests/db/general/toolbarOperations.spec.ts
  70. 6
      tests/playwright/tests/db/general/viewMenu.spec.ts
  71. 6
      tests/playwright/tests/db/general/views.spec.ts
  72. 8
      tests/playwright/tests/db/users&Accounts/accountLicense.spec.ts
  73. 6
      tests/playwright/tests/db/users&Accounts/accountTokenManagement.spec.ts
  74. 12
      tests/playwright/tests/db/users&Accounts/accountUserManagement.spec.ts
  75. 10
      tests/playwright/tests/db/users&Accounts/accountUserSettings.spec.ts
  76. 16
      tests/playwright/tests/db/users&Accounts/authChangePassword.spec.ts
  77. 12
      tests/playwright/tests/db/users&Accounts/rolesCreate.spec.ts
  78. 8
      tests/playwright/tests/db/users&Accounts/rolesPreview.spec.ts
  79. 4
      tests/playwright/tests/db/users&Accounts/rolesSuperUser.spec.ts
  80. 12
      tests/playwright/tests/db/views/viewForm.spec.ts
  81. 6
      tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts
  82. 6
      tests/playwright/tests/db/views/viewGridShare.spec.ts
  83. 8
      tests/playwright/tests/db/views/viewKanban.spec.ts
  84. 6
      tests/playwright/tests/db/views/viewMap.spec.ts

4
README.md

@ -156,14 +156,14 @@ curl http://get.nocodb.com/linux-arm64 -o nocodb -L && chmod +x nocodb && ./noco
##### Windows (x64) ##### Windows (x64)
```bash ```bash
iwr http://get.nocodb.com/win-x64.exe iwr http://get.nocodb.com/win-x64.exe -o Noco-win-x64.exe
.\Noco-win-x64.exe .\Noco-win-x64.exe
``` ```
##### Windows (arm64) ##### Windows (arm64)
```bash ```bash
iwr http://get.nocodb.com/win-arm64.exe iwr http://get.nocodb.com/win-arm64.exe -o Noco-win-arm64.exe
.\Noco-win-arm64.exe .\Noco-win-arm64.exe
``` ```

2
packages/nc-gui/components/cell/Currency.vue

@ -83,6 +83,8 @@ onMounted(() => {
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
@contextmenu.stop @contextmenu.stop

2
packages/nc-gui/components/cell/DatePicker.vue

@ -70,6 +70,8 @@ watch(
(next) => { (next) => {
if (next) { if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false)) onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
} else {
editable.value = false
} }
}, },
{ flush: 'post' }, { flush: 'post' },

2
packages/nc-gui/components/cell/DateTimePicker.vue

@ -122,6 +122,8 @@ watch(
(next) => { (next) => {
if (next) { if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false)) onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
} else {
editable.value = false
} }
}, },
{ flush: 'post' }, { flush: 'post' },

2
packages/nc-gui/components/cell/Decimal.vue

@ -55,6 +55,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/Duration.vue

@ -96,6 +96,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/Email.vue

@ -64,6 +64,8 @@ watch(
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/Float.vue

@ -55,6 +55,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

9
packages/nc-gui/components/cell/Integer.vue

@ -41,6 +41,15 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
function onKeyDown(evt: KeyboardEvent) { function onKeyDown(evt: KeyboardEvent) {
const cmdOrCtrl = isMac() ? evt.metaKey : evt.ctrlKey
if (cmdOrCtrl && !evt.altKey) {
switch (evt.keyCode) {
case 90: {
evt.stopPropagation()
break
}
}
}
return evt.key === '.' && evt.preventDefault() return evt.key === '.' && evt.preventDefault()
} }
</script> </script>

2
packages/nc-gui/components/cell/Percent.vue

@ -46,6 +46,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/Text.vue

@ -38,6 +38,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputEle
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/TextArea.vue

@ -37,6 +37,8 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLTextArea
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/TimePicker.vue

@ -69,6 +69,8 @@ watch(
(next) => { (next) => {
if (next) { if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false)) onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
} else {
editable.value = false
} }
}, },
{ flush: 'post' }, { flush: 'post' },

2
packages/nc-gui/components/cell/Url.vue

@ -93,6 +93,8 @@ watch(
@keydown.right.stop @keydown.right.stop
@keydown.up.stop @keydown.up.stop
@keydown.delete.stop @keydown.delete.stop
@keydown.ctrl.z.stop
@keydown.meta.z.stop
@selectstart.capture.stop @selectstart.capture.stop
@mousedown.stop @mousedown.stop
/> />

2
packages/nc-gui/components/cell/YearPicker.vue

@ -55,6 +55,8 @@ watch(
(next) => { (next) => {
if (next) { if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false)) onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, () => (open.value = false))
} else {
editable.value = false
} }
}, },
{ flush: 'post' }, { flush: 'post' },

23
packages/nc-gui/components/cell/attachment/index.vue

@ -2,6 +2,7 @@
import { onKeyDown } from '@vueuse/core' import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils' import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort' import { useSortable } from './sort'
import { RowHeightInj } from '~/context'
import { import {
ActiveCellInj, ActiveCellInj,
CurrentCellInj, CurrentCellInj,
@ -129,11 +130,16 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
} }
} }
}) })
const rowHeight = inject(RowHeightInj, ref(1.8))
</script> </script>
<template> <template>
<div <div
ref="attachmentCellRef" ref="attachmentCellRef"
:style="{
height: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1" class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1"
> >
<LazyCellAttachmentCarousel /> <LazyCellAttachmentCarousel />
@ -153,7 +159,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
<div <div
v-if="!isReadonly" v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }" :class="{ 'mx-auto px-4': !visibleItems.length }"
class="group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-testid="attachment-cell-file-picker-button" data-testid="attachment-cell-file-picker-button"
@click.stop="open" @click.stop="open"
> >
@ -162,7 +168,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
<a-tooltip v-else placement="bottom"> <a-tooltip v-else placement="bottom">
<template #title> Click or drop a file into cell</template> <template #title> Click or drop a file into cell</template>
<div class="flex items-center gap-2"> <div class="flex items-center gap-1">
<MaterialSymbolsAttachFile <MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]" class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/> />
@ -183,7 +189,10 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
<div <div
ref="sortableRef" ref="sortableRef"
:class="{ dragging }" :class="{ dragging }"
class="flex cursor-pointer justify-center items-center flex-wrap gap-2 p-1 scrollbar-thin-dull max-h-[150px] overflow-auto" class="flex cursor-pointer justify-center items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start"
:style="{
maxHeight: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
> >
<template v-for="(item, i) of visibleItems" :key="item.url || item.title"> <template v-for="(item, i) of visibleItems" :key="item.url || item.title">
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
@ -192,7 +201,11 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
</template> </template>
<div v-if="isImage(item.title, item.mimetype ?? item.type)"> <div v-if="isImage(item.title, item.mimetype ?? item.type)">
<div class="nc-attachment flex items-center justify-center" @click.stop="selectedImage = item"> <div class="nc-attachment flex items-center justify-center" @click.stop="selectedImage = item">
<LazyCellAttachmentImage :alt="item.title || `#${i}`" :srcs="getPossibleAttachmentSrc(item)" /> <LazyCellAttachmentImage
class="max-h-[1.8rem] max-w-[1.8rem]"
:alt="item.title || `#${i}`"
:srcs="getPossibleAttachmentSrc(item)"
/>
</div> </div>
</div> </div>
<div v-else class="nc-attachment flex items-center justify-center" @click="openAttachment(item)"> <div v-else class="nc-attachment flex items-center justify-center" @click="openAttachment(item)">
@ -229,7 +242,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
.nc-cell { .nc-cell {
.nc-attachment-cell { .nc-attachment-cell {
.nc-attachment { .nc-attachment {
@apply w-[50px] h-[50px] min-h-[50px] min-w-[50px] ring-1 ring-gray-300 rounded; @apply w-[1.8rem] h-[1.8rem] min-h-[1.8rem] min-w-[1.8rem] ring-1 ring-gray-300 rounded;
} }
.ghost, .ghost,

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

@ -101,6 +101,8 @@ const contextMenu = computed({
}) })
const contextMenuClosing = ref(false) const contextMenuClosing = ref(false)
const scrolling = ref(false)
const bulkUpdateDlg = ref(false) const bulkUpdateDlg = ref(false)
const routeQuery = $computed(() => route.query as Record<string, string>) const routeQuery = $computed(() => route.query as Record<string, string>)
@ -111,6 +113,9 @@ const expandedFormRowState = ref<Record<string, any>>()
const gridWrapper = ref<HTMLElement>() const gridWrapper = ref<HTMLElement>()
const tableHeadEl = ref<HTMLElement>() const tableHeadEl = ref<HTMLElement>()
const tableBodyEl = ref<HTMLElement>() const tableBodyEl = ref<HTMLElement>()
const fillHandle = ref<HTMLElement>()
const gridRect = useElementBounding(gridWrapper)
const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value) const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value)
@ -209,6 +214,8 @@ const {
resetSelectedRange, resetSelectedRange,
makeActive, makeActive,
selectedRange, selectedRange,
isCellInFillRange,
isFillMode,
} = useMultiSelect( } = useMultiSelect(
meta, meta,
fields, fields,
@ -350,6 +357,7 @@ const {
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title) await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
}, },
bulkUpdateRows, bulkUpdateRows,
fillHandle,
) )
function scrollToCell(row?: number | null, col?: number | null) { function scrollToCell(row?: number | null, col?: number | null) {
@ -652,10 +660,15 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
} }
}) })
/** On clicking outside of table reset active cell */
const smartTable = ref(null) const smartTable = ref(null)
/** On clicking outside of table reset active cell */
onClickOutside(tableBodyEl, (e) => { onClickOutside(tableBodyEl, (e) => {
// do nothing if mousedown on the scrollbar (scrolling)
if (scrolling.value) {
return
}
// do nothing if context menu was open // do nothing if context menu was open
if (contextMenu.value) return if (contextMenu.value) return
@ -665,6 +678,9 @@ onClickOutside(tableBodyEl, (e) => {
if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return
// skip if fill mode is active
if (isFillMode.value) return
// ignore unselecting if clicked inside or on the picker(Date, Time, DateTime, Year) // ignore unselecting if clicked inside or on the picker(Date, Time, DateTime, Year)
// or single/multi select options // or single/multi select options
const activePickerOrDropdownEl = document.querySelector( const activePickerOrDropdownEl = document.querySelector(
@ -865,6 +881,15 @@ const closeAddColumnDropdown = (scrollToLastCol = false) => {
const confirmDeleteRow = (row: number) => { const confirmDeleteRow = (row: number) => {
try { try {
deleteRow(row) deleteRow(row)
if (selectedRange.isRowInRange(row)) {
clearSelectedRange()
}
if (activeCell.row === row) {
activeCell.row = null
activeCell.col = null
}
} catch (e: any) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
@ -883,10 +908,78 @@ function addEmptyRow(row?: number) {
nextTick().then(() => { nextTick().then(() => {
clearSelectedRange() clearSelectedRange()
makeActive(row ?? data.value.length - 1, 0) makeActive(row ?? data.value.length - 1, 0)
selectedRange.startRange({ row: activeCell.row!, col: activeCell.col! })
scrollToCell?.() scrollToCell?.()
}) })
return rowObj return rowObj
} }
const fillHandleTop = ref()
const fillHandleLeft = ref()
const cellRefs = ref<{ el: HTMLElement }[]>([])
const showFillHandle = computed(
() =>
!readOnly.value &&
!isLocked.value &&
!editEnabled &&
(!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null)) &&
!data.value[(isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) ?? -1]?.rowMeta?.new,
)
const refreshFillHandle = () => {
const cellRef = cellRefs.value.find(
(cell) =>
cell.el.dataset.rowIndex === String(isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) &&
cell.el.dataset.colIndex === String(isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col),
)
if (cellRef) {
const cellRect = useElementBounding(cellRef.el)
if (!cellRect || !gridWrapper.value) return
fillHandleTop.value = cellRect.top.value + cellRect.height.value - gridRect.top.value + gridWrapper.value.scrollTop
fillHandleLeft.value = cellRect.left.value + cellRect.width.value - gridRect.left.value + gridWrapper.value.scrollLeft
}
}
const addRowExpandOnClose = (row: Row) => {
if (!skipRowRemovalOnCancel.value) {
const removed = removeRowIfNew(row)
if (removed) {
clearSelectedRange()
activeCell.row = null
activeCell.col = null
}
}
}
watch(
[() => selectedRange.end.row, () => selectedRange.end.col, () => activeCell.row, () => activeCell.col],
([sr, sc, ar, ac], [osr, osc, oar, oac]) => {
if (sr !== osr || sc !== osc || ar !== oar || ac !== oac) {
refreshFillHandle()
}
},
)
useEventListener(gridWrapper, 'scroll', () => {
refreshFillHandle()
})
useEventListener(document, 'mousedown', (e) => {
if (e.offsetX > (e.target as HTMLElement)?.clientWidth || e.offsetY > (e.target as HTMLElement)?.clientHeight) {
scrolling.value = true
}
})
useEventListener(document, 'mouseup', () => {
// wait for click event to finish before setting scrolling to false
setTimeout(() => {
scrolling.value = false
}, 100)
})
</script> </script>
<template> <template>
@ -897,219 +990,237 @@ function addEmptyRow(row?: number) {
</div> </div>
</general-overlay> </general-overlay>
<div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull"> <div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull relative">
<a-dropdown <a-dropdown
v-model:visible="contextMenu" v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']" :trigger="isSqlView ? [] : ['contextmenu']"
overlay-class-name="nc-dropdown-grid-context-menu" overlay-class-name="nc-dropdown-grid-context-menu"
> >
<table <div class="table-overlay">
ref="smartTable" <table
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white" ref="smartTable"
@contextmenu="showContextMenu" class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
> @contextmenu="showContextMenu"
<thead ref="tableHeadEl"> >
<tr class="nc-grid-header"> <thead ref="tableHeadEl">
<th class="w-[85px] min-w-[85px]" data-testid="grid-id-column"> <tr class="nc-grid-header">
<div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all"> <th class="w-[85px] min-w-[85px]" data-testid="grid-id-column">
<template v-if="!readOnly"> <div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div> <template v-if="!readOnly">
<div <div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }" <div
class="nc-check-all w-full items-center" :class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
> class="nc-check-all w-full items-center"
<a-checkbox v-model:checked="selectedAllRecords" /> >
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" /> <span class="flex-1" />
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="text-gray-500">#</div> <div class="text-gray-500">#</div>
</template> </template>
</div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div>
</th>
<th
v-if="isAddingColumnAllowed"
v-e="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
>
<a-dropdown
v-model:visible="addColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-grid-add-column"
>
<div class="h-full w-[60px] flex items-center justify-center">
<component :is="iconMap.plus" class="text-sm nc-column-add" />
</div> </div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<template #overlay> <LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
<SmartsheetColumnEditOrAddProvider </div>
v-if="addColumnDropdown" </th>
:preload="preloadColumn" <th
:column-position="columnOrder" v-if="isAddingColumnAllowed"
@submit="closeAddColumnDropdown(true)" v-e="['c:column:add']"
@cancel="closeAddColumnDropdown()" class="cursor-pointer"
@click.stop @click.stop="addColumnDropdown = true"
@keydown.stop
/>
</template>
</a-dropdown>
</th>
</tr>
</thead>
<tbody ref="tableBodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr
class="nc-grid-row"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`"
> >
<td <a-dropdown
key="row-index" v-model:visible="addColumnDropdown"
class="caption nc-grid-cell pl-5 pr-1" :trigger="['click']"
:data-testid="`cell-Id-${rowIndex}`" overlay-class-name="nc-dropdown-grid-add-column"
@contextmenu="contextMenuTarget = null"
> >
<div class="items-center flex gap-1 min-w-[60px]"> <div class="h-full w-[60px] flex items-center justify-center">
<div <component :is="iconMap.plus" class="text-sm nc-column-add" />
v-if="!readOnly || !isLocked" </div>
class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ ((paginationData.page ?? 1) - 1) * (paginationData.pageSize ?? 25) + rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div <template #overlay>
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)" <SmartsheetColumnEditOrAddProvider
class="nc-expand" v-if="addColumnDropdown"
:data-testid="`nc-expand-${rowIndex}`" :preload="preloadColumn"
:class="{ 'nc-comment': row.rowMeta?.commentCount }" :column-position="columnOrder"
> @submit="closeAddColumnDropdown(true)"
<a-spin @cancel="closeAddColumnDropdown()"
v-if="row.rowMeta.saving" @click.stop
class="!flex items-center" @keydown.stop
:data-testid="`row-save-spinner-${rowIndex}`" />
/> </template>
<template v-else> </a-dropdown>
<span </th>
v-if="row.rowMeta?.commentCount" </tr>
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)" </thead>
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }" <tbody ref="tableBodyEl">
@click="expandForm(row, state)" <LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
> <template #default="{ state }">
{{ row.rowMeta.commentCount }} <tr
</span> class="nc-grid-row"
<div :style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
v-else :data-testid="`grid-row-${rowIndex}`"
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)" >
> <td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<component <div class="items-center flex gap-1 min-w-[60px]">
:is="iconMap.expand" <div
v-e="['c:row-expand']" v-if="!readOnly || !isLocked"
class="select-none transform hover:(text-accent scale-120) nc-row-expand" class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ ((paginationData.page ?? 1) - 1) * (paginationData.pageSize ?? 25) + rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else>
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandForm(row, state)" @click="expandForm(row, state)"
/> >
</div> {{ row.rowMeta.commentCount }}
</template> </span>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<component
:is="iconMap.expand"
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)"
/>
</div>
</template>
</div>
</div> </div>
</div> </td>
</td> <SmartsheetTableDataCell
<SmartsheetTableDataCell v-for="(columnObj, colIndex) of fields"
v-for="(columnObj, colIndex) of fields" :key="columnObj.id"
:key="columnObj.id" ref="cellRefs"
class="cell relative nc-grid-cell" class="cell relative nc-grid-cell"
:class="{ :class="{
'cursor-pointer': hasEditPermission, 'cursor-pointer': hasEditPermission,
'active': hasEditPermission && isCellSelected(rowIndex, colIndex), 'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), 'active-cell':
'align-middle': !rowHeight || rowHeight === 1, hasEditPermission &&
'align-top': rowHeight && rowHeight !== 1, ((activeCell.row === rowIndex && activeCell.col === colIndex) ||
}" (selectedRange._start?.row === rowIndex && selectedRange._start?.col === colIndex)),
:data-testid="`cell-${columnObj.title}-${rowIndex}`" 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
:data-key="rowIndex + columnObj.id" 'align-middle': !rowHeight || rowHeight === 1,
:data-col="columnObj.id" 'align-top': rowHeight && rowHeight !== 1,
:data-title="columnObj.title" 'filling': isCellInFillRange(rowIndex, colIndex),
@mousedown="handleMouseDown($event, rowIndex, colIndex)" }"
@mouseover="handleMouseOver($event, rowIndex, colIndex)" :data-testid="`cell-${columnObj.title}-${rowIndex}`"
@click="handleCellClick($event, rowIndex, colIndex)" :data-key="rowIndex + columnObj.id"
@dblclick="makeEditable(row, columnObj)" :data-col="columnObj.id"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })" :data-title="columnObj.title"
> :data-row-index="rowIndex"
<div v-if="!switchingTab" class="w-full h-full"> :data-col-index="colIndex"
<LazySmartsheetVirtualCell @mousedown="handleMouseDown($event, rowIndex, colIndex)"
v-if="isVirtualCol(columnObj)" @mouseover="handleMouseOver($event, rowIndex, colIndex)"
v-model="row.row[columnObj.title]" @click="handleCellClick($event, rowIndex, colIndex)"
:column="columnObj" @dblclick="makeEditable(row, columnObj)"
:active="activeCell.col === colIndex && activeCell.row === rowIndex" @contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
:row="row" >
:read-only="readOnly" <div v-if="!switchingTab" class="w-full h-full">
@navigate="onNavigate" <LazySmartsheetVirtualCell
@save="updateOrSaveRow(row, '', state)" v-if="isVirtualCol(columnObj)"
/> v-model="row.row[columnObj.title]"
:column="columnObj"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
:read-only="readOnly"
@navigate="onNavigate"
@save="updateOrSaveRow(row, '', state)"
/>
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="row.row[columnObj.title]" v-model="row.row[columnObj.title]"
:column="columnObj" :column="columnObj"
:edit-enabled=" :edit-enabled="
!!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex !!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex
" "
:row-index="rowIndex" :row-index="rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex" :active="activeCell.col === colIndex && activeCell.row === rowIndex"
:read-only="readOnly" :read-only="readOnly"
@update:edit-enabled="editEnabled = $event" @update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow(row, columnObj.title, state)" @save="updateOrSaveRow(row, columnObj.title, state)"
@navigate="onNavigate" @navigate="onNavigate"
@cancel="editEnabled = false" @cancel="editEnabled = false"
/> />
</div> </div>
</SmartsheetTableDataCell> </SmartsheetTableDataCell>
</tr> </tr>
</template> </template>
</LazySmartsheetRow> </LazySmartsheetRow>
<tr <tr
v-if="isAddingEmptyRowAllowed" v-if="isAddingEmptyRowAllowed"
v-e="['c:row:add:grid-bottom']" v-e="['c:row:add:grid-bottom']"
class="cursor-pointer" class="cursor-pointer relative z-3"
@mouseup.stop @mouseup.stop
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<td class="text-left pointer nc-grid-add-new-cell sticky left-0 !z-5 !border-r-0"> <td class="text-left pointer nc-grid-add-new-cell sticky left-0 !z-5 !border-r-0">
<div class="px-2 w-full flex items-center text-gray-500"> <div class="px-2 w-full flex items-center text-gray-500">
<component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" /> <component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" />
</div> </div>
</td> </td>
<td :colspan="visibleColLength"></td> <td :colspan="visibleColLength"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- Fill Handle -->
<div
v-show="showFillHandle"
ref="fillHandle"
class="nc-fill-handle"
:class="
(!selectedRange.isEmpty() && selectedRange.end.col !== 0) || (selectedRange.isEmpty() && activeCell.col !== 0)
? 'z-3'
: 'z-4'
"
:style="{ top: `${fillHandleTop}px`, left: `${fillHandleLeft}px`, cursor: 'crosshair' }"
/>
</div>
<template v-if="!isLocked && hasEditPermission" #overlay> <template v-if="!isLocked && hasEditPermission" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false"> <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
@ -1213,7 +1324,7 @@ function addEmptyRow(row?: number) {
:state="expandedFormRowState" :state="expandedFormRowState"
:meta="meta" :meta="meta"
:view="view" :view="view"
@update:model-value="!skipRowRemovalOnCancel && removeRowIfNew(expandedFormRow)" @update:model-value="addRowExpandOnClose(expandedFormRow)"
/> />
</Suspense> </Suspense>
@ -1292,9 +1403,29 @@ function addEmptyRow(row?: number) {
// todo: replace with css variable // todo: replace with css variable
td.active::after { td.active::after {
@apply text-primary border-current bg-primary bg-opacity-5;
}
td.active-cell::after {
@apply border-1 border-solid text-primary border-current bg-primary bg-opacity-5; @apply border-1 border-solid text-primary border-current bg-primary bg-opacity-5;
} }
td.filling::after {
content: '';
position: absolute;
z-index: 3;
height: calc(100% + 2px);
width: calc(100% + 2px);
left: -1px;
top: -1px;
pointer-events: none;
}
// todo: replace with css variable
td.filling::after {
@apply border-1 border-dashed text-primary border-current bg-gray-100 bg-opacity-50;
}
//td.active::before { //td.active::before {
// content: ''; // content: '';
// z-index:4; // z-index:4;
@ -1394,4 +1525,14 @@ tbody tr:hover {
.nc-required-cell { .nc-required-cell {
box-shadow: inset 0 0 2px #f00; box-shadow: inset 0 0 2px #f00;
} }
.nc-fill-handle {
@apply w-[6px] h-[6px] absolute rounded-full bg-red-500 !pointer-events-auto mt-[-4px] ml-[-4px];
}
.nc-fill-handle:hover,
.nc-fill-handle:active,
.nc-fill-handle:focus {
@apply w-[8px] h-[8px] mt-[-5px] ml-[-5px];
}
</style> </style>

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

@ -8,6 +8,8 @@ const cellClickHook = createEventHook()
provide(CellClickHookInj, cellClickHook) provide(CellClickHookInj, cellClickHook)
provide(CurrentCellInj, el) provide(CurrentCellInj, el)
defineExpose({ el })
</script> </script>
<template> <template>

3
packages/nc-gui/components/smartsheet/expanded-form/Header.vue

@ -5,6 +5,7 @@ import {
ReloadRowDataHookInj, ReloadRowDataHookInj,
iconMap, iconMap,
isMac, isMac,
useCopy,
useExpandedFormStoreOrThrow, useExpandedFormStoreOrThrow,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
@ -46,7 +47,7 @@ const iconColor = '#1890ff'
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const { copy } = useClipboard() const { copy } = useCopy()
const copyRecordUrl = () => { const copyRecordUrl = () => {
copy( copy(

11
packages/nc-gui/components/smartsheet/header/Icon.vue

@ -0,0 +1,11 @@
<script lang="ts" setup>
import { ColumnType, isVirtualCol } from 'nocodb-sdk'
const { column } = defineProps<{ column: ColumnType }>()
</script>
<template>
<SmartsheetHeaderVirtualCellIcon v-if="isVirtualCol(column)" :column-meta="column" />
<SmartsheetHeaderCellIcon v-else :column-meta="column" />
</template>

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

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isSystemColumn } from 'nocodb-sdk'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -27,10 +28,15 @@ const searchDropdown = ref(null)
onClickOutside(searchDropdown, () => (isDropdownOpen.value = false)) onClickOutside(searchDropdown, () => (isDropdownOpen.value = false))
const columns = computed(() => const columns = computed(() =>
(meta.value as TableType)?.columns?.map((column) => ({ (meta.value as TableType)?.columns
value: column.id, ?.filter((c) => {
label: column.title, return !isSystemColumn(c)
})), })
.map((column) => ({
value: column.id,
label: column.title,
column,
})),
) )
watch( watch(
@ -65,10 +71,16 @@ function onPressEnter() {
:open="isDropdownOpen" :open="isDropdownOpen"
size="small" size="small"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
:options="columns"
dropdown-class-name="!py-0 !rounded nc-dropdown-toolbar-search-field-option" dropdown-class-name="!py-0 !rounded nc-dropdown-toolbar-search-field-option"
class="!absolute top-0 left-0 w-full h-full z-10 !text-xs opacity-0" class="!absolute top-0 left-0 w-full h-full z-10 !text-xs opacity-0"
/> >
<a-select-option v-for="op of columns" :key="op.value" :value="op.value">
<div class="flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="" :column="op.column" />
{{ op.label }}
</div>
</a-select-option>
</a-select>
</div> </div>
<a-input <a-input

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

@ -267,7 +267,11 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
addOrEditStackRow(row.value, isNewRow) addOrEditStackRow(row.value, isNewRow)
} }
message.success(`${displayValue.value || 'Row'} updated successfully.`) // trim the display value if greater than 20chars
const trimmedDisplayValue =
displayValue.value && displayValue.value?.length > 20 ? `${displayValue.value?.substring(0, 20)}...` : displayValue.value
message.success(`${trimmedDisplayValue || 'Row'} updated successfully.`)
changedColumns.value = new Set() changedColumns.value = new Set()
} catch (e: any) { } catch (e: any) {

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

@ -24,6 +24,24 @@ export class CellRange {
return !this.isEmpty() && this._start?.row === this._end?.row return !this.isEmpty() && this._start?.row === this._end?.row
} }
isCellInRange(cell: Cell) {
return (
!this.isEmpty() &&
cell.row >= this.start.row &&
cell.row <= this.end.row &&
cell.col >= this.start.col &&
cell.col <= this.end.col
)
}
isRowInRange(row: number) {
return !this.isEmpty() && row >= this.start.row && row <= this.end.row
}
isColInRange(col: number) {
return !this.isEmpty() && col >= this.start.col && col <= this.end.col
}
get start(): Cell { get start(): Cell {
return { return {
row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN), row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN),

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

@ -48,6 +48,7 @@ export function useMultiSelect(
keyEventHandler?: Function, keyEventHandler?: Function,
syncCellData?: Function, syncCellData?: Function,
bulkUpdateRows?: Function, bulkUpdateRows?: Function,
fillHandle?: MaybeRef<HTMLElement | undefined>,
) { ) {
const meta = ref(_meta) const meta = ref(_meta)
@ -59,14 +60,18 @@ export function useMultiSelect(
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
const { isMysql } = useProject() const { isMysql, isPg } = useProject()
const editEnabled = ref(_editEnabled) const editEnabled = ref(_editEnabled)
let isMouseDown = $ref(false) const isMouseDown = ref(false)
const isFillMode = ref(false)
const selectedRange = reactive(new CellRange()) const selectedRange = reactive(new CellRange())
const fillRange = reactive(new CellRange())
const activeCell = reactive<Nullable<Cell>>({ row: null, col: null }) const activeCell = reactive<Nullable<Cell>>({ row: null, col: null })
const columnLength = $computed(() => unref(fields)?.length) const columnLength = $computed(() => unref(fields)?.length)
@ -123,7 +128,7 @@ export function useMultiSelect(
}) })
} }
if (columnObj.uidt === UITypes.DateTime || columnObj.uidt === UITypes.Time) { if (columnObj.uidt === UITypes.DateTime) {
// remove `"` // remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z // e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z
textToCopy = textToCopy.replace(/["']/g, '') textToCopy = textToCopy.replace(/["']/g, '')
@ -142,16 +147,44 @@ export function useMultiSelect(
// users can change the datetime format in UI // users can change the datetime format in UI
// `textToCopy` would be always in YYYY-MM-DD HH:mm:ss(Z / +xx:yy) format // `textToCopy` would be always in YYYY-MM-DD HH:mm:ss(Z / +xx:yy) format
// therefore, here we reformat to the correct datetime format based on the meta // therefore, here we reformat to the correct datetime format based on the meta
textToCopy = d.format( textToCopy = d.format(constructDateTimeFormat(columnObj))
columnObj.uidt === UITypes.DateTime ? constructDateTimeFormat(columnObj) : constructTimeFormat(columnObj),
)
if (!dayjs(textToCopy).isValid()) { if (!dayjs(textToCopy).isValid()) {
// return empty string for invalid datetime / time // return empty string for invalid datetime
return '' return ''
} }
} }
if (columnObj.uidt === UITypes.Time) {
// remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z
textToCopy = textToCopy.replace(/["']/g, '')
const isMySQL = isMysql(columnObj.base_id)
const isPostgres = isPg(columnObj.base_id)
let d = dayjs(textToCopy)
if (!d.isValid()) {
// insert a datetime value, copy the value without refreshing
// e.g. textToCopy = 2023-05-12T03:49:25.000Z
// feed custom parse format
d = dayjs(textToCopy, isMySQL ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ')
}
if (!d.isValid()) {
// MySQL and Postgres store time in HH:mm:ss format so we need to feed custom parse format
d = isMySQL || isPostgres ? dayjs(textToCopy, 'HH:mm:ss') : dayjs(textToCopy)
}
if (!d.isValid()) {
// return empty string for invalid time
return ''
}
textToCopy = d.format(constructTimeFormat(columnObj))
}
if (columnObj.uidt === UITypes.LongText) { if (columnObj.uidt === UITypes.LongText) {
textToCopy = `"${textToCopy.replace(/\"/g, '""')}"` textToCopy = `"${textToCopy.replace(/\"/g, '""')}"`
} }
@ -159,23 +192,33 @@ export function useMultiSelect(
return textToCopy return textToCopy
} }
const copyTable = async (rows: Row[], cols: ColumnType[]) => { const serializeRange = (rows: Row[], cols: ColumnType[]) => {
let copyHTML = '<table>' let html = '<table>'
let copyPlainText = '' let text = ''
const json: string[][] = []
rows.forEach((row, i) => { rows.forEach((row, i) => {
let copyRow = '<tr>' let copyRow = '<tr>'
const jsonRow: string[] = []
cols.forEach((col, i) => { cols.forEach((col, i) => {
const value = valueToCopy(row, col) const value = valueToCopy(row, col)
copyRow += `<td>${value}</td>` copyRow += `<td>${value}</td>`
copyPlainText = `${copyPlainText}${value}${cols.length - 1 !== i ? '\t' : ''}` text = `${text}${value}${cols.length - 1 !== i ? '\t' : ''}`
jsonRow.push(col.uidt === UITypes.LongText ? value.replace(/^"/, '').replace(/"$/, '').replace(/""/g, '"') : value)
}) })
copyHTML += `${copyRow}</tr>` html += `${copyRow}</tr>`
if (rows.length - 1 !== i) { if (rows.length - 1 !== i) {
copyPlainText = `${copyPlainText}\n` text = `${text}\n`
} }
json.push(jsonRow)
}) })
copyHTML += '</table>' html += '</table>'
return { html, text, json }
}
const copyTable = async (rows: Row[], cols: ColumnType[]) => {
const { html: copyHTML, text: copyPlainText } = serializeRange(rows, cols)
const blobHTML = new Blob([copyHTML], { type: 'text/html' }) const blobHTML = new Blob([copyHTML], { type: 'text/html' })
const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' }) const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' })
@ -217,20 +260,71 @@ export function useMultiSelect(
return true return true
} }
if (selectedRange.start === null || selectedRange.end === null) { return selectedRange.isCellInRange({ row, col })
}
function isCellInFillRange(row: number, col: number) {
if (fillRange._start === null || fillRange._end === null) {
return false
}
if (selectedRange.isCellInRange({ row, col })) {
return false
}
return fillRange.isCellInRange({ row, col })
}
const isPasteable = (row?: Row, col?: ColumnType, showInfo = false) => {
if (!row || !col) {
if (showInfo) {
message.info('Please select a cell to paste')
}
return false
}
// skip pasting virtual columns (including LTAR columns for now) and system columns
if (isVirtualCol(col) || isSystemColumn(col)) {
if (showInfo) {
message.info(t('msg.info.pasteNotSupported'))
}
return false
}
// skip pasting auto increment columns
if (col.ai) {
if (showInfo) {
message.info(t('msg.info.autoIncFieldNotEditable'))
}
return false return false
} }
return ( // skip pasting primary key columns
col >= selectedRange.start.col && if (col.pk && !row.rowMeta.new) {
col <= selectedRange.end.col && if (showInfo) {
row >= selectedRange.start.row && message.info(t('msg.info.editingPKnotSupported'))
row <= selectedRange.end.row }
) return false
}
return true
} }
function handleMouseOver(event: MouseEvent, row: number, col: number) { function handleMouseOver(event: MouseEvent, row: number, col: number) {
if (!isMouseDown) { if (isFillMode.value) {
const rw = unref(data)[row]
if (!selectedRange._start || !selectedRange._end) return
// fill is not supported for new rows yet
if (rw.rowMeta.new) return
fillRange.endRange({ row, col: selectedRange._end.col })
scrollToCell?.(row, col)
return
}
if (!isMouseDown.value) {
return return
} }
@ -252,7 +346,7 @@ export function useMultiSelect(
} }
// if edit is enabled, don't start the selection (some cells shrink after edit mode, which causes the selection to expand if flag is set) // if edit is enabled, don't start the selection (some cells shrink after edit mode, which causes the selection to expand if flag is set)
if (!editEnabled.value) isMouseDown = true if (!editEnabled.value) isMouseDown.value = true
contextMenu.value = false contextMenu.value = false
@ -290,8 +384,90 @@ export function useMultiSelect(
} }
const handleMouseUp = (_event: MouseEvent) => { const handleMouseUp = (_event: MouseEvent) => {
if (isMouseDown) { if (isFillMode.value) {
isMouseDown = false isFillMode.value = false
if (fillRange._start === null || fillRange._end === null) return
if (selectedRange._start !== null && selectedRange._end !== null) {
const tempActiveCell = { row: selectedRange._start.row, col: selectedRange._start.col }
const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
const rawMatrix = serializeRange(cprows, cpcols).json
const fillDirection = fillRange._start.row <= fillRange._end.row ? 1 : -1
let fillIndex = fillDirection === 1 ? 0 : rawMatrix.length - 1
const rowsToPaste: Row[] = []
const propsToPaste: string[] = []
for (
let row = fillRange._start.row;
fillDirection === 1 ? row <= fillRange._end.row : row >= fillRange._end.row;
row += fillDirection
) {
if (isCellSelected(row, selectedRange.start.col)) {
continue
}
const rowObj = unref(data)[row]
let pasteIndex = 0
for (let col = fillRange.start.col; col <= fillRange.end.col; col++) {
const colObj = unref(fields)[col]
if (!isPasteable(rowObj, colObj)) {
pasteIndex++
continue
}
propsToPaste.push(colObj.title!)
const pasteValue = convertCellData(
{
value: rawMatrix[fillIndex][pasteIndex],
to: colObj.uidt as UITypes,
column: colObj,
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
true,
)
if (pasteValue !== undefined) {
rowObj.row[colObj.title!] = pasteValue
rowsToPaste.push(rowObj)
}
pasteIndex++
}
if (fillDirection === 1) {
fillIndex = fillIndex < rawMatrix.length - 1 ? fillIndex + 1 : 0
} else {
fillIndex = fillIndex >= 1 ? fillIndex - 1 : rawMatrix.length - 1
}
}
bulkUpdateRows?.(rowsToPaste, propsToPaste).then(() => {
if (fillRange._start === null || fillRange._end === null) return
selectedRange.startRange(tempActiveCell)
selectedRange.endRange(fillRange._end)
makeActive(tempActiveCell.row, tempActiveCell.col)
fillRange.clear()
})
} else {
fillRange.clear()
}
return
}
if (isMouseDown.value) {
isMouseDown.value = false
// timeout is needed, because we want to set cell as active AFTER all the child's click handler's called // timeout is needed, because we want to set cell as active AFTER all the child's click handler's called
// this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown // this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown
setTimeout(() => { setTimeout(() => {
@ -530,41 +706,6 @@ export function useMultiSelect(
const clearSelectedRange = selectedRange.clear.bind(selectedRange) const clearSelectedRange = selectedRange.clear.bind(selectedRange)
const isPasteable = (row?: Row, col?: ColumnType, showInfo = false) => {
if (!row || !col) {
if (showInfo) {
message.info('Please select a cell to paste')
}
return false
}
// skip pasting virtual columns (including LTAR columns for now) and system columns
if (isVirtualCol(col) || isSystemColumn(col)) {
if (showInfo) {
message.info(t('msg.info.pasteNotSupported'))
}
return false
}
// skip pasting auto increment columns
if (col.ai) {
if (showInfo) {
message.info(t('msg.info.autoIncFieldNotEditable'))
}
return false
}
// skip pasting primary key columns
if (col.pk && !row.rowMeta.new) {
if (showInfo) {
message.info(t('msg.info.editingPKnotSupported'))
}
return false
}
return true
}
const handlePaste = async (e: ClipboardEvent) => { const handlePaste = async (e: ClipboardEvent) => {
if (isDrawerOrModalExist()) { if (isDrawerOrModalExist()) {
return return
@ -751,10 +892,27 @@ export function useMultiSelect(
} }
} }
function fillHandleMouseDown(event: MouseEvent) {
if (event?.button !== MAIN_MOUSE_PRESSED) {
return
}
isFillMode.value = true
if (selectedRange._start && selectedRange._end) {
fillRange.startRange({ row: selectedRange._start?.row, col: selectedRange._start.col })
fillRange.endRange({ row: selectedRange._end?.row, col: selectedRange._end.col })
}
event.preventDefault()
}
useEventListener(document, 'keydown', handleKeyDown) useEventListener(document, 'keydown', handleKeyDown)
useEventListener(document, 'mouseup', handleMouseUp) useEventListener(document, 'mouseup', handleMouseUp)
useEventListener(document, 'paste', handlePaste) useEventListener(document, 'paste', handlePaste)
useEventListener(fillHandle, 'mousedown', fillHandleMouseDown)
return { return {
isCellActive, isCellActive,
handleMouseDown, handleMouseDown,
@ -767,5 +925,8 @@ export function useMultiSelect(
resetSelectedRange, resetSelectedRange,
selectedRange, selectedRange,
makeActive, makeActive,
isCellInFillRange,
isMouseDown,
isFillMode,
} }
} }

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

@ -938,7 +938,9 @@ export function useViewData(
if (index > -1 && row.rowMeta.new) { if (index > -1 && row.rowMeta.new) {
formattedData.value.splice(index, 1) formattedData.value.splice(index, 1)
return true
} }
return false
} }
// get current expanded row index // get current expanded row index

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

@ -23,6 +23,7 @@ export class AttachmentCellPageObject extends BasePage {
// //
async addFile({ index, columnHeader, filePath }: { index?: number; columnHeader: string; filePath: string[] }) { async addFile({ index, columnHeader, filePath }: { index?: number; columnHeader: string; filePath: string[] }) {
await this.get({ index, columnHeader }).scrollIntoViewIfNeeded(); await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.get({ index, columnHeader }).click({ position: { x: 1, y: 1 } });
const attachFileAction = this.get({ index, columnHeader }) const attachFileAction = this.get({ index, columnHeader })
.locator('[data-testid="attachment-cell-file-picker-button"]') .locator('[data-testid="attachment-cell-file-picker-button"]')
.click(); .click();

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

@ -15,6 +15,7 @@ export class RatingCellPageObject extends BasePage {
} }
async select({ index, columnHeader, rating }: { index?: number; columnHeader: string; rating: number }) { async select({ index, columnHeader, rating }: { index?: number; columnHeader: string; rating: number }) {
await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
await this.waitForResponse({ await this.waitForResponse({
uiAction: async () => await this.get({ index, columnHeader }).locator('.ant-rate-star > div').nth(rating).click(), uiAction: async () => await this.get({ index, columnHeader }).locator('.ant-rate-star > div').nth(rating).click(),
httpMethodsToMatch: ['POST', 'PATCH'], httpMethodsToMatch: ['POST', 'PATCH'],

4
tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts

@ -115,7 +115,9 @@ export class SelectOptionCellPageObject extends BasePage {
const selectCell = this.get({ index, columnHeader }); const selectCell = this.get({ index, columnHeader });
// check if cell active // check if cell active
if (!(await selectCell.getAttribute('class')).includes('active')) { // drag based non-primary cell will have 'active' attribute
// primary cell with blue border will have 'active-cell' attribute
if (!(await selectCell.getAttribute('class')).includes('active-cell')) {
await selectCell.click(); await selectCell.click();
} }

32
tests/playwright/pages/Dashboard/common/Cell/TimeCell.ts

@ -20,4 +20,36 @@ export class TimeCellPageObject extends BasePage {
await cell.locator(`input[title="${value}"]`).waitFor({ state: 'visible' }); await cell.locator(`input[title="${value}"]`).waitFor({ state: 'visible' });
await expect(cell.locator(`[title="${value}"]`)).toBeVisible(); await expect(cell.locator(`[title="${value}"]`)).toBeVisible();
} }
async selectTime({
// hour: 0 - 23
// minute: 0 - 59
// second: 0 - 59
hour,
minute,
}: {
hour: number;
minute: number;
}) {
const timePanel = await this.rootPage.locator('.ant-picker-time-panel-column');
await timePanel.nth(0).locator('.ant-picker-time-panel-cell').nth(hour).click();
await timePanel.nth(1).locator('.ant-picker-time-panel-cell').nth(minute).click();
if (hour < 12) {
await timePanel.nth(2).locator('.ant-picker-time-panel-cell').nth(0).click();
} else {
await timePanel.nth(2).locator('.ant-picker-time-panel-cell').nth(1).click();
}
}
async save() {
await this.rootPage.locator('button:has-text("Ok"):visible').click();
}
async set({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
const [hour, minute, _second] = value.split(':');
await this.get({ index, columnHeader }).click();
await this.get({ index, columnHeader }).click();
await this.selectTime({ hour: +hour, minute: +minute });
await this.save();
}
} }

10
tests/playwright/tests/db/columnAttachments.spec.ts → tests/playwright/tests/db/columns/columnAttachments.spec.ts

@ -1,9 +1,9 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { SharedFormPage } from '../../pages/SharedForm'; import { SharedFormPage } from '../../../pages/SharedForm';
import setup from '../../setup'; import setup from '../../../setup';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import { AccountLicensePage } from '../../pages/Account/License'; import { AccountLicensePage } from '../../../pages/Account/License';
test.describe('Attachment column', () => { test.describe('Attachment column', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

6
tests/playwright/tests/db/columnBarcode.spec.ts → tests/playwright/tests/db/columns/columnBarcode.spec.ts

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
interface ExpectedBarcodeData { interface ExpectedBarcodeData {
referencedValue: string; referencedValue: string;

6
tests/playwright/tests/db/columnCheckbox.spec.ts → tests/playwright/tests/db/columns/columnCheckbox.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { Api } from 'nocodb-sdk'; import { Api } from 'nocodb-sdk';
let api: Api<any>; let api: Api<any>;

4
tests/playwright/tests/db/columnDateTime.spec.ts → tests/playwright/tests/db/columns/columnDateTime.spec.ts

@ -1,6 +1,6 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
const dateTimeData = [ const dateTimeData = [
{ {

4
tests/playwright/tests/db/columnDuration.spec.ts → tests/playwright/tests/db/columns/columnDuration.spec.ts

@ -1,6 +1,6 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
// Storing one additional dummy value "10" at end of every input array // Storing one additional dummy value "10" at end of every input array
// this will trigger update to previously committed data // this will trigger update to previously committed data

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

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup, { NcContext } from '../../setup'; import setup, { NcContext } from '../../../setup';
import { isPg, isSqlite } from '../../setup/db'; import { isPg, isSqlite } from '../../../setup/db';
// Add formula to be verified here & store expected results for 5 rows // Add formula to be verified here & store expected results for 5 rows
// Column data from City table (Sakila DB) // Column data from City table (Sakila DB)

6
tests/playwright/tests/db/columnGeoData.spec.ts → tests/playwright/tests/db/columns/columnGeoData.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
test.describe('Geo Data column', () => { test.describe('Geo Data column', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

4
tests/playwright/tests/db/columnLinkToAnotherRecord.spec.ts → tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts

@ -1,6 +1,6 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('LTAR create & update', () => { test.describe('LTAR create & update', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

4
tests/playwright/tests/db/columnLookupRollup.spec.ts → tests/playwright/tests/db/columns/columnLookupRollup.spec.ts

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

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

@ -1,9 +1,9 @@
import { expect, Locator, test } from '@playwright/test'; import { expect, Locator, test } from '@playwright/test';
import setup from '../../setup'; import setup from '../../../setup';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import { getTextExcludeIconText } from '../utils/general'; import { getTextExcludeIconText } from '../../utils/general';
let api: Api<any>; let api: Api<any>;
const recordCount = 10; const recordCount = 10;

4
tests/playwright/tests/db/columnMenuOperations.spec.ts → tests/playwright/tests/db/columns/columnMenuOperations.spec.ts

@ -1,6 +1,6 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
const columns = [ const columns = [
{ {

8
tests/playwright/tests/db/columnMultiSelect.spec.ts → tests/playwright/tests/db/columns/columnMultiSelect.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
test.describe('Multi select', () => { test.describe('Multi select', () => {
let dashboard: DashboardPage, grid: GridPage; let dashboard: DashboardPage, grid: GridPage;

6
tests/playwright/tests/db/columnQrCode.spec.ts → tests/playwright/tests/db/columns/columnQrCode.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
type ExpectedQrCodeData = { type ExpectedQrCodeData = {
referencedValue: string; referencedValue: string;

6
tests/playwright/tests/db/columnRating.spec.ts → tests/playwright/tests/db/columns/columnRating.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
test.describe('Rating - cell, filter, sort', () => { test.describe('Rating - cell, filter, sort', () => {
let dashboard: DashboardPage, toolbar: ToolbarPage; let dashboard: DashboardPage, toolbar: ToolbarPage;

4
tests/playwright/tests/db/columnRelationalExtendedTests.spec.ts → tests/playwright/tests/db/columns/columnRelationalExtendedTests.spec.ts

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

8
tests/playwright/tests/db/columnSingleSelect.spec.ts → tests/playwright/tests/db/columns/columnSingleSelect.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
test.describe('Single select', () => { test.describe('Single select', () => {
let dashboard: DashboardPage, grid: GridPage; let dashboard: DashboardPage, grid: GridPage;

12
tests/playwright/tests/db/baseShare.spec.ts → tests/playwright/tests/db/features/baseShare.spec.ts

@ -1,10 +1,10 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { LoginPage } from '../../pages/LoginPage'; import { LoginPage } from '../../../pages/LoginPage';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { getDefaultPwd } from '../utils/general'; import { getDefaultPwd } from '../../utils/general';
test.describe('Shared base', () => { test.describe('Shared base', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

14
tests/playwright/tests/db/erd.spec.ts → tests/playwright/tests/db/features/erd.spec.ts

@ -5,13 +5,13 @@ import {
pgSakilaSqlViews, pgSakilaSqlViews,
pgSakilaTables, pgSakilaTables,
sqliteSakilaSqlViews, sqliteSakilaSqlViews,
} from '../utils/sakila'; } from '../../utils/sakila';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { SettingsSubTab, SettingTab } from '../../pages/Dashboard/Settings'; import { SettingsSubTab, SettingTab } from '../../../pages/Dashboard/Settings';
import setup from '../../setup'; import setup from '../../../setup';
import { isMysql, isPg, isSqlite } from '../../setup/db'; import { isMysql, isPg, isSqlite } from '../../../setup/db';
import { SettingsErdPage } from '../../pages/Dashboard/Settings/Erd'; import { SettingsErdPage } from '../../../pages/Dashboard/Settings/Erd';
import { defaultBaseName } from '../../constants'; import { defaultBaseName } from '../../../constants';
test.describe('Erd', () => { test.describe('Erd', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

10
tests/playwright/tests/db/expandedFormUrl.spec.ts → tests/playwright/tests/db/features/expandedFormUrl.spec.ts

@ -1,9 +1,9 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GalleryPage } from '../../pages/Dashboard/Gallery'; import { GalleryPage } from '../../../pages/Dashboard/Gallery';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
test.describe('Expanded form URL', () => { test.describe('Expanded form URL', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

12
tests/playwright/tests/db/filters.spec.ts → tests/playwright/tests/db/features/filters.spec.ts

@ -1,13 +1,13 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { Api } from 'nocodb-sdk'; import { Api } from 'nocodb-sdk';
import { rowMixedValue } from '../../setup/xcdb-records'; import { rowMixedValue } from '../../../setup/xcdb-records';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { createDemoTable } from '../../setup/demoTable'; import { createDemoTable } from '../../../setup/demoTable';
import { isPg } from '../../setup/db'; import { isPg } from '../../../setup/db';
let dashboard: DashboardPage, toolbar: ToolbarPage; let dashboard: DashboardPage, toolbar: ToolbarPage;
let context: any; let context: any;

8
tests/playwright/tests/db/findRowByScanner.spec.ts → tests/playwright/tests/db/features/findRowByScanner.spec.ts

@ -1,8 +1,8 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { FormPage } from '../../pages/Dashboard/Form'; import { FormPage } from '../../../pages/Dashboard/Form';
import setup from '../../setup'; import setup from '../../../setup';
// Skip for now as it is not working in CI atm // Skip for now as it is not working in CI atm
test.describe.skip('Find row by scanner', () => { test.describe.skip('Find row by scanner', () => {

10
tests/playwright/tests/db/import.spec.ts → tests/playwright/tests/db/features/import.spec.ts

@ -1,9 +1,9 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { airtableApiBase, airtableApiKey } from '../../constants'; import { airtableApiBase, airtableApiKey } from '../../../constants';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { quickVerify } from '../../quickTests/commonTest'; import { quickVerify } from '../../../quickTests/commonTest';
import setup from '../../setup'; import setup from '../../../setup';
import { isPg, isSqlite } from '../../setup/db'; import { isPg, isSqlite } from '../../../setup/db';
test.describe('Import', () => { test.describe('Import', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

6
tests/playwright/tests/db/keyboardShortcuts.spec.ts → tests/playwright/tests/db/features/keyboardShortcuts.spec.ts

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
let api: Api<any>; let api: Api<any>;

8
tests/playwright/tests/db/language.spec.ts → tests/playwright/tests/db/features/language.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import setup from '../../setup'; import setup from '../../../setup';
const langMenu = [ const langMenu = [
'help-translate', 'help-translate',
@ -58,7 +58,7 @@ test.describe('Common', () => {
// Index is the order in which menu options appear // Index is the order in which menu options appear
for (let i = 1; i < langMenu.length; i++) { for (let i = 1; i < langMenu.length; i++) {
// scripts/playwright/tests/language.spec.ts // scripts/playwright/tests/language.spec.ts
const json = require(`../../../../packages/nc-gui/lang/${langMenu[i]}`); const json = require(`../../../../../packages/nc-gui/lang/${langMenu[i]}`);
await projectsPage.openLanguageMenu(); await projectsPage.openLanguageMenu();
await projectsPage.selectLanguage({ index: i }); await projectsPage.selectLanguage({ index: i });
await projectsPage.verifyLanguage({ json }); await projectsPage.verifyLanguage({ json });

11
tests/playwright/tests/db/metaLTAR.spec.ts → tests/playwright/tests/db/features/metaLTAR.spec.ts

@ -20,13 +20,12 @@
*/ */
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import setup from '../../setup'; import setup from '../../../setup';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import { createXcdb, deleteXcdb } from '../../setup/xcdbProject'; import { createXcdb, deleteXcdb } from '../../../setup/xcdbProject';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { isSqlite } from '../../setup/db';
let api: Api<any>; let api: Api<any>;
const recordCount = 10; const recordCount = 10;

8
tests/playwright/tests/db/metaSync.spec.ts → tests/playwright/tests/db/features/metaSync.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { SettingsPage, SettingTab } from '../../pages/Dashboard/Settings'; import { SettingsPage, SettingTab } from '../../../pages/Dashboard/Settings';
import setup, { NcContext } from '../../setup'; import setup, { NcContext } from '../../../setup';
import { isMysql, isPg, isSqlite, mysqlExec, pgExec, sqliteExec } from '../../setup/db'; import { isMysql, isPg, isSqlite, mysqlExec, pgExec, sqliteExec } from '../../../setup/db';
test.describe('Meta sync', () => { test.describe('Meta sync', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

8
tests/playwright/tests/db/mobileMode.spec.ts → tests/playwright/tests/db/features/mobileMode.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { FormPage } from '../../pages/Dashboard/Form'; import { FormPage } from '../../../pages/Dashboard/Form';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Mobile Mode', () => { test.describe('Mobile Mode', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

4
tests/playwright/tests/db/pagination.spec.ts → tests/playwright/tests/db/features/pagination.spec.ts

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

6
tests/playwright/tests/db/swagger.spec.ts → tests/playwright/tests/db/features/swagger.spec.ts

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Table Column Operations', () => { test.describe('Table Column Operations', () => {
let grid: GridPage, dashboard: DashboardPage; let grid: GridPage, dashboard: DashboardPage;

26
tests/playwright/tests/db/timezone.spec.ts → tests/playwright/tests/db/features/timezone.spec.ts

@ -1,12 +1,12 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { knex } from 'knex'; import { knex } from 'knex';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { isMysql, isPg, isSqlite } from '../../setup/db'; import { isMysql, isPg, isSqlite } from '../../../setup/db';
import { getKnexConfig } from '../utils/config'; import { getKnexConfig } from '../../utils/config';
import { getBrowserTimezoneOffset } from '../utils/general'; import { getBrowserTimezoneOffset } from '../../utils/general';
let api: Api<any>, records: any[]; let api: Api<any>, records: any[];
const columns = [ const columns = [
@ -107,7 +107,6 @@ test.describe.serial('Timezone-XCDB : Japan/Tokyo', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true }); context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project); dashboard = new DashboardPage(page, context.project);
if (!isSqlite(context)) return;
try { try {
const { project, table } = await timezoneSuite(context.token, 'xcdb0'); const { project, table } = await timezoneSuite(context.token, 'xcdb0');
@ -140,8 +139,6 @@ test.describe.serial('Timezone-XCDB : Japan/Tokyo', () => {
* Display value is converted to Asia/Tokyo * Display value is converted to Asia/Tokyo
*/ */
test('API insert, verify display value', async () => { test('API insert, verify display value', async () => {
if (!isSqlite(context)) return;
await dashboard.clickHome(); await dashboard.clickHome();
const projectsPage = new ProjectsPage(dashboard.rootPage); const projectsPage = new ProjectsPage(dashboard.rootPage);
await projectsPage.openProject({ title: 'xcdb0', withoutPrefix: true }); await projectsPage.openProject({ title: 'xcdb0', withoutPrefix: true });
@ -179,8 +176,6 @@ test.describe.serial('Timezone-XCDB : Japan/Tokyo', () => {
*/ */
test('API Insert, verify API read response', async () => { test('API Insert, verify API read response', async () => {
if (!isSqlite(context)) return;
const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`); const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// translate dateInserted to UTC in YYYY-MM-DD HH:mm format // translate dateInserted to UTC in YYYY-MM-DD HH:mm format
const dateInsertedInUTC = dateInserted.toISOString().replace('T', ' ').replace('Z', ''); const dateInsertedInUTC = dateInserted.toISOString().replace('T', ' ').replace('Z', '');
@ -282,9 +277,6 @@ test.describe.serial('Timezone-XCDB : Asia/Hong-kong', () => {
context = await setup({ page, isEmptyProject: true }); context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project); dashboard = new DashboardPage(page, context.project);
// Apply only for sqlite, as currently- root DB for all instances is SQLite
if (!isSqlite(context)) return;
const { project } = await timezoneSuite(context.token, 'xcdb2', true); const { project } = await timezoneSuite(context.token, 'xcdb2', true);
context.project = project; context.project = project;
@ -323,8 +315,6 @@ test.describe.serial('Timezone-XCDB : Asia/Hong-kong', () => {
* *
*/ */
test('Cell insert', async () => { test('Cell insert', async () => {
if (!isSqlite(context)) return;
// Verify stored value in database is UTC // Verify stored value in database is UTC
records = await api.dbTableRow.list('noco', context.project.id, 'dateTimeTable', { limit: 10 }); records = await api.dbTableRow.list('noco', context.project.id, 'dateTimeTable', { limit: 10 });
@ -352,8 +342,6 @@ test.describe.serial('Timezone-XCDB : Asia/Hong-kong', () => {
* *
*/ */
test('Expanded record insert', async () => { test('Expanded record insert', async () => {
if (!isSqlite(context)) return;
await dashboard.grid.openExpandedRow({ index: 0 }); await dashboard.grid.openExpandedRow({ index: 0 });
await dashboard.expandedForm.fillField({ await dashboard.expandedForm.fillField({
columnTitle: 'DateTime', columnTitle: 'DateTime',
@ -387,8 +375,6 @@ test.describe.serial('Timezone-XCDB : Asia/Hong-kong', () => {
* *
*/ */
test('Copy paste', async () => { test('Copy paste', async () => {
if (!isSqlite(context)) return;
await dashboard.grid.addNewRow({ index: 1, columnHeader: 'Title', value: 'Copy paste test' }); await dashboard.grid.addNewRow({ index: 1, columnHeader: 'Title', value: 'Copy paste test' });
await dashboard.rootPage.reload(); await dashboard.rootPage.reload();

12
tests/playwright/tests/db/undo-redo.spec.ts → tests/playwright/tests/db/features/undo-redo.spec.ts

@ -1,11 +1,11 @@
import { expect, Page, test } from '@playwright/test'; import { expect, Page, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
import { rowMixedValue } from '../../setup/xcdb-records'; import { rowMixedValue } from '../../../setup/xcdb-records';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { isSqlite } from '../../setup/db'; import { isSqlite } from '../../../setup/db';
let dashboard: DashboardPage, let dashboard: DashboardPage,
grid: GridPage, grid: GridPage,

12
tests/playwright/tests/db/updateBulk.ts → tests/playwright/tests/db/features/updateBulk.ts

@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import setup from '../../setup'; import setup from '../../../setup';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { Api } from 'nocodb-sdk'; import { Api } from 'nocodb-sdk';
import { createDemoTable } from '../../setup/demoTable'; import { createDemoTable } from '../../../setup/demoTable';
import { BulkUpdatePage } from '../../pages/Dashboard/BulkUpdate'; import { BulkUpdatePage } from '../../../pages/Dashboard/BulkUpdate';
import { AccountLicensePage } from '../../pages/Account/License'; import { AccountLicensePage } from '../../../pages/Account/License';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
let bulkUpdateForm: BulkUpdatePage; let bulkUpdateForm: BulkUpdatePage;
let dashboard: DashboardPage; let dashboard: DashboardPage;

258
tests/playwright/tests/db/features/verticalFillHandle.spec.ts

@ -0,0 +1,258 @@
import { expect, test } from '@playwright/test';
import setup from '../../../setup';
import { DashboardPage } from '../../../pages/Dashboard';
import { Api } from 'nocodb-sdk';
import { createDemoTable } from '../../../setup/demoTable';
import { BulkUpdatePage } from '../../../pages/Dashboard/BulkUpdate';
let dashboard: DashboardPage;
let context: any;
let api: Api<any>;
let table;
async function dragDrop({ firstColumn, lastColumn }: { firstColumn: string; lastColumn: string }) {
await dashboard.grid.cell.get({ index: 0, columnHeader: firstColumn }).click();
await dashboard.rootPage.keyboard.press(
(await dashboard.grid.isMacOs()) ? 'Meta+Shift+ArrowRight' : 'Control+Shift+ArrowRight'
);
// get fill handle locator
const src = await dashboard.rootPage.locator(`.nc-fill-handle`);
const dst = await dashboard.grid.cell.get({ index: 3, columnHeader: lastColumn });
// drag and drop
await src.dragTo(dst);
}
async function beforeEachInit({ page, tableType }: { page: any; tableType: string }) {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project);
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
table = await createDemoTable({ context, type: tableType, recordCnt: 10 });
await page.reload();
await dashboard.treeView.openTable({ title: tableType });
}
test.describe('Fill Handle', () => {
test.beforeEach(async ({ page }) => {
await beforeEachInit({ page, tableType: 'textBased' });
});
test('Text based', async () => {
const fields = [
{ title: 'SingleLineText', value: 'Afghanistan', type: 'text' },
{ title: 'Email', value: 'jbutt@gmail.com', type: 'text' },
{ title: 'PhoneNumber', value: '1-541-754-3010', type: 'text' },
{ title: 'URL', value: 'https://www.google.com', type: 'text' },
{ title: 'MultiLineText', value: 'Aberdeen, United Kingdom', type: 'longText' },
];
await dragDrop({ firstColumn: 'SingleLineText', lastColumn: 'URL' });
// verify data on grid (verifying just two rows)
for (let i = 0; i < fields.length; i++) {
for (let j = 0; j < 4; j++) {
await dashboard.grid.cell.verify({ index: j, columnHeader: fields[i].title, value: fields[i].value });
}
}
// verify api response
const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 4 })).list;
for (let i = 0; i < updatedRecords.length; i++) {
for (let j = 0; j < fields.length; j++) {
expect(updatedRecords[i][fields[j].title]).toEqual(fields[j].value);
}
}
});
});
test.describe('Fill Handle', () => {
test.beforeEach(async ({ page }) => {
await beforeEachInit({ page, tableType: 'numberBased' });
});
test('Number based', async () => {
const fields = [
{ title: 'Number', value: '33', type: 'text' },
{ title: 'Decimal', value: '33.3', type: 'text' },
{ title: 'Currency', value: '33.30', type: 'text' },
{ title: 'Percent', value: '33', type: 'text' },
{ title: 'Duration', value: '00:01', type: 'text' },
{ title: 'Rating', value: '3', type: 'rating' },
{ title: 'Year', value: '2023', type: 'year' },
{ title: 'Time', value: '02:02', type: 'time' },
];
// kludge: insert time from browser until mysql issue with timezone is fixed
await dashboard.grid.cell.time.set({ index: 0, columnHeader: 'Time', value: '02:02' });
// set rating for first record
await dashboard.grid.cell.rating.select({ index: 0, columnHeader: 'Rating', rating: 2 });
await dragDrop({ firstColumn: 'Number', lastColumn: 'Time' });
// verify data on grid
for (let i = 0; i < fields.length; i++) {
for (let j = 0; j < 4; j++) {
if (fields[i].type === 'rating') {
await dashboard.grid.cell.rating.verify({
index: j,
columnHeader: fields[i].title,
rating: +fields[i].value,
});
} else if (fields[i].type === 'year') {
await dashboard.grid.cell.year.verify({ index: j, columnHeader: fields[i].title, value: +fields[i].value });
} else if (fields[i].type === 'time') {
await dashboard.grid.cell.time.verify({ index: j, columnHeader: fields[i].title, value: fields[i].value });
} else {
await dashboard.grid.cell.verify({ index: j, columnHeader: fields[i].title, value: fields[i].value });
}
}
}
// verify api response
// duration in seconds
const APIResponse = [33, 33.3, 33.3, 33, 60, 3, 2023, '02:02:00'];
const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 4 })).list;
for (let i = 0; i < updatedRecords.length; i++) {
for (let j = 0; j < fields.length; j++) {
if (fields[j].title === 'Time') {
expect(updatedRecords[i][fields[j].title]).toContain(APIResponse[j]);
} else {
expect(+updatedRecords[i][fields[j].title]).toEqual(APIResponse[j]);
}
}
}
});
});
test.describe('Fill Handle', () => {
test.beforeEach(async ({ page }) => {
await beforeEachInit({ page, tableType: 'selectBased' });
});
test('Select based', async () => {
const fields = [
{ title: 'SingleSelect', value: 'jan', type: 'singleSelect' },
{ title: 'MultiSelect', value: 'jan,feb,mar', type: 'multiSelect' },
];
await dragDrop({ firstColumn: 'SingleSelect', lastColumn: 'MultiSelect' });
// verify data on grid
const displayOptions = ['jan', 'feb', 'mar'];
for (let i = 0; i < fields.length; i++) {
for (let j = 0; j < 4; j++) {
if (fields[i].type === 'singleSelect') {
await dashboard.grid.cell.selectOption.verify({
index: j,
columnHeader: fields[i].title,
option: fields[i].value,
});
} else {
await dashboard.grid.cell.selectOption.verifyOptions({
index: j,
columnHeader: fields[i].title,
options: displayOptions,
});
}
}
}
// verify api response
const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 4 })).list;
for (let i = 0; i < updatedRecords.length; i++) {
for (let j = 0; j < fields.length; j++) {
expect(updatedRecords[i][fields[j].title]).toContain(fields[j].value);
}
}
});
});
test.describe('Fill Handle', () => {
test.beforeEach(async ({ page }) => {
await beforeEachInit({ page, tableType: 'miscellaneous' });
});
test('Miscellaneous (Checkbox, attachment)', async () => {
const fields = [
{ title: 'Checkbox', value: 'true', type: 'checkbox' },
{ title: 'Attachment', value: `${process.cwd()}/fixtures/sampleFiles/1.json`, type: 'attachment' },
];
await dashboard.grid.cell.checkbox.click({ index: 0, columnHeader: 'Checkbox' });
const filepath = [`${process.cwd()}/fixtures/sampleFiles/1.json`];
await dashboard.grid.cell.attachment.addFile({
index: 0,
columnHeader: 'Attachment',
filePath: filepath,
});
await dragDrop({ firstColumn: 'Checkbox', lastColumn: 'Attachment' });
// verify data on grid
for (let i = 0; i < fields.length; i++) {
for (let j = 0; j < 4; j++) {
if (fields[i].type === 'checkbox') {
await dashboard.grid.cell.checkbox.verifyChecked({
index: j,
columnHeader: fields[i].title,
});
} else {
await dashboard.grid.cell.attachment.verifyFileCount({
index: j,
columnHeader: fields[i].title,
count: 1,
});
}
}
}
// verify api response
const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 4 })).list;
for (let i = 0; i < updatedRecords.length; i++) {
for (let j = 0; j < fields.length; j++) {
expect(+updatedRecords[i]['Checkbox']).toBe(1);
expect(updatedRecords[i]['Attachment'][0].title).toBe('1.json');
expect(updatedRecords[i]['Attachment'][0].mimetype).toBe('application/json');
}
}
});
});
test.describe('Fill Handle', () => {
test.beforeEach(async ({ page }) => {
await beforeEachInit({ page, tableType: 'dateTimeBased' });
});
test('Date Time Based', async () => {
const row0_date = await api.dbTableRow.read('noco', context.project.id, table.id, 1);
const fields = [{ title: 'Date', value: row0_date['Date'], type: 'date' }];
await dragDrop({ firstColumn: 'Date', lastColumn: 'Date' });
// verify data on grid
for (let i = 0; i < fields.length; i++) {
for (let j = 0; j < 4; j++) {
await dashboard.grid.cell.date.verify({
index: j,
columnHeader: fields[i].title,
date: fields[i].value,
});
}
}
// verify api response
const updatedRecords = (await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 4 })).list;
for (let i = 0; i < updatedRecords.length; i++) {
for (let j = 0; j < fields.length; j++) {
expect(updatedRecords[i]['Date']).toBe(fields[j].value);
}
}
});
});

12
tests/playwright/tests/db/01-webhook.spec.ts → tests/playwright/tests/db/features/webhook.spec.ts

@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import makeServer from '../../setup/server'; import makeServer from '../../../setup/server';
import { WebhookFormPage } from '../../pages/Dashboard/WebhookForm'; import { WebhookFormPage } from '../../../pages/Dashboard/WebhookForm';
import { isSubset } from '../utils/general'; import { isSubset } from '../../utils/general';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
import { isMysql, isPg, isSqlite } from '../../setup/db'; import { isMysql, isPg, isSqlite } from '../../../setup/db';
const hookPath = 'http://localhost:9090/hook'; const hookPath = 'http://localhost:9090/hook';
let api: Api<any>; let api: Api<any>;

6
tests/playwright/tests/db/cellSelection.spec.ts → tests/playwright/tests/db/general/cellSelection.spec.ts

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Verify cell selection', () => { test.describe('Verify cell selection', () => {
let dashboard: DashboardPage, grid: GridPage; let dashboard: DashboardPage, grid: GridPage;

2
tests/playwright/tests/db/megaTable.spec.ts → tests/playwright/tests/db/general/megaTable.spec.ts

@ -1,5 +1,5 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import setup from '../../setup'; import setup from '../../../setup';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { Api } from 'nocodb-sdk'; import { Api } from 'nocodb-sdk';
let api: Api<any>; let api: Api<any>;

16
tests/playwright/tests/db/projectOperations.spec.ts → tests/playwright/tests/db/general/projectOperations.spec.ts

@ -1,13 +1,13 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { airtableApiBase, airtableApiKey } from '../../constants'; import { airtableApiBase, airtableApiKey } from '../../../constants';
import { quickVerify } from '../../quickTests/commonTest'; import { quickVerify } from '../../../quickTests/commonTest';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { Api } from 'nocodb-sdk'; import { Api } from 'nocodb-sdk';
import { ProjectInfo, ProjectInfoApiUtil } from '../utils/projectInfoApiUtil'; import { ProjectInfo, ProjectInfoApiUtil } from '../../utils/projectInfoApiUtil';
import { deepCompare } from '../utils/objectCompareUtil'; import { deepCompare } from '../../utils/objectCompareUtil';
test.describe('Project operations', () => { test.describe('Project operations', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

6
tests/playwright/tests/db/tableColumnOperation.spec.ts → tests/playwright/tests/db/general/tableColumnOperation.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid'; import { GridPage } from '../../../pages/Dashboard/Grid';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Table Column Operations', () => { test.describe('Table Column Operations', () => {
let grid: GridPage, dashboard: DashboardPage; let grid: GridPage, dashboard: DashboardPage;

10
tests/playwright/tests/db/tableOperations.spec.ts → tests/playwright/tests/db/general/tableOperations.spec.ts

@ -1,10 +1,10 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Api, TableListType, TableType } from 'nocodb-sdk'; import { Api, TableListType, TableType } from 'nocodb-sdk';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { SettingsPage, SettingTab } from '../../pages/Dashboard/Settings'; import { SettingsPage, SettingTab } from '../../../pages/Dashboard/Settings';
import { deepCompare } from '../utils/objectCompareUtil'; import { deepCompare } from '../../utils/objectCompareUtil';
import setup from '../../setup'; import setup from '../../../setup';
import { ProjectInfoApiUtil, TableInfo } from '../utils/projectInfoApiUtil'; import { ProjectInfoApiUtil, TableInfo } from '../../utils/projectInfoApiUtil';
test.describe('Table Operations', () => { test.describe('Table Operations', () => {
let dashboard: DashboardPage, settings: SettingsPage; let dashboard: DashboardPage, settings: SettingsPage;

6
tests/playwright/tests/db/toolbarOperations.spec.ts → tests/playwright/tests/db/general/toolbarOperations.spec.ts

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Toolbar operations (GRID)', () => { test.describe('Toolbar operations (GRID)', () => {
let dashboard: DashboardPage, toolbar: ToolbarPage; let dashboard: DashboardPage, toolbar: ToolbarPage;

6
tests/playwright/tests/db/viewMenu.spec.ts → tests/playwright/tests/db/general/viewMenu.spec.ts

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

6
tests/playwright/tests/db/views.spec.ts → tests/playwright/tests/db/general/views.spec.ts

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Views CRUD Operations', () => { test.describe('Views CRUD Operations', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

8
tests/playwright/tests/db/accountLicense.spec.ts → tests/playwright/tests/db/users&Accounts/accountLicense.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import setup from '../../setup'; import setup from '../../../setup';
import { AccountLicensePage } from '../../pages/Account/License'; import { AccountLicensePage } from '../../../pages/Account/License';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
test.describe('Enterprise License', () => { test.describe('Enterprise License', () => {
// @ts-ignore // @ts-ignore

6
tests/playwright/tests/db/accountTokenManagement.spec.ts → tests/playwright/tests/db/users&Accounts/accountTokenManagement.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import { AccountTokenPage } from '../../pages/Account/Token'; import { AccountTokenPage } from '../../../pages/Account/Token';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('User roles', () => { test.describe('User roles', () => {
let accountTokenPage: AccountTokenPage; let accountTokenPage: AccountTokenPage;

12
tests/playwright/tests/db/accountUserManagement.spec.ts → tests/playwright/tests/db/users&Accounts/accountUserManagement.spec.ts

@ -1,10 +1,10 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import { AccountUsersPage } from '../../pages/Account/Users'; import { AccountUsersPage } from '../../../pages/Account/Users';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { SignupPage } from '../../pages/SignupPage'; import { SignupPage } from '../../../pages/SignupPage';
import setup from '../../setup'; import setup from '../../../setup';
import { getDefaultPwd } from '../utils/general'; import { getDefaultPwd } from '../../utils/general';
const roleDb = [ const roleDb = [
{ email: 'creator@nocodb.com', role: 'Organization Level Creator', url: '' }, { email: 'creator@nocodb.com', role: 'Organization Level Creator', url: '' },

10
tests/playwright/tests/db/accountUserSettings.spec.ts → tests/playwright/tests/db/users&Accounts/accountUserSettings.spec.ts

@ -1,9 +1,9 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import { AccountSettingsPage } from '../../pages/Account/Settings'; import { AccountSettingsPage } from '../../../pages/Account/Settings';
import { SignupPage } from '../../pages/SignupPage'; import { SignupPage } from '../../../pages/SignupPage';
import setup from '../../setup'; import setup from '../../../setup';
import { getDefaultPwd } from '../utils/general'; import { getDefaultPwd } from '../../utils/general';
test.describe('App settings', () => { test.describe('App settings', () => {
let accountSettingsPage: AccountSettingsPage; let accountSettingsPage: AccountSettingsPage;

16
tests/playwright/tests/db/authChangePassword.spec.ts → tests/playwright/tests/db/users&Accounts/authChangePassword.spec.ts

@ -1,12 +1,12 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { LoginPage } from '../../pages/LoginPage'; import { LoginPage } from '../../../pages/LoginPage';
import { SettingsPage, SettingTab } from '../../pages/Dashboard/Settings'; import { SettingsPage, SettingTab } from '../../../pages/Dashboard/Settings';
import { SignupPage } from '../../pages/SignupPage'; import { SignupPage } from '../../../pages/SignupPage';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import { getDefaultPwd } from '../utils/general'; import { getDefaultPwd } from '../../utils/general';
test.describe('Auth', () => { test.describe('Auth', () => {
let context: any; let context: any;

12
tests/playwright/tests/db/rolesCreate.spec.ts → tests/playwright/tests/db/users&Accounts/rolesCreate.spec.ts

@ -1,10 +1,10 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { SettingsPage, SettingTab } from '../../pages/Dashboard/Settings'; import { SettingsPage, SettingTab } from '../../../pages/Dashboard/Settings';
import { SignupPage } from '../../pages/SignupPage'; import { SignupPage } from '../../../pages/SignupPage';
import { ProjectsPage } from '../../pages/ProjectsPage'; import { ProjectsPage } from '../../../pages/ProjectsPage';
import { getDefaultPwd } from '../utils/general'; import { getDefaultPwd } from '../../utils/general';
const roleDb = [ const roleDb = [
{ email: 'creator@nocodb.com', role: 'creator', url: '' }, { email: 'creator@nocodb.com', role: 'creator', url: '' },

8
tests/playwright/tests/db/rolesPreview.spec.ts → tests/playwright/tests/db/users&Accounts/rolesPreview.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import { SettingsPage, SettingTab } from '../../pages/Dashboard/Settings'; import { SettingsPage, SettingTab } from '../../../pages/Dashboard/Settings';
const roles = ['Editor', 'Commenter', 'Viewer']; const roles = ['Editor', 'Commenter', 'Viewer'];

4
tests/playwright/tests/db/rolesSuperUser.spec.ts → tests/playwright/tests/db/users&Accounts/rolesSuperUser.spec.ts

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

12
tests/playwright/tests/db/viewForm.spec.ts → tests/playwright/tests/db/views/viewForm.spec.ts

@ -1,10 +1,10 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import setup from '../../setup'; import setup from '../../../setup';
import { FormPage } from '../../pages/Dashboard/Form'; import { FormPage } from '../../../pages/Dashboard/Form';
import { SharedFormPage } from '../../pages/SharedForm'; import { SharedFormPage } from '../../../pages/SharedForm';
import { AccountPage } from '../../pages/Account'; import { AccountPage } from '../../../pages/Account';
import { AccountAppStorePage } from '../../pages/Account/AppStore'; import { AccountAppStorePage } from '../../../pages/Account/AppStore';
import { Api, UITypes } from 'nocodb-sdk'; import { Api, UITypes } from 'nocodb-sdk';
let api: Api<any>; let api: Api<any>;

6
tests/playwright/tests/db/viewFormShareSurvey.spec.ts → tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

@ -1,7 +1,7 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { SurveyFormPage } from '../../pages/Dashboard/SurveyForm'; import { SurveyFormPage } from '../../../pages/Dashboard/SurveyForm';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Share form', () => { test.describe('Share form', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;

6
tests/playwright/tests/db/viewGridShare.spec.ts → tests/playwright/tests/db/views/viewGridShare.spec.ts

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

8
tests/playwright/tests/db/viewKanban.spec.ts → tests/playwright/tests/db/views/viewKanban.spec.ts

@ -1,9 +1,9 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import setup from '../../setup'; import setup from '../../../setup';
import { isPg, isSqlite } from '../../setup/db'; import { isPg, isSqlite } from '../../../setup/db';
const filmRatings = ['G', 'PG', 'PG-13', 'R', 'NC-17']; const filmRatings = ['G', 'PG', 'PG-13', 'R', 'NC-17'];

6
tests/playwright/tests/db/viewMap.spec.ts → tests/playwright/tests/db/views/viewMap.spec.ts

@ -1,8 +1,8 @@
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/Dashboard'; import { DashboardPage } from '../../../pages/Dashboard';
import { ToolbarPage } from '../../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar';
import setup from '../../setup'; import setup from '../../../setup';
test.describe('Map View', () => { test.describe('Map View', () => {
let dashboard: DashboardPage, toolbar: ToolbarPage; let dashboard: DashboardPage, toolbar: ToolbarPage;
Loading…
Cancel
Save