Browse Source

Merge pull request #5138 from nocodb/develop

pull/5140/head 0.105.1
github-actions[bot] 2 years ago committed by GitHub
parent
commit
1194c873a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/release-executables.yml
  2. 2
      .github/workflows/release-timely-executables.yml
  3. 11
      packages/nc-gui/assets/style.scss
  4. 1
      packages/nc-gui/components.d.ts
  5. 102
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  6. 35
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  7. 8
      packages/nc-gui/components/template/Editor.vue
  8. 4
      packages/nc-gui/composables/useColumnCreateStore.ts
  9. 3
      packages/nc-gui/composables/useExpandedFormStore.ts
  10. 1
      packages/nc-gui/lang/en.json
  11. 64
      packages/nc-gui/package-lock.json
  12. 2
      packages/nc-gui/package.json
  13. 26
      packages/nc-gui/utils/validation.ts
  14. 3401
      packages/nc-plugin/package-lock.json
  15. 2
      packages/nc-plugin/package.json
  16. 4
      packages/noco-docs/content/en/engineering/playwright.md
  17. 4
      packages/nocodb-sdk/package-lock.json
  18. 6
      packages/nocodb/Dockerfile
  19. 7
      packages/nocodb/docker/index.js
  20. 5
      packages/nocodb/docker/webpack.config.js
  21. 186
      packages/nocodb/package-lock.json
  22. 6
      packages/nocodb/package.json
  23. 31
      packages/nocodb/src/lib/meta/api/columnApis.ts
  24. 13
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts
  25. 4
      packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts
  26. 1
      packages/nocodb/src/lib/meta/api/helpers/index.ts
  27. 15
      packages/nocodb/src/lib/meta/api/tableApis.ts
  28. 13
      packages/nocodb/src/lib/models/Column.ts
  29. 7
      packages/nocodb/webpack.config.js
  30. 5
      tests/playwright/pages/Account/ChangePassword.ts
  31. 37
      tests/playwright/pages/Base.ts
  32. 31
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  33. 2
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts
  34. 8
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  35. 16
      tests/playwright/pages/Dashboard/Grid/index.ts
  36. 2
      tests/playwright/pages/Dashboard/Import/ImportTemplate.ts
  37. 2
      tests/playwright/pages/Dashboard/Settings/Acl.ts
  38. 2
      tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts
  39. 6
      tests/playwright/pages/Dashboard/TreeView.ts
  40. 12
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts
  41. 2
      tests/playwright/pages/Dashboard/WebhookForm/index.ts
  42. 2
      tests/playwright/pages/Dashboard/common/Cell/RatingCell.ts
  43. 2
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  44. 15
      tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts
  45. 4
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  46. 11
      tests/playwright/pages/Dashboard/common/Toolbar/Sort.ts
  47. 6
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  48. 8
      tests/playwright/pages/ProjectsPage/index.ts
  49. 2
      tests/playwright/pages/SharedForm/index.ts
  50. 4
      tests/playwright/tests/expandedFormUrl.spec.ts
  51. 137
      tests/playwright/tests/filters.spec.ts

2
.github/workflows/release-executables.yml

@ -87,7 +87,7 @@ jobs:
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=arm64 --target_libc=musl
# clean up code to optimize size
npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,nocodb/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build

2
.github/workflows/release-timely-executables.yml

@ -103,7 +103,7 @@ jobs:
# clean up code to optimize size
npx modclean --patterns="default:*" --ignore="nc-lib-gui-daily/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
npx modclean --patterns="default:*" --ignore="nc-lib-gui-daily/**,nocodb-daily/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build

11
packages/nc-gui/assets/style.scss

@ -318,3 +318,14 @@ a {
white-space: pre;
user-select: auto;
}
.nc-drawer-expanded-form .ant-drawer-content-wrapper {
transition: width 0.3s !important;
}
.nc-icon-transition {
@apply transform transition-transform !hover:(scale-115) !active:(scale-100)
}

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

