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. 4
      packages/nc-gui/components/cell/Checkbox.vue
  3. 1
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  4. 12
      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. 45
      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. 52
      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. 28
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  19. 36
      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. 19
      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. 3
      tests/playwright/tests/db/features/metaLTAR.spec.ts
  29. 6
      tests/playwright/tests/db/features/verticalFillHandle.spec.ts
  30. 2
      tests/playwright/tests/db/general/projectOperations.spec.ts
  31. 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'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio'] ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate'] ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row'] 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'] CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default'] ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['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'] IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default'] IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['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'] MaterialSymbolsVisibility: typeof import('~icons/material-symbols/visibility')['default']
MaterialSymbolsVisibilityOff: typeof import('~icons/material-symbols/visibility-off')['default'] MaterialSymbolsVisibilityOff: typeof import('~icons/material-symbols/visibility-off')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default'] MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccordionUp: typeof import('~icons/mdi/accordion-up')['default']
MdiAccount: typeof import('~icons/mdi/account')['default'] MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircleOutline: typeof import('~icons/mdi/account-circle-outline')['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'] 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'] MdiAppleKeyboardShift: typeof import('~icons/mdi/apple-keyboard-shift')['default']
MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default'] MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['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'] MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default'] MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default'] MdiChat: typeof import('~icons/mdi/chat')['default']
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default'] MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default'] MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiChevronUp: typeof import('~icons/mdi/chevron-up')['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'] MdiMessageOutline: typeof import('~icons/mdi/message-outline')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default'] MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default'] MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiMoreVert: typeof import('~icons/mdi/more-vert')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default'] MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiReload: typeof import('~icons/mdi/reload')['default'] MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['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'] MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default'] MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
NcIconsInbox: typeof import('~icons/nc-icons/inbox')['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'] 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'] RiExternalLinkLine: typeof import('~icons/ri/external-link-line')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

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

