Browse Source

Merge branch 'develop' into refactor/new-data-apis

# Conflicts:
#	tests/playwright/tests/db/features/verticalFillHandle.spec.ts
pull/6470/head
Pranav C 1 year ago
parent
commit
21f3f3d5a4
  1. 10
      packages/nc-gui/components.d.ts
  2. 22
      packages/nc-gui/components/cell/Checkbox.vue
  3. 1
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  4. 14
      packages/nc-gui/components/general/ColorPicker.vue
  5. 9
      packages/nc-gui/components/general/DeleteModal.vue
  6. 2
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  7. 5
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  8. 2
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  9. 51
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  10. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  11. 10
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  12. 54
      packages/nc-gui/composables/useViewFilters.ts
  13. 2
      packages/nocodb/package.json
  14. 6
      packages/nocodb/src/db/conditionV2.ts
  15. 27
      pnpm-lock.yaml
  16. 3
      tests/playwright/package.json
  17. 7
      tests/playwright/pages/Account/Users.ts
  18. 32
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  19. 42
      tests/playwright/pages/Dashboard/Form/index.ts
  20. 11
      tests/playwright/pages/Dashboard/Grid/Group.ts
  21. 11
      tests/playwright/pages/Dashboard/Grid/index.ts
  22. 17
      tests/playwright/pages/Dashboard/TreeView.ts
  23. 6
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts
  24. 21
      tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
  25. 3
      tests/playwright/pages/Dashboard/common/Cell/CheckboxCell.ts
  26. 3
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  27. 3
      tests/playwright/pages/Dashboard/index.ts
  28. 2
      tests/playwright/tests/db/features/expandedFormUrl.spec.ts
  29. 3
      tests/playwright/tests/db/features/metaLTAR.spec.ts
  30. 6
      tests/playwright/tests/db/features/verticalFillHandle.spec.ts
  31. 2
      tests/playwright/tests/db/general/projectOperations.spec.ts
  32. 9
      tests/playwright/tests/db/usersAccounts/accountUserManagement.spec.ts

10
packages/nc-gui/components.d.ts vendored