@ -204,6 +204,7 @@ declare module '@vue/runtime-core' {
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuArrowDown: typeof import('~icons/mdi/menu-arrow-down')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']

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

@ -108,7 +108,11 @@ const onConfirmDeleteRowClick = async () => {
<template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div>
</template>
<mdi-reload v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1 min-w-4" @click="loadRow" />
<mdi-reload
v-if="!isNew"
class="nc-icon-transition cursor-pointer select-none text-gray-500 mx-1 min-w-4"
@click="loadRow"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
@ -117,7 +121,7 @@ const onConfirmDeleteRowClick = async () => {
</template>
<mdi-link
v-if="!isNew"
class="cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url min-w-4"
class="nc-icon-transition cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url min-w-4"
@click="copyRecordUrl"
/>
</a-tooltip>
@ -130,61 +134,40 @@ const onConfirmDeleteRowClick = async () => {
<MdiCommentTextOutline
v-if="isUIAllowed('rowComments') && !isNew"
v-e="['c:row-expand:comment-toggle']"
class="cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1 min-w-4"
class="nc-icon-transition cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1 min-w-4"
@click="commentsDrawer = !commentsDrawer"
/>
</a-tooltip>
<a-button class="!text mx-1 nc-expand-form-close-btn" @click="emit('cancel')">
<div class="flex items-center">
<MdiCloseCircleOutline class="mr-1" />
<!-- Close -->
{{ $t('general.close') }}
</div>
</a-button>
<a-dropdown>
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-1 items-center">
<!-- More -->
<span class="!text-sm font-weight-medium">{{ $t('general.more') }}</span>
<MdiMenuDown class="text-grey" />
</div>
</a-button>
<template #overlay>
<div class="bg-gray-50 py-2 shadow-lg !border">
<div>
<div
v-e="['a:actions:duplicate-row']"
class="nc-menu-item"
:class="{ disabled: isNew }"
@click="!isNew && emit('duplicateRow')"
>
<MdiContentCopy class="text-gray-500" />
{{ $t('activity.duplicateRow') }}
</div>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Duplicate row -->
<template #title>
<div class="text-center w-full">{{ $t('activity.duplicateRow') }}</div>
</template>
<MdiContentCopy
v-if="isUIAllowed('xcDatatableEditable') && !isNew"
v-e="['c:row-expand:duplicate']"
class="nc-icon-transition cursor-pointer select-none nc-duplicate-row text-gray-500 mx-1 min-w-4"
@click="!isNew && emit('duplicateRow')"
/>
</a-tooltip>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick">
<p>Are you sure you want to delete this row?</p>
</a-modal>
<div
v-e="['a:actions:delete-row']"
class="nc-menu-item"
:class="{ disabled: isNew }"
@click="!isNew && onDeleteRowClick()"
>
<MdiDelete class="text-gray-500" />
{{ $t('activity.deleteRow') }}
</div>
</div>
</div>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Delete row -->
<template #title>
<div class="text-center w-full">{{ $t('activity.deleteRow') }}</div>
</template>
</a-dropdown>
<MdiDeleteOutline
v-if="isUIAllowed('xcDatatableEditable') && !isNew"
v-e="['c:row-expand:delete']"
class="nc-icon-transition cursor-pointer select-none nc-delete-row text-gray-500 mx-1 min-w-4"
@click="!isNew && onDeleteRowClick()"
/>
</a-tooltip>
<a-dropdown-button class="nc-expand-form-save-btn" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<template #icon><MdiMenuDown /></template>
<template #overlay>
<a-menu class="nc-expand-form-save-dropdown-menu">
<a-menu-item key="0" class="!py-2 flex gap-2" @click="saveRowAndStay = 0">
@ -210,5 +193,26 @@ const onConfirmDeleteRowClick = async () => {
{{ $t('activity.saveAndStay') }}
</div>
</a-dropdown-button>
<a-tooltip placement="bottom">
<!-- Close -->
<template #title>
<div class="text-center w-full">{{ $t('general.close') }}</div>
</template>
<MdiCloseCircleOutline
class="nc-icon-transition cursor-pointer select-none nc-close-form text-gray-500 mx-1 min-w-4"
@click="emit('cancel')"
/>
</a-tooltip>
<a-modal v-model:visible="showDeleteRowModal" title="Delete row?" @ok="onConfirmDeleteRowClick">
<p>Are you sure you want to delete this row?</p>
</a-modal>
</div>
</template>
<style scoped>
:deep(svg) {
@apply outline-none;
}
</style>

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

@ -163,7 +163,7 @@ export default {
<a-drawer
v-model:visible="isExpanded"
:footer="null"
width="min(90vw,800px)"
:width="commentsDrawer ? 'min(90vw,900px)' : 'min(90vw,700px)'"
:body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }"
:closable="false"
class="nc-drawer-expanded-form"
@ -171,24 +171,23 @@ export default {
>
<SmartsheetExpandedFormHeader :view="props.view" @cancel="onClose" @duplicate-row="onDuplicateRow" />
<div class="!bg-gray-100 rounded flex-1 relative">
<template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.nextRow') }}
</template>
<MdiChevronRight class="cursor-pointer nc-next-arrow" @click="$emit('next')" />
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.prevRow') }}
</template>
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
</a-tooltip>
</template>
<div class="!bg-gray-100 rounded flex-1">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container relative">
<template v-if="props.showNextPrevIcons">
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.nextRow') }}
</template>
<MdiChevronRight class="cursor-pointer nc-next-arrow" @click="$emit('next')" />
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
{{ $t('labels.prevRow') }}
</template>
<MdiChevronLeft class="cursor-pointer nc-prev-arrow" @click="$emit('prev')" />
</a-tooltip>
</template>
<div class="w-[500px] mx-auto">
<div v-if="duplicatingRowInProgress" class="flex items-center justify-center h-[100px]">
<a-spin size="large" />

8
packages/nc-gui/components/template/Editor.vue

@ -12,6 +12,7 @@ import {
computed,
createEventHook,
extractSdkResponseErrorMsg,
fieldLengthValidator,
fieldRequiredValidator,
getDateFormat,
getDateTimeFormat,
@ -110,12 +111,15 @@ const data = reactive<{
})
const validators = computed(() =>
data.tables.reduce<Record<string, [ReturnType<typeof fieldRequiredValidator>]>>((acc, table, tableIdx) => {
data.tables.reduce<Record<string, [ReturnType<typeof fieldRequiredValidator>]>>((acc: Record<string, any>, table, tableIdx) => {
acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator()]
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator()]
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [
fieldRequiredValidator(),
fieldLengthValidator(project.value?.bases?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true

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

@ -27,7 +27,8 @@ interface ValidationsObj {
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUis, isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = useProject()
const { project, sqlUis, isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = useProject()
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
@ -93,6 +94,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
})
},
},
fieldLengthValidator(project.value?.bases?.[0].type || ClientType.MYSQL),
],
uidt: [
{

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

@ -190,7 +190,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
} else {
// No columns to update
return message.info(t('msg.info.noColumnsToUpdate'))
message.info(t('msg.info.noColumnsToUpdate'))
return
}
}

1
packages/nc-gui/lang/en.json

@ -690,6 +690,7 @@
"nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _",
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
"projectNameCannotStartWithSpace": "Project name cannot start with space",
"requiredField": "Required field",

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

@ -29,7 +29,7 @@
"jwt-decode": "^3.1.2",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "0.105.0",
"nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"qrcode": "^1.5.1",
"socket.io-client": "^4.5.1",
@ -98,7 +98,6 @@
},
"../nocodb-sdk": {
"version": "0.105.0",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -8555,6 +8554,7 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true,
"funding": [
{
"type": "individual",
@ -11970,21 +11970,8 @@
}
},
"node_modules/nocodb-sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/nocodb-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abi": {
"version": "3.23.0",
@ -15876,9 +15863,9 @@
}
},
"node_modules/undici": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.12.0.tgz",
"integrity": "sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==",
"version": "5.19.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.19.1.tgz",
"integrity": "sha512-YiZ61LPIgY73E7syxCDxxa3LV2yl3sN8spnIuTct60boiiRaE1J8mNWHO8Im2Zi/sFrPusjLlmRPrsyraSqX6A==",
"dev": true,
"dependencies": {
"busboy": "^1.6.0"
@ -23975,7 +23962,8 @@
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true
},
"form-data": {
"version": "4.0.0",
@ -26448,22 +26436,22 @@
}
},
"nocodb-sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
}
},
"node-abi": {
@ -29386,9 +29374,9 @@
}
},
"undici": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.12.0.tgz",
"integrity": "sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==",
"version": "5.19.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.19.1.tgz",
"integrity": "sha512-YiZ61LPIgY73E7syxCDxxa3LV2yl3sN8spnIuTct60boiiRaE1J8mNWHO8Im2Zi/sFrPusjLlmRPrsyraSqX6A==",
"dev": true,
"requires": {
"busboy": "^1.6.0"

2
packages/nc-gui/package.json

@ -52,7 +52,7 @@
"jwt-decode": "^3.1.2",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "0.105.0",
"nocodb-sdk": "file:../nocodb-sdk",
"papaparse": "^5.3.2",
"qrcode": "^1.5.1",
"socket.io-client": "^4.5.1",

26
packages/nc-gui/utils/validation.ts

@ -80,6 +80,32 @@ export const fieldRequiredValidator = () => {
}
}
export const fieldLengthValidator = (sqlClientType: string) => {
return {
validator: (rule: any, value: any) => {
const { t } = getI18n().global
// no limit for sqlite but set as 255
let fieldLengthLimit = 255
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
fieldLengthLimit = 64
} else if (sqlClientType === 'pg') {
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
return new Promise((resolve, reject) => {
if (value?.length > fieldLengthLimit) {
reject(new Error(t('msg.error.columnNameExceedsCharacters', { value: fieldLengthLimit })))
}
resolve(true)
})
},
}
}
export const importUrlValidator = {
validator: (rule: any, value: any) => {
return new Promise((resolve, reject) => {

3401
packages/nc-plugin/package-lock.json generated

File diff suppressed because it is too large Load Diff

2
packages/nc-plugin/package.json

@ -60,7 +60,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"ava": "^3.12.1",
"ava": "^5.2.0",
"codecov": "^3.5.0",
"cspell": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",

4
packages/noco-docs/content/en/engineering/playwright.md

@ -189,7 +189,7 @@ This a method which will reset/clear all the filters. Since this is an action me
```js
async resetFilter() {
await this.waitForResponse({
uiAction: this.get().locator('.nc-filter-item-remove-btn').click(),
uiAction: () => this.get().locator('.nc-filter-item-remove-btn').click(),
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: '/api/v1/db/meta/filters/',
});
@ -221,4 +221,4 @@ async verifyFilter({ title }: { title: string }) {
- Open `Summary` tab in the CI workflow in github actions.
- Scroll down to `Artifacts` section.
- Access reports which suffixed with the db type and shard number(corresponding to the CI workerflow name). i.e `playwright-report-mysql-2` is for `playwright-mysql-2` workflow.
- Download it and run `npm install -D @playwright/test && npx playwright show-report ./` inside the downloaded folder.
- Download it and run `npm install -D @playwright/test && npx playwright show-report ./` inside the downloaded folder.

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.104.3",
"version": "0.105.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.104.3",
"version": "0.105.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

6
packages/nocodb/Dockerfile

@ -31,9 +31,9 @@ COPY ./package*.json ./
COPY ./docker/main.js ./docker/main.js
#COPY ./docker/start.sh /usr/src/appEntry/start.sh
COPY ./docker/start-litestream.sh /usr/src/appEntry/start.sh
COPY ./src/lib/public/css/*.css /usr/src/appEntry/public/css/
COPY ./src/lib/public/js/*.js /usr/src/appEntry/public/js/
COPY ./src/lib/public/favicon.ico /usr/src/appEntry/public/
COPY ./src/lib/public/css/*.css ./docker/public/css/
COPY ./src/lib/public/js/*.js ./docker/public/js/
COPY ./src/lib/public/favicon.ico ./docker/public/
# install production dependencies,
# reduce node_module size with modclean & removing sqlite deps,

7
packages/nocodb/docker/index.js

@ -10,10 +10,7 @@ server.set('view engine', 'ejs');
(async () => {
const httpServer = server.listen(process.env.PORT || 8080, () => {
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`);
const httpServer = server.listen(process.env.PORT || 8080, async () => {
server.use(await Noco.init({}, httpServer, server));
})
server.use(await Noco.init({}, httpServer, server));
})().catch(e => console.log(e))

5
packages/nocodb/docker/webpack.config.js

@ -56,4 +56,7 @@ module.exports = {
// })
],
target: 'node',
};
node: {
__dirname: false,
},
};

186
packages/nocodb/package-lock.json generated

@ -58,7 +58,7 @@
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
"minio": "^7.0.18",
"mkdirp": "^0.5.5",
"mkdirp": "^2.1.3",
"morgan": "^1.10.0",
"mssql": "^6.2.0",
"multer": "^1.4.2",
@ -68,7 +68,7 @@
"nc-lib-gui": "0.105.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "0.105.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",
@ -155,7 +155,6 @@
},
"../nocodb-sdk": {
"version": "0.105.0",
"extraneous": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -4259,6 +4258,18 @@
"run-queue": "^1.0.0"
}
},
"node_modules/copy-concurrently/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@ -10321,6 +10332,17 @@
"node": ">8 <=18"
}
},
"node_modules/minio/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/minio/node_modules/through2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
@ -10510,14 +10532,17 @@
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz",
"integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==",
"bin": {
"mkdirp": "bin/cmd.js"
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mocha": {
@ -10874,6 +10899,18 @@
"run-queue": "^1.0.3"
}
},
"node_modules/move-concurrently/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/move-file": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/move-file/-/move-file-1.2.0.tgz",
@ -11021,6 +11058,17 @@
"node": ">= 0.10.0"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/multi-stage-sourcemap": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.3.1.tgz",
@ -11270,13 +11318,8 @@
"dev": true
},
"node_modules/nocodb-sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
"resolved": "../nocodb-sdk",
"link": true
},
"node_modules/node-abort-controller": {
"version": "3.0.1",
@ -17153,6 +17196,17 @@
"node": ">= 0.12.0"
}
},
"node_modules/utility/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -18318,6 +18372,18 @@
"node": ">=0.10.0"
}
},
"node_modules/webpack/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/webpack/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@ -22371,6 +22437,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"copy-descriptor": {
@ -27047,6 +27124,14 @@
"xml2js": "^0.4.15"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
},
"through2": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
@ -27213,12 +27298,9 @@
}
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz",
"integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw=="
},
"mocha": {
"version": "10.1.0",
@ -27489,6 +27571,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"move-file": {
@ -27612,6 +27705,16 @@
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"multi-stage-sourcemap": {
@ -27821,12 +27924,22 @@
"dev": true
},
"nocodb-sdk": {
"version": "0.105.0",
"resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.105.0.tgz",
"integrity": "sha512-QqUM8QtX+10UbKmlNjz0FFDSLWB5IV46E7HjGmzPb3Z3Hagq0KQPSXUlWAp2YaHsYsKEmqH7e8N/0AuLL3D/pg==",
"version": "file:../nocodb-sdk",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"jsep": "^1.3.6"
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
}
},
"node-abort-controller": {
@ -32428,6 +32541,16 @@
"mkdirp": "^0.5.1",
"mz": "^2.7.0",
"unescape": "^1.0.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
}
}
},
"utils-merge": {
@ -33032,6 +33155,15 @@
"to-regex": "^3.0.2"
}
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",