@ -89,8 +89,7 @@ useSelectedCellKeyupListener(active, (e) => {
}" }"
@click="onClick(false, $event)" @click="onClick(false, $event)"
> >
<div class="items-center" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery }" @click="onClick(true)"> <div class="items-center py-2" :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"> <Transition name="layout" mode="out-in" :duration="100">
<component <component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)" :is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
@ -102,7 +101,6 @@ useSelectedCellKeyupListener(active, (e) => {
</Transition> </Transition>
</div> </div>
</div> </div>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

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

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

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

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

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

@ -57,7 +57,14 @@ onKeyStroke('Enter', () => {
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</NcButton> </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}` }} {{ `${$t('general.delete')} ${props.entityName}` }}
<template #loading> <template #loading>
{{ $t('general.deleting') }} {{ $t('general.deleting') }}

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

@ -48,7 +48,7 @@ useProvideSmartsheetRowStore(vModel, rowRef)
<template> <template>
<div class="!my-3 text-xs">Default Value</div> <div class="!my-3 text-xs">Default Value</div>
<div class="flex flex-row gap-2"> <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" /> <LazySmartsheetCell :column="vModel" :model-value="cdfValue" :edit-enabled="true" />
<component <component
:is="iconMap.close" :is="iconMap.close"

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

@ -218,11 +218,12 @@ watch(vModel.value, (next) => {
<LazyGeneralColorPicker <LazyGeneralColorPicker
v-model="element.color" v-model="element.color"
:pick-button="true" :pick-button="true"
@update:model-value="colorMenus[index] = false" @close-modal="colorMenus[index] = false"
@input="(el:string) => (element.color = el)"
/> />
</template> </template>
<MdiArrowDownDropCircle <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] }" :class="{ 'text-[1.75em]': colorMenus[index] }"
:style="{ color: element.color }" :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 comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = 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) { function editComment(log: AuditType) {
editLog.value = log editLog.value = log
isEditing.value = true isEditing.value = true

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

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

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

@ -360,7 +360,7 @@ const onInsertAfter = () => {
</a-menu-item> </a-menu-item>
<a-divider class="!my-0" /> <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"> <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" /> <component :is="iconMap.delete" class="ml-0.75 mr-1" />
<!-- Delete --> <!-- Delete -->

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

@ -10,6 +10,7 @@ import {
computed, computed,
iconMap, iconMap,
inject, inject,
onMounted,
ref, ref,
useNuxtApp, useNuxtApp,
useViewFilters, useViewFilters,
@ -68,6 +69,8 @@ const {
saveOrUpdateDebounced, saveOrUpdateDebounced,
isComparisonOpAllowed, isComparisonOpAllowed,
isComparisonSubOpAllowed, isComparisonSubOpAllowed,
loadBtLookupTypes,
btLookupTypesMap,
} = useViewFilters( } = useViewFilters(
activeView, activeView,
parentId?.value, parentId?.value,
@ -86,7 +89,8 @@ const addFiltersRowDomRef = ref<HTMLElement>()
const columns = computed(() => meta.value?.columns) const columns = computed(() => meta.value?.columns)
const getColumn = (filter: Filter) => { 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>>({}) const filterPrevComparisonOp = ref<Record<string, string>>({})
@ -289,6 +293,10 @@ const showFilterInput = (filter: Filter) => {
onMounted(() => { onMounted(() => {
loadFilters(hookId?.value) loadFilters(hookId?.value)
}) })
onMounted(async () => {
await loadBtLookupTypes()
})
</script> </script>
<template> <template>

52
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 { ComputedRef, Ref } from 'vue'
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk' import { RelationTypes, UITypes, isSystemColumn } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
IsPublicInj, IsPublicInj,
@ -32,6 +32,8 @@ export function useViewFilters(
) { ) {
const currentFilters = ref(_currentFilters) const currentFilters = ref(_currentFilters)
const btLookupTypesMap = ref({})
const reloadHook = inject(ReloadViewDataHookInj) const reloadHook = inject(ReloadViewDataHookInj)
const { nestedFilters } = useSmartsheetStoreOrThrow() const { nestedFilters } = useSmartsheetStoreOrThrow()
@ -44,7 +46,7 @@ export function useViewFilters(
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { metas } = useMetas() const { metas, getMeta } = useMetas()
const { addUndo, clone, defineViewScope } = useUndoRedo() const { addUndo, clone, defineViewScope } = useUndoRedo()
@ -101,7 +103,12 @@ export function useViewFilters(
} }
return meta.value?.columns?.reduce((obj: any, col: any) => { return meta.value?.columns?.reduce((obj: any, col: any) => {
// 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 obj[col.id] = col.uidt
}
return obj 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 { return {
filters, filters,
nonDeletedFilters, nonDeletedFilters,
@ -437,5 +481,7 @@ export function useViewFilters(
saveOrUpdateDebounced, saveOrUpdateDebounced,
isComparisonOpAllowed, isComparisonOpAllowed,
isComparisonSubOpAllowed, 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: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: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: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: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\"", "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", "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) ].includes(column.uidt)
) { ) {
qb = qb.where(field, val); 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])); qb = qb.where(knex.raw('DATE(??) = DATE(?)', [field, val]));
} else { } else {
// mysql is case-insensitive for strings, turn to case-sensitive // mysql is case-insensitive for strings, turn to case-sensitive

27
pnpm-lock.yaml

@ -932,8 +932,8 @@ importers:
version: 0.18.5 version: 0.18.5
devDependencies: devDependencies:
'@playwright/test': '@playwright/test':
specifier: 1.36.1 specifier: 1.38.0
version: 1.36.1 version: 1.38.0
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0(@typescript-eslint/parser@6.1.0)(eslint@8.33.0)(typescript@5.2.2) 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 dev: true
optional: true optional: true
/@playwright/test@1.36.1: /@playwright/test@1.38.0:
resolution: {integrity: sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==} resolution: {integrity: sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==}
engines: {node: '>=16'} engines: {node: '>=16'}
hasBin: true hasBin: true
dependencies: dependencies:
'@types/node': 20.3.1 playwright: 1.38.0
playwright-core: 1.36.1
optionalDependencies:
fsevents: 2.3.2
dev: true dev: true
/@polka/url@1.0.0-next.21: /@polka/url@1.0.0-next.21:
@ -18477,10 +18474,20 @@ packages:
mlly: 1.4.1 mlly: 1.4.1
pathe: 1.1.1 pathe: 1.1.1
/playwright-core@1.36.1: /playwright-core@1.38.0:
resolution: {integrity: sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==} 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'} engines: {node: '>=16'}
hasBin: true hasBin: true
dependencies:
playwright-core: 1.38.0
optionalDependencies:
fsevents: 2.3.2
dev: true dev: true
/plimit-lit@1.5.0: /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: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: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: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:mysql": "E2E_DB_TYPE=mysql pnpm exec playwright test --workers=2",
"ci:test:pg": "E2E_DB_TYPE=pg pnpm exec playwright test --workers=2", "ci:test:pg": "E2E_DB_TYPE=pg pnpm exec playwright test --workers=2",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
@ -48,7 +49,7 @@
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.36.1", "@playwright/test": "1.38.0",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^6.1.0",
"axios": "^0.24.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 }) { async invite({ email, role }: { email: string; role: string }) {
email = this.prefixEmail(email);
await this.inviteUserBtn.click(); await this.inviteUserBtn.click();
await this.inviteUserModal.locator(`input[placeholder="E-mail"]`).fill(email); await this.inviteUserModal.locator(`input[placeholder="E-mail"]`).fill(email);
await this.inviteUserModal.locator(`.nc-user-roles`).click(); 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.inviteUserModal.locator(`button:has-text("Invite")`).click();
await this.verifyToast({ message: 'Successfully added user' }); 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 // http://localhost:3000/#/signup/a5e7bf3a-cbb0-46bc-87f7-c2ae21796707
return (await this.inviteUserModal.locator(`.ant-alert-message`).innerText()).split('\n')[0]; 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 }) { async getUserRow({ email }: { email: string }) {
// ensure page is loaded // ensure page is loaded
email = this.prefixEmail(email);
await this.get().waitFor(); await this.get().waitFor();
return this.get().locator(`tr:has-text("${email}")`); return this.get().locator(`tr:has-text("${email}")`);
} }

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

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

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

@ -146,8 +146,24 @@ export class FormPage extends BasePage {
} }
async configureHeader(param: { subtitle: string; title: string }) { async configureHeader(param: { subtitle: string; title: string }) {
await this.waitForResponse({
uiAction: async () => {
await this.formHeading.click();
await this.formHeading.fill(param.title); 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.formSubHeading.fill(param.subtitle);
await this.formHeading.click();
},
requestUrlPathToMatch: 'api/v1/db/meta/forms',
httpMethodsToMatch: ['PATCH'],
});
} }
async verifyHeader(param: { subtitle: string; title: string }) { async verifyHeader(param: { subtitle: string; title: string }) {
@ -177,14 +193,21 @@ export class FormPage extends BasePage {
label: string; label: string;
helpText: 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() await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`) .locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-testid="nc-form-input-label"]') .locator('div[data-testid="nc-form-input-label"]')
.click(); .click();
await this.getFormFieldsInputLabel().fill(label); await waitForResponse(() => this.getFormFieldsInputLabel().fill(label));
await this.getFormFieldsInputHelpText().fill(helpText); await waitForResponse(() => this.getFormFieldsInputHelpText().fill(helpText));
if (required) { if (required) {
await this.getFormFieldsRequired().click(); await waitForResponse(() => this.getFormFieldsRequired().click());
} }
await this.formHeading.click(); await this.formHeading.click();
} }
@ -241,7 +264,14 @@ export class FormPage extends BasePage {
} }
async configureSubmitMessage(param: { message: string }) { async configureSubmitMessage(param: { message: string }) {
await this.waitForResponse({
uiAction: async () => {
await this.afterSubmitMsg.click();
await this.afterSubmitMsg.fill(param.message); await this.afterSubmitMsg.fill(param.message);
},
requestUrlPathToMatch: 'api/v1/db/meta/forms',
httpMethodsToMatch: ['PATCH'],
});
} }
submitAnotherForm() { 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'); const addNewRowBtn = this.get({ indexMap }).locator('.nc-grid-add-new-row');
await addNewRowBtn.scrollIntoViewIfNeeded(); await addNewRowBtn.scrollIntoViewIfNeeded();
await (await addNewRowBtn.elementHandle()).waitForElementState('stable'); 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(); 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) const rowCount = index + 1;
await this.rootPage.waitForTimeout(100); await expect(this.get({ indexMap }).locator('.nc-grid-row')).toHaveCount(rowCount);
expect(await this.get({ indexMap }).locator('.nc-grid-row').count()).toBe(rowCount + 1);
await this._fillRow({ indexMap, index, columnHeader, value: rowValue }); 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' }); 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 (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(); 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) const rowCount = index + 1;
await this.rootPage.waitForTimeout(100); await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount);
await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1);
await this._fillRow({ index, columnHeader, value: rowValue }); 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/`, 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'); 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 }) { 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.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"]'); const emojiList = this.rootPage.locator('[id="emoji-mart-list"]');
await emojiList.locator('button').first().click(); await emojiList.locator('button').first().click();
await expect( await expect(
@ -302,9 +291,11 @@ export class TreeViewPage extends BasePage {
async openProject({ title, context }: { title: string; context: NcContext }) { async openProject({ title, context }: { title: string; context: NcContext }) {
title = this.scopedProjectTitle({ title, context }); 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.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); 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') .locator('.nc-sidebar-view-node-context-btn')
.click(); .click();
await this.rootPage.waitForTimeout(750);
await this.rootPage await this.rootPage
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`) .locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-delete-icon') .locator('.ant-dropdown-menu-title-content:has-text("Delete")')
.click({ .click({
force: true, 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 }) { async renameView({ title, newTitle }: { title: string; newTitle: string }) {

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

@ -27,7 +27,10 @@ export class AttachmentCellPageObject extends BasePage {
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();
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[] }) { 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 }) { async verifyFileCount({ index, columnHeader, count }: { index: number; columnHeader: string; count: number }) {
// retry below logic for 5 times, with 1 second delay // 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'); const attachments = this.get({ index, columnHeader }).locator('.nc-attachment');
// console.log(await attachments.count()); await expect(attachments).toHaveCount(count);
if ((await attachments.count()) === count) {
break;
}
retryCount++;
await this.rootPage.waitForTimeout(1000);
if (retryCount === 5) {
expect(await attachments.count()).toBe(count);
}
}
} }
async expandModalClose() { 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 }) { 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 }) { // 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 { } else {
await this.get().locator('.nc-filter-item-remove-btn').click(); 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(); 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.rootPage.getByTestId('nc-sidebar-user-logout').waitFor({ state: 'visible' });
await this.sidebar.userMenu.clickLogout(); await this.sidebar.userMenu.clickLogout();
// TODO: Remove this
await this.rootPage.reload();
await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor(); await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor();
await new Promise(resolve => setTimeout(resolve, 150)); await new Promise(resolve => setTimeout(resolve, 150));
} }

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