@ -52,7 +52,6 @@ declare module '@vue/runtime-core' {
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row']
@ -81,7 +80,6 @@ declare module '@vue/runtime-core' {
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
Icon: typeof import('~icons/ic/on')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
@ -105,12 +103,9 @@ declare module '@vue/runtime-core' {
MaterialSymbolsVisibility: typeof import('~icons/material-symbols/visibility')['default']
MaterialSymbolsVisibilityOff: typeof import('~icons/material-symbols/visibility-off')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccordionUp: typeof import('~icons/mdi/accordion-up')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircleOutline: typeof import('~icons/mdi/account-circle-outline')['default']
MdiAccountCircleOutlines: typeof import('~icons/mdi/account-circle-outlines')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAppleKeyboardShift: typeof import('~icons/mdi/apple-keyboard-shift')['default']
MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
@ -121,9 +116,7 @@ declare module '@vue/runtime-core' {
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiChevronUp: typeof import('~icons/mdi/chevron-up')['default']
@ -151,7 +144,6 @@ declare module '@vue/runtime-core' {
MdiMessageOutline: typeof import('~icons/mdi/message-outline')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiMoreVert: typeof import('~icons/mdi/more-vert')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
@ -168,9 +160,7 @@ declare module '@vue/runtime-core' {
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
NcIconsInbox: typeof import('~icons/nc-icons/inbox')['default']
PhLink: typeof import('~icons/ph/link')['default']
PhMagnifyingGlassBold: typeof import('~icons/ph/magnifying-glass-bold')['default']
PhTriangleFill: typeof import('~icons/ph/triangle-fill')['default']
RiExternalLinkLine: typeof import('~icons/ri/external-link-line')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

22
packages/nc-gui/components/cell/Checkbox.vue

@ -89,18 +89,16 @@ useSelectedCellKeyupListener(active, (e) => {
}"
@click="onClick(false, $event)"
>
<div class="items-center" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery }" @click="onClick(true)">
<div :class="{ 'bg-gray-100 rounded-full ': !vModel }">
<Transition name="layout" mode="out-in" :duration="100">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
class="nc-checkbox"
:style="{
color: checkboxMeta.color,
}"
/>
</Transition>
</div>
<div class="items-center py-2" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery }" @click="onClick(true)">
<Transition name="layout" mode="out-in" :duration="100">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
class="nc-checkbox"
:style="{
color: checkboxMeta.color,
}"
/>
</Transition>
</div>
</div>
</template>

1
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -16,7 +16,6 @@ import {
useCommandPalette,
useDialog,
useNuxtApp,
useRouter,
useUndoRedo,
viewTypeAlias,
watch,

14
packages/nc-gui/components/general/ColorPicker.vue

@ -17,21 +17,23 @@ const props = withDefaults(defineProps<Props>(), {
pickButton: false,
})
const emit = defineEmits(['update:modelValue', 'input'])
const emit = defineEmits(['input', 'closeModal'])
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val || null)
emit('input', val || null)
},
})
const picked = ref<string>(props.modelValue || enumColor.light[0])
const selectColor = (color: string) => {
const selectColor = (color: string, closeModal = false) => {
picked.value = color
if (props.pickButton) vModel.value = color
if (closeModal) {
emit('closeModal')
}
}
const isPickerOn = ref(false)
@ -39,9 +41,7 @@ const isPickerOn = ref(false)
const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase()
watch(picked, (n, _o) => {
if (!props.pickButton) {
vModel.value = n
}
vModel.value = n
})
</script>
@ -54,7 +54,7 @@ watch(picked, (n, _o) => {
class="color-selector"
:class="compare(picked, color) ? 'selected' : ''"
:style="{ 'background-color': `${color}` }"
@click="selectColor(color)"
@click="selectColor(color, true)"
>
{{ compare(picked, color) ? '&#10003;' : '' }}
</button>

9
packages/nc-gui/components/general/DeleteModal.vue

@ -57,7 +57,14 @@ onKeyStroke('Enter', () => {
{{ $t('general.cancel') }}
</NcButton>
<NcButton key="submit" type="danger" html-type="submit" :loading="isLoading" @click="onDelete">
<NcButton
key="submit"
type="danger"
html-type="submit"
:loading="isLoading"
data-testid="nc-delete-modal-delete-btn"
@click="onDelete"
>
{{ `${$t('general.delete')} ${props.entityName}` }}
<template #loading>
{{ $t('general.deleting') }}

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

@ -48,7 +48,7 @@ useProvideSmartsheetRowStore(vModel, rowRef)
<template>
<div class="!my-3 text-xs">Default Value</div>
<div class="flex flex-row gap-2">
<div class="border-1 flex items-center w-full px-1 my-[-4px] border-gray-300 rounded-md">
<div class="border-1 flex items-center w-full px-3 my-[-4px] border-gray-300 rounded-md">
<LazySmartsheetCell :column="vModel" :model-value="cdfValue" :edit-enabled="true" />
<component
:is="iconMap.close"

5
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -218,11 +218,12 @@ watch(vModel.value, (next) => {
<LazyGeneralColorPicker
v-model="element.color"
:pick-button="true"
@update:model-value="colorMenus[index] = false"
@close-modal="colorMenus[index] = false"
@input="(el:string) => (element.color = el)"
/>
</template>
<MdiArrowDownDropCircle
class="mr-2 text-[1.5em] outline-0 hover:!text-[1.75em]"
class="mr-2 text-[1.5em] outline-0 hover:!text-[1.75em] cursor-pointer"
:class="{ 'text-[1.75em]': colorMenus[index] }"
:style="{ color: element.color }"
/>

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

@ -75,8 +75,6 @@ onKeyStroke('Enter', (event) => {
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT'))
const isSearchBoxFocused = ref(false)
function editComment(log: AuditType) {
editLog.value = log
isEditing.value = true

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

@ -325,13 +325,14 @@ const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false
await deleteRowById(primaryKey.value)
message.success('Row deleted')
if (!props.lastRow) {
await onNext()
} else if (!props.firstRow) {
emits('prev')
} else {
onClose()
}
// if (!props.lastRow) {
// await onNext()
// } else if (!props.firstRow) {
// emits('prev')
// } else {
// }
reloadTrigger.trigger()
onClose()
}
watch(
@ -398,7 +399,7 @@ export default {
<template #overlay>
<NcMenu>
<NcMenuItem v-if="!isNew" class="text-gray-700" @click="_loadRow()">
<div v-e="['c:row-expand:reload']" class="flex gap-2 items-center">
<div v-e="['c:row-expand:reload']" class="flex gap-2 items-center" data-testid="nc-expanded-form-reload">
<component :is="iconMap.reload" class="cursor-pointer" />
{{ $t('general.reload') }}
</div>
@ -408,25 +409,34 @@ export default {
class="text-gray-700"
@click="!isNew ? onDuplicateRow() : () => {}"
>
<div v-e="['c:row-expand:duplicate']" class="flex gap-2 items-center">
<div
v-e="['c:row-expand:duplicate']"
data-testid="nc-expanded-form-duplicate"
class="flex gap-2 items-center"
>
<component :is="iconMap.copy" class="cursor-pointer nc-duplicate-row" />
Duplicate record
</div>
</NcMenuItem>
<NcDivider />
<NcDivider v-if="isUIAllowed('dataEdit') && !isNew" />
<NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew"
v-e="['c:row-expand:delete']"
class="!text-red-500"
@click="!isNew && onDeleteRowClick()"
>
<component :is="iconMap.delete" class="cursor-pointer nc-delete-row" />
<component :is="iconMap.delete" data-testid="nc-expanded-form-delete" class="cursor-pointer nc-delete-row" />
Delete record
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<NcButton type="secondary" class="nc-expand-form-close-btn w-10" @click="onClose">
<NcButton
type="secondary"
class="nc-expand-form-close-btn w-10"
data-testid="nc-expanded-form-close"
@click="onClose"
>
<GeneralIcon icon="close" class="text-md text-gray-700" />
</NcButton>
</div>
@ -542,7 +552,7 @@ export default {
<template #overlay>
<NcMenu>
<NcMenuItem v-if="!isNew" class="text-gray-700" @click="_loadRow()">
<div v-e="['c:row-expand:reload']" class="flex gap-2 items-center">
<div v-e="['c:row-expand:reload']" class="flex gap-2 items-center" data-testid="nc-expanded-form-reload">
<component :is="iconMap.reload" class="cursor-pointer" />
{{ $t('general.reload') }}
</div>
@ -554,8 +564,10 @@ export default {
class="!text-red-500"
@click="!isNew && onDeleteRowClick()"
>
<component :is="iconMap.delete" class="cursor-pointer nc-delete-row" />
Delete record
<div data-testid="nc-expanded-form-delete">
<component :is="iconMap.delete" class="cursor-pointer nc-delete-row" />
Delete record
</div>
</NcMenuItem>
</NcMenu>
</template>
@ -566,12 +578,19 @@ export default {
v-if="isMobileMode"
type="secondary"
size="medium"
data-testid="nc-expanded-form-save"
class="nc-expand-form-save-btn !xs:(text-base)"
@click="onClose"
>
<div class="px-1">Close</div>
</NcButton>
<NcButton type="primary" size="medium" class="nc-expand-form-save-btn !xs:(text-base)" @click="save">
<NcButton
data-testid="nc-expanded-form-save"
type="primary"
size="medium"
class="nc-expand-form-save-btn !xs:(text-base)"
@click="save"
>
<div class="xs:px-1">Save</div>
</NcButton>
</div>

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

@ -360,7 +360,7 @@ const onInsertAfter = () => {
</a-menu-item>
<a-divider class="!my-0" />
<a-menu-item v-if="!column?.pv" @click="handleDelete" class="!hover:bg-red-50">
<a-menu-item v-if="!column?.pv" class="!hover:bg-red-50" @click="handleDelete">
<div class="nc-column-delete nc-header-menu-item my-0.75 text-red-600">
<component :is="iconMap.delete" class="ml-0.75 mr-1" />
<!-- Delete -->

10
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -10,6 +10,7 @@ import {
computed,
iconMap,
inject,
onMounted,
ref,
useNuxtApp,
useViewFilters,
@ -68,6 +69,8 @@ const {
saveOrUpdateDebounced,
isComparisonOpAllowed,
isComparisonSubOpAllowed,
loadBtLookupTypes,
btLookupTypesMap,
} = useViewFilters(
activeView,
parentId?.value,
@ -86,7 +89,8 @@ const addFiltersRowDomRef = ref<HTMLElement>()
const columns = computed(() => meta.value?.columns)
const getColumn = (filter: Filter) => {
return columns.value?.find((col: ColumnType) => col.id === filter.fk_column_id)
// extract looked up column if available
return btLookupTypesMap.value[filter.fk_column_id] || columns.value?.find((col: ColumnType) => col.id === filter.fk_column_id)
}
const filterPrevComparisonOp = ref<Record<string, string>>({})
@ -289,6 +293,10 @@ const showFilterInput = (filter: Filter) => {
onMounted(() => {
loadFilters(hookId?.value)
})
onMounted(async () => {
await loadBtLookupTypes()
})
</script>
<template>

54
packages/nc-gui/composables/useViewFilters.ts

@ -1,7 +1,7 @@
import type { ColumnType, FilterType, ViewType } from 'nocodb-sdk'
import type { ColumnType, FilterType, LinkToAnotherRecordType, LookupType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { SelectProps } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { RelationTypes, UITypes, isSystemColumn } from 'nocodb-sdk'
import {
ActiveViewInj,
IsPublicInj,
@ -32,6 +32,8 @@ export function useViewFilters(
) {
const currentFilters = ref(_currentFilters)
const btLookupTypesMap = ref({})
const reloadHook = inject(ReloadViewDataHookInj)
const { nestedFilters } = useSmartsheetStoreOrThrow()
@ -44,7 +46,7 @@ export function useViewFilters(
const { isUIAllowed } = useRoles()
const { metas } = useMetas()
const { metas, getMeta } = useMetas()
const { addUndo, clone, defineViewScope } = useUndoRedo()
@ -101,7 +103,12 @@ export function useViewFilters(
}
return meta.value?.columns?.reduce((obj: any, col: any) => {
obj[col.id] = col.uidt
// if column is a lookup column, then use the lookup type extracted from the column
if (btLookupTypesMap.value[col.id]) {
obj[col.id] = btLookupTypesMap.value[col.id].uidt
} else {
obj[col.id] = col.uidt
}
return obj
}, {})
})
@ -425,6 +432,43 @@ export function useViewFilters(
},
)
// method to extract looked up column meta for all bt lookup columns
// it helps to decide the condition operations for the column
const loadBtLookupTypes = async () => {
const btLookupTypes = {}
try {
for (const col of meta.value?.columns || []) {
if (col.uidt !== UITypes.Lookup) continue
let nextCol = col
let btLookup = true
// check all the relation of nested lookup columns is bt or not
// include the column only if all only if all relations are bt
while (btLookup && nextCol && nextCol.uidt === UITypes.Lookup) {
const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find(
(c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id,
)
if ((lookupRelation.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO) {
btLookup = false
continue
}
const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id)
nextCol = relatedTableMeta?.columns?.find((c) => c.id === (nextCol.colOptions as LookupType).fk_lookup_column_id)
// if next column is same as root lookup column then break the loop
// since it's going to be a circular loop
if (nextCol.id === col.id) {
break
}
}
btLookupTypes[col.id] = nextCol
}
btLookupTypesMap.value = btLookupTypes
} catch (e) {
// ignore error since it is not blocking any functionality of the app
console.error(e)
}
}
return {
filters,
nonDeletedFilters,
@ -437,5 +481,7 @@ export function useViewFilters(
saveOrUpdateDebounced,
isComparisonOpAllowed,
isComparisonSubOpAllowed,
loadBtLookupTypes,
btLookupTypesMap,
}
}

2
packages/nocodb/package.json

@ -37,7 +37,7 @@
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"watch:run:playwright:mysql": "rm -f ./test_noco.db; cross-env DB_TYPE=mysql NC_DB=\"mysql2://localhost:3306?u=root&p=password&d=pw_ncdb\" PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright:pg": "rm -f ./test_noco.db; cross-env DB_TYPE=pg NC_DB=\"pg://localhost:5432?u=postgres&p=password&d=pw_ncdb\" PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright": "rm -f ./test_noco.db; cross-env DB_TYPE=sqlite DATABASE_URL=sqlite:./test_noco.db PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true NC_SNAPSHOT_WINDOW_SEC=3 nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright": "rm -f ./test_*.db; cross-env DB_TYPE=sqlite DATABASE_URL=sqlite:./test_noco.db PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true NC_SNAPSHOT_WINDOW_SEC=3 nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright:quick": "rm -f ./test_noco.db; cp ../../tests/playwright/fixtures/noco_0_91_7.db ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright:pg:cyquick": "rm -f ./test_noco.db; cp ../../tests/playwright/fixtures/noco_0_91_7.db ./test_noco.db; cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"test:unit": "cross-env EE=false TS_NODE_PROJECT=./tests/unit/tsconfig.json mocha -r ts-node/register tests/unit/index.test.ts --recursive --timeout 300000 --exit --delay",

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

@ -460,7 +460,11 @@ const parseConditionV2 = async (
].includes(column.uidt)
) {
qb = qb.where(field, val);
} else if (column.ct === 'timestamp') {
} else if (
column.ct === 'timestamp' ||
column.ct === 'date' ||
column.ct === 'datetime'
) {
qb = qb.where(knex.raw('DATE(??) = DATE(?)', [field, val]));
} else {
// mysql is case-insensitive for strings, turn to case-sensitive

27
pnpm-lock.yaml

@ -932,8 +932,8 @@ importers:
version: 0.18.5
devDependencies:
'@playwright/test':
specifier: 1.36.1
version: 1.36.1
specifier: 1.38.0
version: 1.38.0
'@typescript-eslint/eslint-plugin':
specifier: ^6.1.0
version: 6.1.0(@typescript-eslint/parser@6.1.0)(eslint@8.33.0)(typescript@5.2.2)
@ -5610,15 +5610,12 @@ packages:
dev: true
optional: true
/@playwright/test@1.36.1:
resolution: {integrity: sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==}
/@playwright/test@1.38.0:
resolution: {integrity: sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==}
engines: {node: '>=16'}
hasBin: true
dependencies:
'@types/node': 20.3.1
playwright-core: 1.36.1
optionalDependencies:
fsevents: 2.3.2
playwright: 1.38.0
dev: true
/@polka/url@1.0.0-next.21:
@ -18477,10 +18474,20 @@ packages:
mlly: 1.4.1
pathe: 1.1.1
/playwright-core@1.36.1:
resolution: {integrity: sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==}
/playwright-core@1.38.0:
resolution: {integrity: sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==}
engines: {node: '>=16'}
hasBin: true
dev: true
/playwright@1.38.0:
resolution: {integrity: sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.38.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/plimit-lit@1.5.0:

3
tests/playwright/package.json

@ -34,6 +34,7 @@
"ci:test:shard:2": "pnpm exec playwright test --workers=2 --shard=2/4",
"ci:test:shard:3": "pnpm exec playwright test --workers=2 --shard=3/4",
"ci:test:shard:4": "pnpm exec playwright test --workers=2 --shard=4/4",
"ci:test:flaky:repeat": "pnpm exec playwright test --workers=4 --grep @flaky --repeat-each=3",
"ci:test:mysql": "E2E_DB_TYPE=mysql pnpm exec playwright test --workers=2",
"ci:test:pg": "E2E_DB_TYPE=pg pnpm exec playwright test --workers=2",
"preinstall": "npx only-allow pnpm"
@ -48,7 +49,7 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@playwright/test": "1.36.1",
"@playwright/test": "1.38.0",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"axios": "^0.24.0",

7
tests/playwright/pages/Account/Users.ts

@ -39,6 +39,8 @@ export class AccountUsersPage extends BasePage {
}
async invite({ email, role }: { email: string; role: string }) {
email = this.prefixEmail(email);
await this.inviteUserBtn.click();
await this.inviteUserModal.locator(`input[placeholder="E-mail"]`).fill(email);
await this.inviteUserModal.locator(`.nc-user-roles`).click();
@ -47,6 +49,9 @@ export class AccountUsersPage extends BasePage {
await this.inviteUserModal.locator(`button:has-text("Invite")`).click();
await this.verifyToast({ message: 'Successfully added user' });
// TODO: Wait on the invite api and get the invite url a better way as we are not waiting if the url is reflected in the UI
await this.rootPage.waitForTimeout(1000);
// http://localhost:3000/#/signup/a5e7bf3a-cbb0-46bc-87f7-c2ae21796707
return (await this.inviteUserModal.locator(`.ant-alert-message`).innerText()).split('\n')[0];
}
@ -63,6 +68,8 @@ export class AccountUsersPage extends BasePage {
async getUserRow({ email }: { email: string }) {
// ensure page is loaded
email = this.prefixEmail(email);
await this.get().waitFor();
return this.get().locator(`tr:has-text("${email}")`);
}

32
tests/playwright/pages/Dashboard/ExpandedForm/index.ts

@ -141,6 +141,10 @@ export class ExpandedFormPage extends BasePage {
async escape() {
await this.rootPage.keyboard.press('Escape');
await this.get().locator('.nc-drawer-expanded-form').waitFor({ state: 'hidden' });
await this.rootPage.waitForLoadState('networkidle');
await this.rootPage.waitForLoadState('domcontentloaded');
await this.rootPage.waitForTimeout(500);
}
async close() {
@ -165,29 +169,27 @@ export class ExpandedFormPage extends BasePage {
// expect(await this.btn_moreActions.count()).toBe(1);
await this.btn_moreActions.click();
const menu = this.rootPage.locator('.ant-dropdown:visible');
await menu.waitFor({ state: 'visible' });
const menuItems = menu.locator('.ant-dropdown-menu-item');
for (let i = 0; i < (await menuItems.count()); i++) {
if (role === 'owner' || role === 'editor' || role === 'creator') {
const menuText = ['Reload', 'Duplicate record', 'Delete record'];
expect(await getTextExcludeIconText(menuItems.nth(i))).toBe(menuText[i]);
} else {
const menuText = ['Reload', 'Close'];
expect(await menuItems.nth(i).innerText()).toBe(menuText[i]);
}
if (role === 'owner' || role === 'editor' || role === 'creator') {
await expect(this.rootPage.getByTestId('nc-expanded-form-reload')).toBeVisible();
await expect(this.rootPage.getByTestId('nc-expanded-form-duplicate')).toBeVisible();
await expect(this.rootPage.getByTestId('nc-expanded-form-delete')).toBeVisible();
} else {
await expect(this.rootPage.getByTestId('nc-expanded-form-reload')).toBeVisible();
await expect(this.rootPage.getByTestId('nc-expanded-form-duplicate')).toHaveCount(0);
await expect(this.rootPage.getByTestId('nc-expanded-form-delete')).toHaveCount(0);
}
if (role === 'owner' || role === 'editor' || role === 'creator') {
expect(await this.btn_save.count()).toBe(1);
await expect(this.rootPage.getByTestId('nc-expanded-form-save')).toHaveCount(1);
} else {
expect(await this.btn_save.count()).toBe(0);
await expect(this.rootPage.getByTestId('nc-expanded-form-save')).toHaveCount(0);
}
if (role === 'viewer') {
expect(await this.get().locator('.nc-comments-drawer').count()).toBe(0);
await expect(this.get().locator('.nc-comments-drawer')).toHaveCount(0);
} else {
expect(await this.get().locator('.nc-comments-drawer').count()).toBe(1);
await expect(this.get().locator('.nc-comments-drawer')).toHaveCount(1);
}
// press escape to close the expanded form

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

@ -146,8 +146,24 @@ export class FormPage extends BasePage {
}
async configureHeader(param: { subtitle: string; title: string }) {
await this.formHeading.fill(param.title);
await this.formSubHeading.fill(param.subtitle);
await this.waitForResponse({
uiAction: async () => {
await this.formHeading.click();
await this.formHeading.fill(param.title);
await this.formSubHeading.click();
},
requestUrlPathToMatch: 'api/v1/db/meta/forms',
httpMethodsToMatch: ['PATCH'],
});
await this.waitForResponse({
uiAction: async () => {
await this.formSubHeading.click();
await this.formSubHeading.fill(param.subtitle);
await this.formHeading.click();
},
requestUrlPathToMatch: 'api/v1/db/meta/forms',
httpMethodsToMatch: ['PATCH'],
});
}
async verifyHeader(param: { subtitle: string; title: string }) {
@ -177,14 +193,21 @@ export class FormPage extends BasePage {
label: string;
helpText: string;
}) {
const waitForResponse = async (action: () => Promise<any>) =>
await this.waitForResponse({
uiAction: action,
requestUrlPathToMatch: 'api/v1/db/meta/form-columns',
httpMethodsToMatch: ['PATCH'],
});
await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-testid="nc-form-input-label"]')
.click();
await this.getFormFieldsInputLabel().fill(label);
await this.getFormFieldsInputHelpText().fill(helpText);
await waitForResponse(() => this.getFormFieldsInputLabel().fill(label));
await waitForResponse(() => this.getFormFieldsInputHelpText().fill(helpText));
if (required) {
await this.getFormFieldsRequired().click();
await waitForResponse(() => this.getFormFieldsRequired().click());
}
await this.formHeading.click();
}
@ -241,7 +264,14 @@ export class FormPage extends BasePage {
}
async configureSubmitMessage(param: { message: string }) {
await this.afterSubmitMsg.fill(param.message);
await this.waitForResponse({
uiAction: async () => {
await this.afterSubmitMsg.click();
await this.afterSubmitMsg.fill(param.message);
},
requestUrlPathToMatch: 'api/v1/db/meta/forms',
httpMethodsToMatch: ['PATCH'],
});
}
submitAnotherForm() {

11
tests/playwright/pages/Dashboard/Grid/Group.ts

@ -100,15 +100,16 @@ export class GroupPageObject extends BasePage {
const addNewRowBtn = this.get({ indexMap }).locator('.nc-grid-add-new-row');
await addNewRowBtn.scrollIntoViewIfNeeded();
await (await addNewRowBtn.elementHandle()).waitForElementState('stable');
await this.rootPage.waitForTimeout(100);
const rowCount = await this.get({ indexMap }).locator('.nc-grid-row').count();
await this.rootPage.waitForTimeout(200);
await this.rootPage.waitForLoadState('networkidle');
await this.rootPage.waitForTimeout(200);
await this.rootPage.waitForLoadState('domcontentloaded');
await this.get({ indexMap }).locator('.nc-grid-add-new-row').click();
// add delay for UI to render (can wait for count to stabilize by reading it multiple times)
await this.rootPage.waitForTimeout(100);
expect(await this.get({ indexMap }).locator('.nc-grid-row').count()).toBe(rowCount + 1);
const rowCount = index + 1;
await expect(this.get({ indexMap }).locator('.nc-grid-row')).toHaveCount(rowCount);
await this._fillRow({ indexMap, index, columnHeader, value: rowValue });

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

@ -115,15 +115,16 @@ export class GridPage extends BasePage {
if (index !== 0) await this.get().locator('.nc-grid-row').nth(0).waitFor({ state: 'attached' });
await (await this.get().locator('.nc-grid-add-new-cell').elementHandle())?.waitForElementState('stable');
await this.rootPage.waitForTimeout(100);
const rowCount = await this.get().locator('.nc-grid-row').count();
await this.rootPage.waitForTimeout(200);
await this.rootPage.waitForLoadState('networkidle');
await this.rootPage.waitForTimeout(200);
await this.rootPage.waitForLoadState('domcontentloaded');
await this.get().locator('.nc-grid-add-new-cell').click();
// add delay for UI to render (can wait for count to stabilize by reading it multiple times)
await this.rootPage.waitForTimeout(100);
await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1);
const rowCount = index + 1;
await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount);
await this._fillRow({ index, columnHeader, value: rowValue });

17
tests/playwright/pages/Dashboard/TreeView.ts

@ -185,17 +185,6 @@ export class TreeViewPage extends BasePage {
requestUrlPathToMatch: `/api/v1/db/meta/tables/`,
});
await expect
.poll(
async () =>
await this.dashboard.tabBar
.locator('.ant-tabs-tab', {
hasText: title,
})
.isVisible()
)
.toBe(false);
await (await this.rootPage.locator('.nc-container').last().elementHandle())?.waitForElementState('stable');
}
@ -238,7 +227,7 @@ export class TreeViewPage extends BasePage {
async changeTableIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) {
await this.get().locator(`.nc-project-tree-tbl-${title} .nc-table-icon`).click();
await this.rootPage.locator('.emoji-mart-search').type(icon);
await this.rootPage.locator('.emoji-mart-search > input').fill(icon);
const emojiList = this.rootPage.locator('[id="emoji-mart-list"]');
await emojiList.locator('button').first().click();
await expect(
@ -302,9 +291,11 @@ export class TreeViewPage extends BasePage {
async openProject({ title, context }: { title: string; context: NcContext }) {
title = this.scopedProjectTitle({ title, context });
// loop through nodes.count() to find the node with title
await this.get().getByTestId(`nc-sidebar-project-title-${title}`).click();
await this.rootPage.waitForTimeout(1000);
// TODO: FIx why project click is not always registering
await this.get().getByTestId(`nc-sidebar-project-title-${title}`).click();
await this.rootPage.waitForTimeout(1000);
}

6
tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -127,14 +127,16 @@ export class ViewSidebarPage extends BasePage {
.locator('.nc-sidebar-view-node-context-btn')
.click();
await this.rootPage.waitForTimeout(750);
await this.rootPage
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-delete-icon')
.locator('.ant-dropdown-menu-title-content:has-text("Delete")')
.click({
force: true,
});
await this.rootPage.locator('button:has-text("Delete View"):visible').click();
await this.rootPage.getByTestId('nc-delete-modal-delete-btn').click();
}
async renameView({ title, newTitle }: { title: string; newTitle: string }) {

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

@ -27,7 +27,10 @@ export class AttachmentCellPageObject extends BasePage {
const attachFileAction = this.get({ index, columnHeader })
.locator('[data-testid="attachment-cell-file-picker-button"]')
.click();
return await this.attachFile({ filePickUIAction: attachFileAction, filePath });
await this.attachFile({ filePickUIAction: attachFileAction, filePath });
// wait for file to be uploaded
await this.rootPage.waitForTimeout(750);
}
async expandModalAddFile({ filePath }: { filePath: string[] }) {
@ -51,20 +54,8 @@ export class AttachmentCellPageObject extends BasePage {
async verifyFileCount({ index, columnHeader, count }: { index: number; columnHeader: string; count: number }) {
// retry below logic for 5 times, with 1 second delay
let retryCount = 0;
while (retryCount < 5) {
const attachments = this.get({ index, columnHeader }).locator('.nc-attachment');
// console.log(await attachments.count());
if ((await attachments.count()) === count) {
break;
}
retryCount++;
await this.rootPage.waitForTimeout(1000);
if (retryCount === 5) {
expect(await attachments.count()).toBe(count);
}
}
const attachments = this.get({ index, columnHeader }).locator('.nc-attachment');
await expect(attachments).toHaveCount(count);
}
async expandModalClose() {

3
tests/playwright/pages/Dashboard/common/Cell/CheckboxCell.ts

@ -15,7 +15,8 @@ export class CheckboxCellPageObject extends BasePage {
}
async click({ index, columnHeader }: { index?: number; columnHeader: string }) {
return await this.get({ index, columnHeader }).locator('.nc-cell').click();
await this.get({ index, columnHeader }).locator('.nc-cell').click();
await this.rootPage.waitForTimeout(500);
}
// async isChecked({ index, columnHeader }: { index?: number; columnHeader: string }) {

3
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -356,6 +356,9 @@ export class ToolbarFilterPage extends BasePage {
} else {
await this.get().locator('.nc-filter-item-remove-btn').click();
}
// TODO: Filter reset await not working all the time
await this.rootPage.waitForTimeout(650);
await this.toolbar.clickFilter();
}

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

@ -192,9 +192,6 @@ export class DashboardPage extends BasePage {
await this.rootPage.getByTestId('nc-sidebar-user-logout').waitFor({ state: 'visible' });
await this.sidebar.userMenu.clickLogout();
// TODO: Remove this
await this.rootPage.reload();
await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor();
await new Promise(resolve => setTimeout(resolve, 150));
}

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

@ -42,7 +42,7 @@ test.describe('Expanded form URL', () => {
// expand row & verify URL
// New Expanded Modal don't have functionality to copy URL. Hence gettting URL from root page
await viewObj.openExpandedRow({ index: 0 });
const url = await dashboard.rootPage.url();
await dashboard.expandedForm.escape();

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

@ -56,7 +56,10 @@ test.describe.serial('Test table', () => {
grid = dashboard.grid;
// create a new xcdb project
await dashboard.rootPage.waitForTimeout(650);
const xcdb = await createXcdb(context);
await dashboard.rootPage.waitForTimeout(650);
await dashboard.rootPage.reload();
await dashboard.treeView.openProject({ title: 'xcdb', context });

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

@ -162,7 +162,7 @@ test.describe('Fill Handle', () => {
await unsetup(p.context);
});
test('Select based', async () => {
test('Select based', async ({ page }) => {
const fields = [
{ title: 'SingleSelect', value: 'jan', type: 'singleSelect' },
{ title: 'MultiSelect', value: 'jan,feb,mar', type: 'multiSelect' },
@ -170,7 +170,7 @@ test.describe('Fill Handle', () => {
await dragDrop({ firstColumn: 'SingleSelect', lastColumn: 'MultiSelect', params: p });
await new Promise(r => setTimeout(r, 500));
await page.waitForTimeout(1000);
// verify data on grid
const displayOptions = ['jan', 'feb', 'mar'];
@ -212,7 +212,7 @@ test.describe('Fill Handle', () => {
await unsetup(p.context);
});
test('Miscellaneous (Checkbox, attachment)', async () => {
test('Miscellaneous (Checkbox, attachment) @flaky', async () => {
const fields = [
{ title: 'Checkbox', value: 'true', type: 'checkbox' },
{ title: 'Attachment', value: `${process.cwd()}/fixtures/sampleFiles/1.json`, type: 'attachment' },

2
tests/playwright/tests/db/general/projectOperations.spec.ts

@ -11,7 +11,7 @@ test.describe('Project operations', () => {
let dashboard: DashboardPage;
let context: NcContext;
let api: Api<any>;
test.setTimeout(100000);
test.setTimeout(150000);
async function getProjectList(workspaceId?: string) {
let projectList: ProjectListType;

9
tests/playwright/tests/db/usersAccounts/accountUserManagement.spec.ts

@ -11,8 +11,8 @@ import { isEE } from '../../../setup/db';
let api: Api<any>;
const roleDb = [
{ email: 'org_creator@nocodb.com', role: 'Organization Level Creator', url: '' },
{ email: 'org_viewer@nocodb.com', role: 'Organization Level Viewer', url: '' },
{ email: `org_creator_@nocodb.com`, role: 'Organization Level Creator', url: '' },
{ email: `org_viewer_@nocodb.com`, role: 'Organization Level Viewer', url: '' },
];
test.describe('User roles', () => {
@ -113,16 +113,15 @@ test.describe('User roles', () => {
await signupPage.signUp({
email: roleDb[roleIdx].email,
password: getDefaultPwd(),
withoutPrefix: true,
});
// wait for page rendering to complete after sign up
await dashboard.rootPage.waitForTimeout(1000);
if (roleDb[roleIdx].role === 'Organization Level Creator') {
expect(await dashboard.leftSidebar.btn_newProject.isVisible()).toBeTruthy();
await expect(dashboard.leftSidebar.btn_newProject).toBeVisible();
} else {
expect(await dashboard.leftSidebar.btn_newProject.isVisible()).toBeFalsy();
await expect(dashboard.leftSidebar.btn_newProject).toHaveCount(0);
}
}
});

Loading…
Cancel
Save