6
packages/nocodb/package.json

@ -98,7 +98,7 @@
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
"minio": "^7.0.18",
"mkdirp": "^0.5.5",
"mkdirp": "^2.1.3",
"morgan": "^1.10.0",
"mssql": "^6.2.0",
"multer": "^1.4.2",
@ -108,7 +108,7 @@
"nc-lib-gui": "0.105.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "0.105.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^5.0.0",
@ -184,4 +184,4 @@
"prettier": {
"singleQuote": true
}
}
}

31
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -64,9 +64,27 @@ export async function columnAdd(
const table = await Model.getWithInfo({
id: req.params.tableId,
});
const base = await Base.get(table.base_id);
const project = await base.getProject();
if (req.body.title || req.body.column_name) {
const dbDriver = NcConnectionMgrv2.get(base);
const sqlClientType = dbDriver.clientType();
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
if ((req.body.title || req.body.column_name).length > mxColumnLength) {
NcError.badRequest(
`Column name ${
req.body.title || req.body.column_name
} exceeds ${mxColumnLength} characters`
);
}
}
if (
!isVirtualCol(req.body) &&
!(await Column.checkTitleAvailable({
@ -638,8 +656,21 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
const table = await Model.getWithInfo({
id: column.fk_model_id,
});
const base = await Base.get(table.base_id);
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
const sqlClientType = sqlClient.knex.clientType();
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
if (req.body.column_name.length > mxColumnLength) {
NcError.badRequest(
`Column name ${req.body.column_name} exceeds ${mxColumnLength} characters`
);
}
if (
!isVirtualCol(req.body) &&
!(await Column.checkTitleAvailable({

13
packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts

@ -10,11 +10,16 @@ import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import { getViewAndModelFromRequestByAliasOrId } from './helpers';
import apiMetrics from '../../helpers/apiMetrics';
import getAst from '../../../db/sql-data-mapper/lib/sql/helpers/getAst';
import { parseHrtimeToSeconds } from '../helpers';
// todo: Handle the error case where view doesnt belong to model
async function dataList(req: Request, res: Response) {
const startTime = process.hrtime();
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
res.json(await getDataList(model, view, req));
const responseData = await getDataList(model, view, req);
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(responseData);
}
async function dataFindOne(req: Request, res: Response) {
@ -226,8 +231,12 @@ async function dataExist(req: Request, res: Response) {
// todo: Handle the error case where view doesnt belong to model
async function groupedDataList(req: Request, res: Response) {
const startTime = process.hrtime();
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
res.json(await getGroupedDataList(model, view, req));
const groupedData = await getGroupedDataList(model, view, req);
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(groupedData);
}
async function getGroupedDataList(model, view: View, req) {

4
packages/nocodb/src/lib/meta/api/helpers/apiHelpers.ts

@ -0,0 +1,4 @@
export function parseHrtimeToSeconds(hrtime) {
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3);
return seconds;
}

1
packages/nocodb/src/lib/meta/api/helpers/index.ts

@ -1,4 +1,5 @@
import { populateMeta } from './populateMeta';
export * from './columnHelpers';
export * from './apiHelpers';
export { populateMeta };

15
packages/nocodb/src/lib/meta/api/tableApis.ts

@ -148,10 +148,11 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
}
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.clientType;
const sqlClientType = sqlClient.knex.clientType();
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
@ -164,6 +165,16 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`);
}
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
for (const column of req.body.columns) {
if (column.column_name.length > mxColumnLength) {
NcError.badRequest(
`Column name ${column.column_name} exceeds ${mxColumnLength} characters`
);
}
}
req.body.columns = req.body.columns?.map((c) => ({
...getColumnPropsFromUIDT(c as any, base),
cn: c.column_name,
@ -298,7 +309,7 @@ export async function tableUpdate(req: Request<any, any>, res) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.clientType;
const sqlClientType = sqlClient.knex.clientType();
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {

13
packages/nocodb/src/lib/models/Column.ts

@ -1151,4 +1151,17 @@ export default class Column<T = any> implements ColumnType {
colId
);
}
static getMaxColumnNameLength(sqlClientType: string) {
// no limit for sqlite but set as 255
let fieldLengthLimit = 255;
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
fieldLengthLimit = 64;
} else if (sqlClientType === 'pg') {
fieldLengthLimit = 59;
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128;
}
return fieldLengthLimit;
}
}

7
packages/nocodb/webpack.config.js

@ -40,7 +40,8 @@ module.exports = {
globalObject: "typeof self !== 'undefined' ? self : this"
},
node: {
fs: 'empty'
fs: 'empty',
__dirname: false,
},
plugins: [
new webpack.EnvironmentPlugin([
@ -58,5 +59,5 @@ module.exports = {
// }, []),
],
target: 'node',
};
target: 'node'
};

5
tests/playwright/pages/Account/ChangePassword.ts

@ -29,7 +29,8 @@ export class ChangePasswordPage extends BasePage {
await newPassword.fill(newPass);
await confirmPassword.fill(repeatPass);
const submitChangePassword = this.get().locator('button[data-testid="nc-user-settings-form__submit"]').click();
const submitChangePassword = () =>
this.get().locator('button[data-testid="nc-user-settings-form__submit"]').click();
if (networkValidation) {
await this.waitForResponse({
uiAction: submitChangePassword,
@ -37,7 +38,7 @@ export class ChangePasswordPage extends BasePage {
requestUrlPathToMatch: 'api/v1/auth/password/change',
});
} else {
await submitChangePassword;
await submitChangePassword();
}
}

37
tests/playwright/pages/Base.ts

@ -23,30 +23,31 @@ export default abstract class BasePage {
// A function that takes the response body and returns true if the response is the one we are looking for
responseJsonMatcher,
}: {
uiAction: Promise<any>;
uiAction: () => Promise<any>;
requestUrlPathToMatch: string;
httpMethodsToMatch?: string[];
responseJsonMatcher?: ResponseSelector;
}) {
await Promise.all([
this.rootPage.waitForResponse(async res => {
let isResJsonMatched = true;
if (responseJsonMatcher) {
try {
isResJsonMatched = responseJsonMatcher(await res.json());
} catch (e) {
return false;
}
const waitForResponsePromise = this.rootPage.waitForResponse(async res => {
let isResJsonMatched = true;
if (responseJsonMatcher) {
try {
isResJsonMatched = responseJsonMatcher(await res.json());
} catch (e) {
return false;
}
}
return (
res.request().url().includes(requestUrlPathToMatch) &&
httpMethodsToMatch.includes(res.request().method()) &&
isResJsonMatched
);
}),
uiAction,
]);
return (
res.request().url().includes(requestUrlPathToMatch) &&
httpMethodsToMatch.includes(res.request().method()) &&
isResJsonMatched
);
});
const uiActionPromise = uiAction();
await Promise.all([waitForResponsePromise, uiActionPromise]);
}
async attachFile({ filePickUIAction, filePath }: { filePickUIAction: Promise<any>; filePath: string[] }) {

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

@ -7,7 +7,8 @@ export class ExpandedFormPage extends BasePage {
readonly addNewTableButton: Locator;
readonly copyUrlButton: Locator;
readonly toggleCommentsButton: Locator;
readonly moreOptionsButton: Locator;
readonly duplicateRowButton: Locator;
readonly deleteRowButton: Locator;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
@ -15,7 +16,8 @@ export class ExpandedFormPage extends BasePage {
this.addNewTableButton = this.dashboard.get().locator('.nc-add-new-table');
this.copyUrlButton = this.dashboard.get().locator('.nc-copy-row-url:visible');
this.toggleCommentsButton = this.dashboard.get().locator('.nc-toggle-comments:visible');
this.moreOptionsButton = this.dashboard.get().locator('.nc-actions-menu-btn:visible').last();
this.duplicateRowButton = this.dashboard.get().locator('.nc-duplicate-row:visible');
this.deleteRowButton = this.dashboard.get().locator('.nc-delete-row:visible');
}
get() {
@ -23,10 +25,7 @@ export class ExpandedFormPage extends BasePage {
}
async clickDuplicateRow() {
await this.moreOptionsButton.click();
// wait for the menu to appear
await this.rootPage.waitForTimeout(1000);
await this.rootPage.locator('.nc-menu-item:has-text("Duplicate Row")').click();
await this.duplicateRowButton.click();
// wait for loader to disappear
// await this.dashboard.waitForLoaderToDisappear();
@ -34,22 +33,17 @@ export class ExpandedFormPage extends BasePage {
}
async clickDeleteRow() {
await this.moreOptionsButton.click();
// wait for the menu to appear
await this.rootPage.waitForTimeout(1000);
await this.rootPage.locator('.nc-menu-item:has-text("Delete Row")').click();
await this.deleteRowButton.click();
await this.rootPage.locator('.ant-btn-primary:has-text("OK")').click();
}
async isDisabledDuplicateRow() {
await this.moreOptionsButton.click();
const isDisabled = await this.rootPage.locator('.nc-menu-item.disabled:has-text("Duplicate Row")');
const isDisabled = await this.duplicateRowButton;
return await isDisabled.count();
}
async isDisabledDeleteRow() {
await this.moreOptionsButton.click();
const isDisabled = await this.rootPage.locator('.nc-menu-item.disabled:has-text("Delete Row")');
const isDisabled = await this.deleteRowButton;
return await isDisabled.count();
}
@ -98,9 +92,10 @@ export class ExpandedFormPage extends BasePage {
await dropdownList.locator('.ant-dropdown-menu-item:has-text("Save & Stay")').click();
}
const saveRowAction = saveAndExitMode
? this.get().locator('button:has-text("Save & Exit")').click()
: this.get().locator('button:has-text("Save & Stay")').click();
const saveRowAction = () =>
saveAndExitMode
? this.get().locator('button:has-text("Save & Exit")').click()
: this.get().locator('button:has-text("Save & Stay")').click();
if (waitForRowsData) {
await this.waitForResponse({
@ -139,7 +134,7 @@ export class ExpandedFormPage extends BasePage {
}
async close() {
await this.get().locator('button:has-text("Close")').last().click();
await this.get().locator('.nc-close-form').last().click();
}
async openChildCard(param: { column: string; title: string }) {

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

@ -50,7 +50,7 @@ export class ChildList extends BasePage {
}
async openLinkRecord({ linkTableTitle }: { linkTableTitle: string }) {
const openActions = this.get().locator(`text=/Link to '.*${linkTableTitle}'/i`).click();
const openActions = () => this.get().locator(`text=/Link to '.*${linkTableTitle}'/i`).click();
await this.waitForResponse({
requestUrlPathToMatch: '/exclude',
httpMethodsToMatch: ['GET'],

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

@ -330,7 +330,7 @@ export class ColumnPageObject extends BasePage {
}
await this.waitForResponse({
uiAction: this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field"):visible').click(),
uiAction: () => this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field"):visible').click(),
requestUrlPathToMatch: 'api/v1/db/meta/views',
httpMethodsToMatch: ['PATCH'],
});
@ -340,7 +340,7 @@ export class ColumnPageObject extends BasePage {
async save({ isUpdated }: { isUpdated?: boolean } = {}) {
await this.waitForResponse({
uiAction: this.get().locator('button:has-text("Save")').click(),
uiAction: () => this.get().locator('button:has-text("Save")').click(),
requestUrlPathToMatch: 'api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
responseJsonMatcher: json => json['pageInfo'],
@ -375,9 +375,9 @@ export class ColumnPageObject extends BasePage {
await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click();
let menuOption;
if (direction === 'desc') {
menuOption = this.rootPage.locator('li[role="menuitem"]:has-text("Sort Descending"):visible').click();
menuOption = () => this.rootPage.locator('li[role="menuitem"]:has-text("Sort Descending"):visible').click();
} else {
menuOption = this.rootPage.locator('li[role="menuitem"]:has-text("Sort Ascending"):visible').click();
menuOption = () => this.rootPage.locator('li[role="menuitem"]:has-text("Sort Ascending"):visible').click();
}
await this.waitForResponse({

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

@ -79,10 +79,8 @@ export class GridPage extends BasePage {
await this._fillRow({ index, columnHeader, value: rowValue });
const clickOnColumnHeaderToSave = this.get()
.locator(`[data-title="${columnHeader}"]`)
.locator(`span[title="${columnHeader}"]`)
.click();
const clickOnColumnHeaderToSave = () =>
this.get().locator(`[data-title="${columnHeader}"]`).locator(`span[title="${columnHeader}"]`).click();
if (networkValidation) {
await this.waitForResponse({
@ -92,6 +90,7 @@ export class GridPage extends BasePage {
responseJsonMatcher: resJson => resJson?.[columnHeader] === rowValue,
});
} else {
await clickOnColumnHeaderToSave();
await this.rootPage.waitForTimeout(300);
}
@ -111,10 +110,8 @@ export class GridPage extends BasePage {
}) {
await this._fillRow({ index, columnHeader, value });
const clickOnColumnHeaderToSave = this.get()
.locator(`[data-title="${columnHeader}"]`)
.locator(`span[title="${columnHeader}"]`)
.click();
const clickOnColumnHeaderToSave = () =>
this.get().locator(`[data-title="${columnHeader}"]`).locator(`span[title="${columnHeader}"]`).click();
if (networkValidation) {
await this.waitForResponse({
@ -128,6 +125,7 @@ export class GridPage extends BasePage {
responseJsonMatcher: resJson => resJson?.[columnHeader] === value,
});
} else {
await clickOnColumnHeaderToSave();
await this.rootPage.waitForTimeout(300);
}
@ -231,7 +229,7 @@ export class GridPage extends BasePage {
async clickPagination({ page }: { page: string }) {
await this.waitForResponse({
uiAction: (await this.pagination({ page })).click(),
uiAction: async () => (await this.pagination({ page })).click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: '/views/',
responseJsonMatcher: resJson => resJson?.pageInfo,

2
tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

@ -65,7 +65,7 @@ export class ImportTemplatePage extends BasePage {
await this.waitForResponse({
requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
uiAction: this.get().locator('button:has-text("Import"):visible').click(),
uiAction: () => this.get().locator('button:has-text("Import"):visible').click(),
});
await this.dashboard.waitForTabRender({
title: tblList[0],

2
tests/playwright/pages/Dashboard/Settings/Acl.ts

@ -19,7 +19,7 @@ export class AclPage extends BasePage {
async save() {
await this.waitForResponse({
uiAction: this.get().locator(`button:has-text("Save")`).click(),
uiAction: () => this.get().locator(`button:has-text("Save")`).click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/visibility-rules',
});

2
tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts

@ -14,7 +14,7 @@ export class MiscSettingsPage extends BasePage {
}
async clickShowM2MTables() {
const clickAction = this.get().locator('input[type="checkbox"]').first().click();
const clickAction = () => this.get().locator('input[type="checkbox"]').first().click();
await this.waitForResponse({
uiAction: clickAction,
requestUrlPathToMatch: 'tables?includeM2M',

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

@ -54,7 +54,7 @@ export class TreeViewPage extends BasePage {
if (networkResponse === true) {
await this.waitForResponse({
uiAction: this.get().locator(`.nc-project-tree-tbl-${title}`).click(),
uiAction: () => this.get().locator(`.nc-project-tree-tbl-${title}`).click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: `/api/v1/db/data/noco/`,
responseJsonMatcher: json => json.pageInfo,
@ -74,7 +74,7 @@ export class TreeViewPage extends BasePage {
await this.dashboard.get().getByPlaceholder('Enter table name').fill(title);
await this.waitForResponse({
uiAction: this.dashboard.get().locator('button:has-text("Submit")').click(),
uiAction: () => this.dashboard.get().locator('button:has-text("Submit")').click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `/api/v1/db/meta/projects/`,
responseJsonMatcher: json => json.title === title && json.type === 'table',
@ -101,7 +101,7 @@ export class TreeViewPage extends BasePage {
await this.dashboard.get().locator('div.nc-project-menu-item:has-text("Delete")').click();
await this.waitForResponse({
uiAction: this.dashboard.get().locator('button:has-text("Yes")').click(),
uiAction: () => this.dashboard.get().locator('button:has-text("Yes")').click(),
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: `/api/v1/db/meta/tables/`,
});

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

@ -38,10 +38,8 @@ export class ViewSidebarPage extends BasePage {
private async createView({ title, locator }: { title: string; locator: Locator }) {
await locator.click();
await this.rootPage.locator('input[id="form_item_title"]:visible').fill(title);
const submitAction = this.rootPage
.locator('.ant-modal-content')
.locator('button:has-text("Submit"):visible')
.click();
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button:has-text("Submit"):visible').click();
await this.waitForResponse({
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/',
@ -128,10 +126,8 @@ export class ViewSidebarPage extends BasePage {
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-copy-icon')
.click();
const submitAction = this.rootPage
.locator('.ant-modal-content')
.locator('button:has-text("Submit"):visible')
.click();
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button:has-text("Submit"):visible').click();
await this.waitForResponse({
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/',

2
tests/playwright/pages/Dashboard/WebhookForm/index.ts

@ -105,7 +105,7 @@ export class WebhookFormPage extends BasePage {
}
async save() {
const saveAction = this.saveButton.click();
const saveAction = () => this.saveButton.click();
await this.waitForResponse({
uiAction: saveAction,
requestUrlPathToMatch: '/hooks',

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

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

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

@ -75,7 +75,7 @@ export class CellPageObject extends BasePage {
async inCellExpand({ index, columnHeader }: CellProps) {
await this.get({ index, columnHeader }).hover();
await this.waitForResponse({
uiAction: this.get({ index, columnHeader }).locator('.nc-action-icon >> nth=0').click(),
uiAction: () => this.get({ index, columnHeader }).locator('.nc-action-icon >> nth=0').click(),
requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});

15
tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts

@ -17,10 +17,8 @@ export class ToolbarFieldsPage extends BasePage {
// todo: Click and toggle are similar method. Remove one of them
async toggle({ title, isLocallySaved }: { title: string; isLocallySaved?: boolean }) {
await this.toolbar.clickFields();
const toggleColumn = this.get()
.locator(`[data-testid="nc-fields-menu-${title}"]`)
.locator('input[type="checkbox"]')
.click();
const toggleColumn = () =>
this.get().locator(`[data-testid="nc-fields-menu-${title}"]`).locator('input[type="checkbox"]').click();
await this.waitForResponse({
uiAction: toggleColumn,
@ -43,7 +41,8 @@ export class ToolbarFieldsPage extends BasePage {
async click({ title, isLocallySaved }: { title: string; isLocallySaved?: boolean }) {
await this.waitForResponse({
uiAction: this.get().locator(`[data-testid="nc-fields-menu-${title}"]`).locator('input[type="checkbox"]').click(),
uiAction: () =>
this.get().locator(`[data-testid="nc-fields-menu-${title}"]`).locator('input[type="checkbox"]').click(),
requestUrlPathToMatch: isLocallySaved ? '/api/v1/db/public/' : '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});
@ -53,7 +52,7 @@ export class ToolbarFieldsPage extends BasePage {
async hideAll({ isLocallySaved }: { isLocallySaved?: boolean } = {}) {
await this.toolbar.clickFields();
await this.waitForResponse({
uiAction: this.get().locator(`button:has-text("Hide all")`).click(),
uiAction: () => this.get().locator(`button:has-text("Hide all")`).click(),
requestUrlPathToMatch: isLocallySaved ? '/api/v1/db/public/' : '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});
@ -63,7 +62,7 @@ export class ToolbarFieldsPage extends BasePage {
async showAll({ isLocallySaved }: { isLocallySaved?: boolean } = {}) {
await this.toolbar.clickFields();
await this.waitForResponse({
uiAction: this.get().locator(`button:has-text("Show all")`).click(),
uiAction: () => this.get().locator(`button:has-text("Show all")`).click(),
requestUrlPathToMatch: isLocallySaved ? '/api/v1/db/public/' : '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});
@ -73,7 +72,7 @@ export class ToolbarFieldsPage extends BasePage {
async toggleShowSystemFields({ isLocallySaved }: { isLocallySaved?: boolean } = {}) {
await this.toolbar.clickFields();
await this.waitForResponse({
uiAction: this.get().locator(`.nc-fields-show-system-fields`).click(),
uiAction: () => this.get().locator(`.nc-fields-show-system-fields`).click(),
requestUrlPathToMatch: isLocallySaved ? '/api/v1/db/public/' : '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});

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

@ -137,7 +137,7 @@ export class ToolbarFilterPage extends BasePage {
}
break;
default:
fillFilter = this.rootPage.locator('.nc-filter-value-select > input').last().fill(value);
fillFilter = () => this.rootPage.locator('.nc-filter-value-select > input').last().fill(value);
await this.waitForResponse({
uiAction: fillFilter,
httpMethodsToMatch: ['GET'],
@ -154,7 +154,7 @@ export class ToolbarFilterPage extends BasePage {
await this.toolbar.clickFilter();
if (networkValidation) {
await this.waitForResponse({
uiAction: this.get().locator('.nc-filter-item-remove-btn').click(),
uiAction: () => this.get().locator('.nc-filter-item-remove-btn').click(),
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: '/api/v1/db/meta/filters/',
});

11
tests/playwright/pages/Dashboard/common/Toolbar/Sort.ts

@ -61,11 +61,12 @@ export class ToolbarSortPage extends BasePage {
// await this.toolbar.parent.dashboard.waitForLoaderToDisappear();
await this.rootPage.locator('.nc-sort-dir-select').last().click();
const selectSortDirection = this.rootPage
.locator('.nc-dropdown-sort-dir')
.locator('.ant-select-item')
.nth(isAscending ? 0 : 1)
.click();
const selectSortDirection = () =>
this.rootPage
.locator('.nc-dropdown-sort-dir')
.locator('.ant-select-item')
.nth(isAscending ? 0 : 1)
.click();
await this.waitForResponse({
uiAction: selectSortDirection,

6
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -82,10 +82,10 @@ export class ToolbarPage extends BasePage {
}: { networkValidation?: boolean } = {}) {
const menuOpen = await this.filter.get().isVisible();
const clickFilterAction = this.get().locator(`button.nc-filter-menu-btn`).click();
const clickFilterAction = () => this.get().locator(`button.nc-filter-menu-btn`).click();
// Wait for the menu to close
if (menuOpen) {
await clickFilterAction;
await clickFilterAction();
await this.filter.get().waitFor({ state: 'hidden' });
} else {
if (networkValidation) {
@ -96,7 +96,7 @@ export class ToolbarPage extends BasePage {
httpMethodsToMatch: ['GET'],
});
} else {
await clickFilterAction;
await clickFilterAction();
}
}
}

8
tests/playwright/pages/ProjectsPage/index.ts

@ -26,7 +26,7 @@ export class ProjectsPage extends BasePage {
await this.rootPage.locator(`.nc-metadb-project-name`).waitFor();
await this.rootPage.locator(`input.nc-metadb-project-name`).fill(name);
const createProjectSubmitAction = this.rootPage.locator(`button:has-text("Create")`).click();
const createProjectSubmitAction = () => this.rootPage.locator(`button:has-text("Create")`).click();
await this.waitForResponse({
uiAction: createProjectSubmitAction,
httpMethodsToMatch: ['POST'],
@ -42,7 +42,7 @@ export class ProjectsPage extends BasePage {
}
async reloadProjects() {
const reloadUiAction = this.get().locator('[data-testid="projects-reload-button"]').click();
const reloadUiAction = () => this.get().locator('[data-testid="projects-reload-button"]').click();
await this.waitForResponse({
uiAction: reloadUiAction,
requestUrlPathToMatch: '/api/v1/db/meta/projects',
@ -119,7 +119,7 @@ export class ProjectsPage extends BasePage {
await this.get().locator(`[data-testid="delete-project-${title}"]`).click();
const deleteProjectAction = this.rootPage.locator(`button:has-text("Yes")`).click();
const deleteProjectAction = () => this.rootPage.locator(`button:has-text("Yes")`).click();
await this.waitForResponse({
uiAction: deleteProjectAction,
httpMethodsToMatch: ['DELETE'],
@ -149,7 +149,7 @@ export class ProjectsPage extends BasePage {
await project.locator('input.nc-metadb-project-name').fill(newTitle);
// press enter to save
const submitAction = project.locator('input.nc-metadb-project-name').press('Enter');
const submitAction = () => project.locator('input.nc-metadb-project-name').press('Enter');
await this.waitForResponse({
uiAction: submitAction,
requestUrlPathToMatch: 'api/v1/db/meta/projects/',

2
tests/playwright/pages/SharedForm/index.ts

@ -16,7 +16,7 @@ export class SharedFormPage extends BasePage {
async submit() {
await this.waitForResponse({
uiAction: this.get().getByTestId('shared-form-submit-button').click(),
uiAction: () => this.get().getByTestId('shared-form-submit-button').click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/rows',
});

4
tests/playwright/tests/expandedFormUrl.spec.ts

@ -174,7 +174,7 @@ test.describe('Expanded record duplicate & delete options', () => {
// expand row, duplicate & verify menu
await dashboard.grid.openExpandedRow({ index: 0 });
await dashboard.expandedForm.clickDuplicateRow();
expect(await dashboard.expandedForm.isDisabledDeleteRow()).toBe(1);
expect(await dashboard.expandedForm.isDisabledDuplicateRow()).toBe(1);
expect(await dashboard.expandedForm.isDisabledDeleteRow()).toBe(0);
expect(await dashboard.expandedForm.isDisabledDuplicateRow()).toBe(0);
});
});

137
tests/playwright/tests/filters.spec.ts

@ -56,7 +56,7 @@ async function verifyFilter(param: {
return;
}
await toolbar.clickFilter({ networkValidation: false });
await toolbar.clickFilter();
await toolbar.filter.add({
columnTitle: param.column,
opType: param.opType,
@ -64,7 +64,7 @@ async function verifyFilter(param: {
isLocallySaved: false,
dataType: param?.dataType,
});
await toolbar.clickFilter({ networkValidation: false });
await toolbar.clickFilter();
// verify filtered rows
await validateRowArray({
@ -72,7 +72,7 @@ async function verifyFilter(param: {
});
// Reset filter
await toolbar.filter.reset({ networkValidation: false });
await toolbar.filter.reset();
}
// Number based filters
@ -689,6 +689,137 @@ test.describe('Filter Tests: AddOn', () => {
});
});
// Virtual columns
//
test.describe('Filter Tests: Link to another record, Lookup, Rollup', () => {
async function linkToAnotherRecordFilterTest() {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
// add filter for CityList column
const filterList = [
{ op: 'is', value: 'Kabul', rowCount: 1 },
{ op: 'is not', value: 'Kabul', rowCount: 108 },
{ op: 'is like', value: 'bad', rowCount: 2 },
{ op: 'is not like', value: 'bad', rowCount: 107 },
{ op: 'is blank', value: null, rowCount: 0 },
{ op: 'is not blank', value: null, rowCount: 109 },
];
for (let i = 0; i < filterList.length; i++) {
await verifyFilter({
column: 'City List',
opType: filterList[i].op,
value: filterList[i].value,
result: { rowCount: filterList[i].rowCount },
dataType: 'LinkToAnotherRecord',
});
}
}
async function lookupFilterTest() {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'City', networkResponse: false });
// Create LookUp column
await dashboard.grid.column.create({
title: 'Lookup',
type: 'Lookup',
childTable: 'Address List',
childColumn: 'PostalCode',
});
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
// add filter for CityList column
const filterList = [
{ op: 'is equal', value: '4166', rowCount: 1 },
{ op: 'is not equal', value: '4166', rowCount: 599 },
{ op: 'is like', value: '41', rowCount: 19 },
{ op: 'is not like', value: '41', rowCount: 581 },
{ op: 'is blank', value: null, rowCount: 1 },
{ op: 'is not blank', value: null, rowCount: 599 },
];
for (let i = 0; i < filterList.length; i++) {
await verifyFilter({
column: 'Lookup',
opType: filterList[i].op,
value: filterList[i].value,
result: { rowCount: filterList[i].rowCount },
dataType: 'Lookup',
});
}
}
async function rollupFilterTest() {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'City', networkResponse: false });
// Create LookUp column
await dashboard.grid.column.create({
title: 'Rollup',
type: 'Rollup',
childTable: 'Address List',
childColumn: 'PostalCode',
rollupType: 'Sum',
});
// Enable NULL & EMPTY filters
await dashboard.gotoSettings();
await dashboard.settings.toggleNullEmptyFilters();
// add filter for CityList column
const filterList = [
{ op: 'is equal', value: '4166', rowCount: 1 },
{ op: 'is not equal', value: '4166', rowCount: 599 },
{ op: 'is like', value: '41', rowCount: 19 },
{ op: 'is not like', value: '41', rowCount: 581 },
{ op: 'is blank', value: null, rowCount: 2 },
{ op: 'is not blank', value: null, rowCount: 598 },
];
for (let i = 0; i < filterList.length; i++) {
await verifyFilter({
column: 'Lookup',
opType: filterList[i].op,
value: filterList[i].value,
result: { rowCount: filterList[i].rowCount },
dataType: 'Lookup',
});
}
}
test.beforeEach(async ({ page }) => {
context = await setup({ page });
dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar;
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
});
test('Filter: LTAR columns', async () => {
await linkToAnotherRecordFilterTest();
});
test('Filter: Lookup columns', async () => {
await lookupFilterTest();
});
test.skip('Filter: Rollup columns', async () => {
await rollupFilterTest();
});
});
// Rest of tests
//

Loading…
Cancel
Save