@ -56,7 +56,10 @@ test.describe.serial('Test table', () => {
grid = dashboard.grid; grid = dashboard.grid;
// create a new xcdb project // create a new xcdb project
await dashboard.rootPage.waitForTimeout(650);
const xcdb = await createXcdb(context); const xcdb = await createXcdb(context);
await dashboard.rootPage.waitForTimeout(650);
await dashboard.rootPage.reload(); await dashboard.rootPage.reload();
await dashboard.treeView.openProject({ title: 'xcdb', context }); 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); await unsetup(p.context);
}); });
test('Select based', async () => { test('Select based', async ({ page }) => {
const fields = [ const fields = [
{ title: 'SingleSelect', value: 'jan', type: 'singleSelect' }, { title: 'SingleSelect', value: 'jan', type: 'singleSelect' },
{ title: 'MultiSelect', value: 'jan,feb,mar', type: 'multiSelect' }, { 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 dragDrop({ firstColumn: 'SingleSelect', lastColumn: 'MultiSelect', params: p });
await new Promise(r => setTimeout(r, 500)); await page.waitForTimeout(1000);
// verify data on grid // verify data on grid
const displayOptions = ['jan', 'feb', 'mar']; const displayOptions = ['jan', 'feb', 'mar'];
@ -212,7 +212,7 @@ test.describe('Fill Handle', () => {
await unsetup(p.context); await unsetup(p.context);
}); });
test('Miscellaneous (Checkbox, attachment)', async () => { test('Miscellaneous (Checkbox, attachment) @flaky', async () => {
const fields = [ const fields = [
{ title: 'Checkbox', value: 'true', type: 'checkbox' }, { title: 'Checkbox', value: 'true', type: 'checkbox' },
{ title: 'Attachment', value: `${process.cwd()}/fixtures/sampleFiles/1.json`, type: 'attachment' }, { 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 dashboard: DashboardPage;
let context: NcContext; let context: NcContext;
let api: Api<any>; let api: Api<any>;
test.setTimeout(100000); test.setTimeout(150000);
async function getProjectList(workspaceId?: string) { async function getProjectList(workspaceId?: string) {
let projectList: ProjectListType; 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>; let api: Api<any>;
const roleDb = [ const roleDb = [
{ email: 'org_creator@nocodb.com', role: 'Organization Level Creator', url: '' }, { email: `org_creator_@nocodb.com`, role: 'Organization Level Creator', url: '' },
{ email: 'org_viewer@nocodb.com', role: 'Organization Level Viewer', url: '' }, { email: `org_viewer_@nocodb.com`, role: 'Organization Level Viewer', url: '' },
]; ];
test.describe('User roles', () => { test.describe('User roles', () => {
@ -113,16 +113,15 @@ test.describe('User roles', () => {
await signupPage.signUp({ await signupPage.signUp({
email: roleDb[roleIdx].email, email: roleDb[roleIdx].email,
password: getDefaultPwd(), password: getDefaultPwd(),
withoutPrefix: true,
}); });
// wait for page rendering to complete after sign up // wait for page rendering to complete after sign up
await dashboard.rootPage.waitForTimeout(1000); await dashboard.rootPage.waitForTimeout(1000);
if (roleDb[roleIdx].role === 'Organization Level Creator') { if (roleDb[roleIdx].role === 'Organization Level Creator') {
expect(await dashboard.leftSidebar.btn_newProject.isVisible()).toBeTruthy(); await expect(dashboard.leftSidebar.btn_newProject).toBeVisible();
} else { } else {
expect(await dashboard.leftSidebar.btn_newProject.isVisible()).toBeFalsy(); await expect(dashboard.leftSidebar.btn_newProject).toHaveCount(0);
} }
} }
}); });

Loading…
Cancel
Save