Browse Source

Merge branch 'develop' into feat/kanban-view

pull/3818/head
Wing-Kam Wong 2 years ago
parent
commit
6c9948d818
  1. 9
      .all-contributorsrc
  2. 61
      .github/ISSUE_TEMPLATE/--bug-report.yaml
  3. 40
      .github/ISSUE_TEMPLATE/--feature-request.yaml
  4. 23
      .github/ISSUE_TEMPLATE/I18n_translations.md
  5. 52
      .github/ISSUE_TEMPLATE/bug_report.md
  6. 27
      .github/ISSUE_TEMPLATE/feature_request.md
  7. 32
      .github/ISSUE_TEMPLATE/i18n-translation-request.md
  8. 35
      .github/developer-certificate-of-origin
  9. 1
      README.md
  10. 2
      packages/nc-gui/components.d.ts
  11. 5
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  12. 4
      packages/nc-gui/components/dashboard/settings/Modal.vue
  13. 1
      packages/nc-gui/components/smartsheet-header/Menu.vue
  14. 2
      packages/nc-gui/components/smartsheet-toolbar/SharedViewList.vue
  15. 10
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  16. 12
      packages/nc-gui/composables/useUIPermission/index.ts
  17. 43
      packages/nc-gui/composables/useUIPermission/rolePermissions.ts
  18. 15
      packages/nc-gui/layouts/base.vue
  19. 10
      packages/nc-gui/middleware/auth.global.ts
  20. 1
      packages/nc-gui/nuxt-shim.d.ts
  21. 19
      packages/nc-gui/pages/index/apps.vue
  22. 10
      packages/noco-docs/content/en/setup-and-usages/meta-management.md
  23. 2
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  24. 20
      packages/nocodb/src/lib/meta/api/columnApis.ts
  25. 6
      packages/nocodb/src/lib/meta/api/projectUserApis.ts
  26. 4
      packages/nocodb/src/lib/meta/api/tableApis.ts
  27. 12
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  28. 6
      packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts
  29. 17
      packages/nocodb/src/lib/models/Audit.ts
  30. 34
      packages/nocodb/src/lib/utils/projectAcl.ts
  31. 2
      packages/nocodb/tests/unit/index.test.ts
  32. 5
      scripts/cypress/integration/common/00_pre_configurations.js
  33. 5
      scripts/cypress/integration/common/1a_table_operations.js
  34. 1
      scripts/cypress/integration/common/1b_table_column_operations.js
  35. 2
      scripts/cypress/integration/common/2b_table_with_m2m_column.js
  36. 10
      scripts/cypress/integration/common/3a_filter_sort_fields_operations.js
  37. 6
      scripts/cypress/integration/common/3c_lookup_column.js
  38. 8
      scripts/cypress/integration/common/3d_rollup_column.js
  39. 4
      scripts/cypress/integration/common/3e_duration_column.js
  40. 7
      scripts/cypress/integration/common/3f_link_to_another_record.js
  41. 1
      scripts/cypress/integration/common/4a_table_view_grid_gallery_form.js
  42. 3
      scripts/cypress/integration/common/4c_form_view_detailed.js
  43. 3
      scripts/cypress/integration/common/4e_form_view_share.js
  44. 8
      scripts/cypress/integration/common/4f_pg_grid_view_share.js
  45. 14
      scripts/cypress/integration/common/4g_table_view_expanded_form.js
  46. 51
      scripts/cypress/integration/common/5a_user_role.js
  47. 2
      scripts/cypress/integration/common/5b_preview_role.js
  48. 85
      scripts/cypress/integration/common/5c_super_user_role.js
  49. 14
      scripts/cypress/integration/common/6f_attachments.js
  50. 4
      scripts/cypress/integration/common/6g_base_share.js
  51. 10
      scripts/cypress/integration/common/7a_create_project_from_excel.js
  52. 7
      scripts/cypress/integration/common/8a_webhook.js
  53. 7
      scripts/cypress/integration/common/9a_QuickTest.js
  54. 5
      scripts/cypress/integration/spec/roleValidation.spec.js
  55. 1
      scripts/cypress/integration/test/explicitLogin.js
  56. 2
      scripts/cypress/integration/test/pg-restRoles.js
  57. 3
      scripts/cypress/integration/test/restRoles.js
  58. 2
      scripts/cypress/integration/test/xcdb-restRoles.js
  59. 24
      scripts/cypress/support/commands.js
  60. 43
      scripts/cypress/support/page_objects/mainPage.js
  61. 13
      scripts/cypress/support/page_objects/navigation.js

9
.all-contributorsrc

@ -873,6 +873,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "chetanverma16",
"name": "Chetan Verma",
"avatar_url": "https://avatars.githubusercontent.com/u/16558205?v=4",
"profile": "http://chetanverma.com/",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

61
.github/ISSUE_TEMPLATE/--bug-report.yaml

@ -0,0 +1,61 @@
name: 🐛 Bug Report
description: Create a bug report to help improve NocoDB
title: "🐛 Bug: "
labels: [Type : Bug]
assignees:
- o1lab
body:
- type: markdown
attributes:
value: |
Thank you ❤ for taking the time to fill out this feature request report!
- type: checkboxes
attributes:
label: Please confirm if bug report does NOT exists already ?
description: We kindly ask that you [search](https://github.com/nocodb/nocodb/issues?q=is%3Aissue+sort%3Acreated-desc+) to see if an issue already exists for your bug
options:
- label: I confirm there is no existing issue for this
required: true
- type: textarea
attributes:
label: Steps to reproduce ?
description: A clear and concise steps on how to reproduce the issue. More details the better.
validations:
required: true
- type: textarea
attributes:
label: Desired Behavior
description: Describe the solution you'd like. A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Project Details
description: Where to find it ? (See [YouTube video](https://www.youtube.com/watch?v=AUSNN-RCwhE) or [Docs](https://docs.nocodb.com/FAQs#how-to-check-my-project-info-))
placeholder: |
or provide the following info
```
NocoDB used as docker : true / false
NocoDB version :
Database used in NC_DB URL : mysql | pg | mssql | sqlite3 / (defaults to sqlite3 if empty)
Project was created by clicking : New Project | New Project by connecting to external database
Database on which spreadsheet is created : mysql | pg | mssql | sqlite3 / (defaults to sqlite3 if empty)
OS on which NocoDB is running :
Node.js version if running as node :
Database version :
```
validations:
required: true
- type: textarea
attributes:
label: Attachements
description: Add any relevant attachemnts here
placeholder: |
> Drag & drop relevant image or videos
validations:
required: false

40
.github/ISSUE_TEMPLATE/--feature-request.yaml

@ -0,0 +1,40 @@
name: 🔦 Feature request
description: Suggest a new/missing feature for NocoDB
title: "🔦 Feature: "
labels: [Type : Feature]
assignees:
- o1lab
body:
- type: markdown
attributes:
value: |
Thank you ❤ for taking the time to fill out this feature request report!
- type: checkboxes
attributes:
label: Please confirm if feature request does NOT exists already ?
description: We kindly ask that you [search](https://github.com/nocodb/nocodb/issues?q=is%3Aissue+sort%3Acreated-desc+) to see if an issue already exists for your feature
options:
- label: I confirm there is no existing issue for this
required: true
- type: textarea
attributes:
label: Describe the usecase for the feature
description: A clear and concise description of the feature you're interested in.
validations:
required: true
- type: textarea
attributes:
label: Suggested Solution
description: Describe the solution you'd like. A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional Context
description: Add any other context about the problem here.
validations:
required: false

23
.github/ISSUE_TEMPLATE/I18n_translations.md

@ -1,23 +0,0 @@
# i18n translation request
**Please enter the following details**
1. Select applicalbe
- [ ] **New language support request**
- [ ] **Existing language translation corrections request**
Verify if language support already exists in NocoDB i18n master spreadsheet [here](https://docs.google.com/spreadsheets/d/1kGp92yLwhs1l7lwwgeor3oN1dFl7JZWuQOa4WSeZ0TE/edit#gid=2076107172)
2. Associated language code (pick from [here](https://developers.google.com/admin-sdk/directory/v1/languages)):
3. Google spreadsheet
- Procedure to share google spread sheet can be found [here](https://support.google.com/docs/answer/2494822?hl=en&co=GENIE.Platform%3DDesktop#zippy=%2Cshare-a-file-publicly)
- When requested, select public sharable link.
- Alternatively, you can download & attach spreadsheet here along with the issue as well.
- [ ] **Attached**
- [ ] **Shared link**:
4. Summary of the changes made
- You can highlight modified items in Spreadsheet attached/shared, to help us quickly identify change set
5. Any specific message to be conveyed to moderators?
- Help us simplify process if you find something hard about it

52
.github/ISSUE_TEMPLATE/bug_report.md

@ -1,52 +0,0 @@
---
name: Bug report
about: Please create a clear error report to help us improve
title: ''
labels: ''
assignees: ''
---
**Please enter the following details**
Copy and Paste Project Info - Tutorials: How to check my Project info? ([YouTube Tutorial](https://www.youtube.com/watch?v=AUSNN-RCwhE) or [Documentation](https://docs.nocodb.com/FAQs#how-to-check-my-project-info-))
```
Node: **v16.14.0**
Arch: **arm64**
Platform: **darwin**
Docker: **false**
Database: **mysql2**
ProjectOnRootDB: **false**
RootDB: **mysql2**
PackageVersion: **0.90.5**
```
or provide the following info
```
NocoDB used as docker : true / false
NocoDB version :
Database used in NC_DB URL : mysql | pg | mssql | sqlite3 / (defaults to sqlite3 if empty)
Project was created by clicking : New Project | New Project by connecting to external database
Database on which spreadsheet is created : mysql | pg | mssql | sqlite3 / (defaults to sqlite3 if empty)
OS on which NocoDB is running :
Node.js version if running as node :
Database version :
```
**Steps To Reproduce**
1. Go to '...'
2. Click on '....'
3. See error
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
Join our discord : https://discord.gg/5RgZmkW for realtime help.

27
.github/ISSUE_TEMPLATE/feature_request.md

@ -1,27 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] "
labels: ''
assignees: ''
---
## Recommending Features
NocoDB thrives on community needs. Many of our existing features are derived from community requirements & we will continue to build that way. We invite ideas to make NocoDB better. Do note that, we being an open-source team are constrained by resources & will be able to pick only limited **high demand** ideas for release. So, if you wish to pick this feature request early - spread the word, discuss it in Discord/blog/Reddit, ask your peers & colleagues to vote up for this feature request!
**Need for this feature**
What purpose does your Idea serve? What use cases does it solve?
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

32
.github/ISSUE_TEMPLATE/i18n-translation-request.md

@ -1,32 +0,0 @@
---
name: i18n translation request
about: translation & localisation support
title: "[i18n] Language support extension- <language code>"
labels: i18n translation
assignees: dstala
---
# i18n translation request
**Please enter the following details**
1. Select applicalbe
- [ ] **New language support request**
- [ ] **Existing language translation corrections request**
Verify if language support already exists in the NocoDB i18n master spreadsheet [here](https://docs.google.com/spreadsheets/d/1kGp92yLwhs1l7lwwgeor3oN1dFl7JZWuQOa4WSeZ0TE/edit#gid=2076107172)
2. Associated language code (pick from [here](https://developers.google.com/admin-sdk/directory/v1/languages)):
3. Google spreadsheet
- Procedure to share google spreadsheet can be found [here](https://support.google.com/docs/answer/2494822?hl=en&co=GENIE.Platform%3DDesktop#zippy=%2Cshare-a-file-publicly)
- When requested, select public sharable link.
- Alternatively, you can download & attach a spreadsheet here along with the issue as well.
- [ ] **Attached**
- [ ] **Shared link**:
4. Summary of the changes made
- You can highlight modified items in the Spreadsheet attached/shared, to help us quickly identify changeset
5. Any specific message to be conveyed to moderators?
- Help us simplify the process if you find something hard about it

35
.github/developer-certificate-of-origin

@ -1,35 +0,0 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

1
README.md

@ -454,6 +454,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td> <td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td>
<td align="center"><a href="http://asheerrizvi.com"><img src="https://avatars.githubusercontent.com/u/17976252?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Asheer Rizvi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=asheerrizvi" title="Code">💻</a></td> <td align="center"><a href="http://asheerrizvi.com"><img src="https://avatars.githubusercontent.com/u/17976252?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Asheer Rizvi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=asheerrizvi" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dolsem"><img src="https://avatars.githubusercontent.com/u/14323955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Denis Olsem</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dolsem" title="Code">💻</a></td> <td align="center"><a href="https://github.com/dolsem"><img src="https://avatars.githubusercontent.com/u/14323955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Denis Olsem</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dolsem" title="Code">💻</a></td>
<td align="center"><a href="http://chetanverma.com/"><img src="https://avatars.githubusercontent.com/u/16558205?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chetan Verma</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=chetanverma16" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

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

@ -192,6 +192,8 @@ declare module '@vue/runtime-core' {
MdiSort: typeof import('~icons/mdi/sort')['default'] MdiSort: typeof import('~icons/mdi/sort')['default']
MdiStar: typeof import('~icons/mdi/star')['default'] MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default'] MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default']
MdiStorefrontSutline: typeof import('~icons/mdi/storefront-sutline')['default']
MdiTable: typeof import('~icons/mdi/table')['default'] MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default'] MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default'] MdiTableLarge: typeof import('~icons/mdi/table-large')['default']

5
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -115,7 +115,7 @@ onMounted(async () => {
:body-style="{ width: '100%' }" :body-style="{ width: '100%' }"
> >
<div class="install-btn flex flex-row justify-end space-x-1"> <div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" outlined @click="showInstallPluginModal(app)"> <a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit"> <div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<MdiEditIcon class="pr-0.5" :height="12" /> <MdiEditIcon class="pr-0.5" :height="12" />
Edit Edit
@ -127,7 +127,7 @@ onMounted(async () => {
<div class="flex ml-0.5">Reset</div> <div class="flex ml-0.5">Reset</div>
</div> </div>
</a-button> </a-button>
<a-button v-else size="small" outlined type="primary" ghost @click="showInstallPluginModal(app)"> <a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install"> <div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlusIcon /> <MdiPlusIcon />
Install Install
@ -185,7 +185,6 @@ onMounted(async () => {
.caption { .caption {
font-size: 0.7rem; font-size: 0.7rem;
color: #242f3e;
} }
.avatar { .avatar {

4
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -77,6 +77,8 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth') $e('c:settings:team-auth')
}, },
}, },
...(isUIAllowed('appStore')
? {
appStore: { appStore: {
// App Store // App Store
title: t('title.appStore'), title: t('title.appStore'),
@ -91,6 +93,8 @@ const tabsInfo: TabGroup = {
$e('c:settings:appstore') $e('c:settings:appstore')
}, },
}, },
}
: {}),
projMetaData: { projMetaData: {
// Project Metadata // Project Metadata
title: t('title.projMeta'), title: t('title.projMeta'),

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

@ -23,6 +23,7 @@ const { getMeta } = useMetas()
const deleteColumn = () => const deleteColumn = () =>
Modal.confirm({ Modal.confirm({
title: h('div', ['Do you want to delete ', h('span', { class: 'font-weight-bold' }, [column?.value?.title]), ' column ?']), title: h('div', ['Do you want to delete ', h('span', { class: 'font-weight-bold' }, [column?.value?.title]), ' column ?']),
wrapClassName: 'nc-modal-column-delete',
okText: t('general.delete'), okText: t('general.delete'),
okType: 'danger', okType: 'danger',
cancelText: t('general.cancel'), cancelText: t('general.cancel'),

2
packages/nc-gui/components/smartsheet-toolbar/SharedViewList.vue

@ -125,7 +125,7 @@ const deleteLink = async (id: string) => {
<template #default="{ record }"> <template #default="{ record }">
<div class="flex items-center items-center gap-1"> <div class="flex items-center items-center gap-1">
<template v-if="record.password"> <template v-if="record.password">
<span class="h-min">{{ record.showPassword ? record.password : '***************************' }}</span> <span class="h-min max-w-[250px]">{{ record.showPassword ? record.password : Array(record.password.length + 1).join("*") }}</span>
<component <component
:is="record.showPassword ? MdiVisibilityOffIcon : MdiVisibilityOnIcon" :is="record.showPassword ? MdiVisibilityOffIcon : MdiVisibilityOnIcon"
@click="record.showPassword = !record.showPassword" @click="record.showPassword = !record.showPassword"

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

@ -75,14 +75,18 @@ const copyRecordUrl = () => {
<template #title> <template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div> <div class="text-center w-full">{{ $t('general.reload') }}</div>
</template> </template>
<mdi-reload v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1" @click="loadRow" /> <mdi-reload v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1 min-w-4" @click="loadRow" />
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<!-- todo: i18n --> <!-- todo: i18n -->
<div class="text-center w-full">Copy record URL</div> <div class="text-center w-full">Copy record URL</div>
</template> </template>
<mdi-link v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url" @click="copyRecordUrl" /> <mdi-link
v-if="!isNew"
class="cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url min-w-4"
@click="copyRecordUrl"
/>
</a-tooltip> </a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom"> <a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Toggle comments draw --> <!-- Toggle comments draw -->
@ -92,7 +96,7 @@ const copyRecordUrl = () => {
<MdiCommentTextOutline <MdiCommentTextOutline
v-if="isUIAllowed('rowComments') && !isNew" v-if="isUIAllowed('rowComments') && !isNew"
v-e="['c:row-expand:comment-toggle']" v-e="['c:row-expand:comment-toggle']"
class="cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1" class="cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1 min-w-4"
@click="commentsDrawer = !commentsDrawer" @click="commentsDrawer = !commentsDrawer"
/> />
</a-tooltip> </a-tooltip>

12
packages/nc-gui/composables/useUIPermission/index.ts

@ -11,6 +11,14 @@ const hasPermission = (role: Role | ProjectRole, hasRole: boolean, permission: P
if (isString(rolePermission) && rolePermission === '*') return true if (isString(rolePermission) && rolePermission === '*') return true
if ('include' in rolePermission && rolePermission.include) {
return !!rolePermission.include[permission as keyof typeof rolePermission.include]
}
if ('exclude' in rolePermission && rolePermission.exclude) {
return !rolePermission.exclude[permission as keyof typeof rolePermission.exclude]
}
return rolePermission[permission as keyof typeof rolePermission] return rolePermission[permission as keyof typeof rolePermission]
} }
@ -20,9 +28,7 @@ export const useUIPermission = createSharedComposable(() => {
const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => { const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => {
if (previewAs.value && !skipPreviewAs) { if (previewAs.value && !skipPreviewAs) {
const hasPreviewPermission = hasPermission(previewAs.value, true, permission) return hasPermission(previewAs.value, true, permission)
if (hasPreviewPermission) return true
} }
return Object.entries(allRoles.value).some(([role, hasRole]) => return Object.entries(allRoles.value).some(([role, hasRole]) =>

43
packages/nc-gui/composables/useUIPermission/rolePermissions.ts

@ -2,23 +2,41 @@ import { ProjectRole, Role } from '~/lib'
const rolePermissions = { const rolePermissions = {
// general role permissions // general role permissions
/**
* Each permission value means the following
* `*` - which is wildcard, means all permissions are allowed
* `include` - which is an object, means only the permissions listed in the object are allowed
* `exclude` - which is an object, means all permissions are allowed except the ones listed in the object
* `undefined` or `{}` - which is the default value, means no permissions are allowed
* */
/** todo: enable wildcard permission /** todo: enable wildcard permission
* limited permission due to unexpected behaviour in shared base if opened in same window */ * limited permission due to unexpected behaviour in shared base if opened in same window */
[Role.Super]: { [Role.Super]: '*',
projectTheme: true, [Role.Admin]: {} as Record<string, boolean>,
}, [Role.Guest]: {} as Record<string, boolean>,
[Role.Admin]: {},
[Role.Guest]: {},
[Role.User]: { [Role.User]: {
include: {
projectCreate: true, projectCreate: true,
projectActions: true, projectActions: true,
projectSettings: true, projectSettings: true,
}, },
},
// Project role permissions // Project role permissions
[ProjectRole.Creator]: '*', [ProjectRole.Creator]: {
[ProjectRole.Owner]: '*', exclude: {
appStore: true,
},
},
[ProjectRole.Owner]: {
exclude: {
appStore: true,
},
},
[ProjectRole.Editor]: { [ProjectRole.Editor]: {
include: {
smartSheet: true, smartSheet: true,
xcDatatableEditable: true, xcDatatableEditable: true,
column: true, column: true,
@ -36,24 +54,29 @@ const rolePermissions = {
projectSettings: true, projectSettings: true,
newUser: false, newUser: false,
}, },
},
[ProjectRole.Commenter]: { [ProjectRole.Commenter]: {
include: {
smartSheet: true, smartSheet: true,
column: true, column: true,
rowComments: true, rowComments: true,
projectSettings: true, projectSettings: true,
}, },
},
[ProjectRole.Viewer]: { [ProjectRole.Viewer]: {
include: {
smartSheet: true, smartSheet: true,
column: true, column: true,
projectSettings: true, projectSettings: true,
}, },
},
} as const } as const
type RolePermissions = Omit<typeof rolePermissions, 'creator' | 'owner' | 'guest' | 'admin'> type RolePermissions = Omit<typeof rolePermissions, 'guest' | 'admin' | 'super'>
type GetKeys<T> = T extends Record<string, any> ? keyof T : never type GetKeys<T> = T extends Record<any, Record<infer Key, boolean>> ? Key : never
export type Permission<K extends keyof RolePermissions = keyof RolePermissions> = RolePermissions[K] extends Record<string, any> export type Permission<K extends keyof RolePermissions = keyof RolePermissions> = RolePermissions[K] extends Record<any, any>
? GetKeys<RolePermissions[K]> ? GetKeys<RolePermissions[K]>
: never : never

15
packages/nc-gui/layouts/base.vue

@ -13,6 +13,8 @@ const hasSider = ref(false)
const sidebar = ref<HTMLDivElement>() const sidebar = ref<HTMLDivElement>()
const { isUIAllowed } = useUIPermission()
const logout = () => { const logout = () => {
signOut() signOut()
navigateTo('/signin') navigateTo('/signin')
@ -87,6 +89,19 @@ hooks.hook('page:finish', () => {
</nuxt-link> </nuxt-link>
</a-menu-item> </a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item v-if="isUIAllowed('appStore')" key="0" class="!rounded-t">
<nuxt-link
v-e="['c:settings:appstore', { page: true }]"
class="nc-project-menu-item group !no-underline"
to="/apps"
>
<MdiStorefrontOutline class="mt-1 group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary">{{ $t('title.appStore') }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" /> <a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b group"> <a-menu-item key="1" class="!rounded-b group">

10
packages/nc-gui/middleware/auth.global.ts

@ -1,6 +1,6 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { defineNuxtRouteMiddleware, navigateTo } from '#app' import { defineNuxtRouteMiddleware, navigateTo } from '#app'
import { useApi, useGlobal } from '#imports' import { useApi, useGlobal, useRoles } from '#imports'
/** /**
* Global auth middleware * Global auth middleware
@ -38,6 +38,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
const { api } = useApi() const { api } = useApi()
const { allRoles } = useRoles()
/** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */ /** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */
if (!state.signedIn && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth() if (!state.signedIn && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth()
@ -68,6 +70,12 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
return navigateTo(from.path) return navigateTo(from.path)
} }
} else { } else {
/** If page is limited to certain users verify the user have the roles */
if (to.meta.allowedRoles && to.meta.allowedRoles.every((role) => !allRoles.value[role])) {
message.error("You don't have enough permission to access the page.")
return navigateTo('/')
}
/** if users are accessing the projects without having enough permissions, redirect to My Projects page */ /** if users are accessing the projects without having enough permissions, redirect to My Projects page */
if (to.params.projectId && from.params.projectId !== to.params.projectId) { if (to.params.projectId && from.params.projectId !== to.params.projectId) {
const user = await api.auth.me({ project_id: to?.params?.projectId as string }) const user = await api.auth.me({ project_id: to?.params?.projectId as string })

1
packages/nc-gui/nuxt-shim.d.ts vendored

@ -28,6 +28,7 @@ declare module 'vue-router' {
public?: boolean public?: boolean
hideHeader?: boolean hideHeader?: boolean
title?: string title?: string
allowedRoles?: Role[]
} }
interface RouteParams { interface RouteParams {

19
packages/nc-gui/pages/index/apps.vue

@ -0,0 +1,19 @@
<script lang="ts" setup>
import AppStore from '~/components/dashboard/settings/AppStore.vue'
import { Role } from '~/lib'
definePageMeta({
requiresAuth: true,
allowedRoles: [Role.Super],
title: 'title.appStore',
})
</script>
<template>
<div class="p-10 h-full overflow-auto">
<h1 class="text-3xl text-center mb-11 nc-app-store-title">{{ $t('title.appStore') }}</h1>
<AppStore />
</div>
</template>
<style scoped></style>

10
packages/noco-docs/content/en/setup-and-usages/meta-management.md

@ -69,9 +69,17 @@ Go to `Project Metadata`, under ``ERD View``, you can see the ERD of your databa
<img width="1419" alt="image" src="https://user-images.githubusercontent.com/35809690/191258324-18bd4ed0-521b-4480-a3f6-fe4660b8ddd5.png"> <img width="1419" alt="image" src="https://user-images.githubusercontent.com/35809690/191258324-18bd4ed0-521b-4480-a3f6-fe4660b8ddd5.png">
### Junction table names within ERD
- Enable `Show M2M Tables` within Miscellaneous tab
- Double click on `Show Columns` to see additional checkboxes get enabled.
- Enabling which you should be able to see junction tables and their table names.
<img width="1681" alt="Show Junction table names for many to many table" src="https://user-images.githubusercontent.com/5435402/192140913-9da37700-28fe-404d-88e8-35ba0c8e2f53.png">
## Miscellaneous ## Miscellaneous
- Enabling, `Show M2M Tables` will show junction tables between many to many tables.
Currently only `Show M2M Tables` can be configurated under Miscellaneous.
<img width="1409" alt="image" src="https://user-images.githubusercontent.com/35809690/191258441-72a12941-2d2b-4a0d-84b8-f7f8783aa4e8.png"> <img width="1409" alt="image" src="https://user-images.githubusercontent.com/35809690/191258441-72a12941-2d2b-4a0d-84b8-f7f8783aa4e8.png">

2
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -1946,7 +1946,7 @@ class BaseModelSqlv2 {
public async afterUpdate(data: any, _trx: any, req): Promise<void> { public async afterUpdate(data: any, _trx: any, req): Promise<void> {
const id = this._extractPksValues(data); const id = this._extractPksValues(data);
Audit.insert({ await Audit.insert({
fk_model_id: this.model.id, fk_model_id: this.model.id,
row_id: id, row_id: id,
op_type: AuditOperationTypes.DATA, op_type: AuditOperationTypes.DATA,

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

@ -646,7 +646,7 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
await table.getColumns(); await table.getColumns();
Audit.insert({ await Audit.insert({
project_id: base.project_id, project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN, op_type: AuditOperationTypes.TABLE_COLUMN,
op_sub_type: AuditOperationSubTypes.CREATED, op_sub_type: AuditOperationSubTypes.CREATED,
@ -911,7 +911,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
]); ]);
} else { } else {
await baseModel.bulkUpdateAll( await baseModel.bulkUpdateAll(
{ where: `(${column.title},eq,${option.title})` }, { where: `(${column.column_name},eq,${option.title})` },
{ [column.column_name]: null }, { [column.column_name]: null },
{ cookie: req } { cookie: req }
); );
@ -966,7 +966,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
} }
} }
let interchange = []; const interchange = [];
// Handle option update // Handle option update
if (column.colOptions?.options) { if (column.colOptions?.options) {
@ -985,11 +985,11 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
); );
} }
let newOp = { const newOp = {
...colBody.colOptions.options.find((el) => option.id === el.id), ...colBody.colOptions.options.find((el) => option.id === el.id),
}; };
if (old_titles.includes(newOp.title)) { if (old_titles.includes(newOp.title)) {
let def_option = { ...newOp }; const def_option = { ...newOp };
let title_counter = 1; let title_counter = 1;
while (old_titles.includes(newOp.title)) { while (old_titles.includes(newOp.title)) {
newOp.title = `${def_option.title}_${title_counter++}`; newOp.title = `${def_option.title}_${title_counter++}`;
@ -1077,7 +1077,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
]); ]);
} else { } else {
await baseModel.bulkUpdateAll( await baseModel.bulkUpdateAll(
{ where: `(${column.title},eq,${option.title})` }, { where: `(${column.column_name},eq,${option.title})` },
{ [column.column_name]: newOp.title }, { [column.column_name]: newOp.title },
{ cookie: req } { cookie: req }
); );
@ -1138,7 +1138,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
} }
for (const ch of interchange) { for (const ch of interchange) {
let newOp = ch.def_option; const newOp = ch.def_option;
if (column.uidt === UITypes.SingleSelect) { if (column.uidt === UITypes.SingleSelect) {
if (driverType === 'mssql') { if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [ await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [
@ -1150,7 +1150,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
]); ]);
} else { } else {
await baseModel.bulkUpdateAll( await baseModel.bulkUpdateAll(
{ where: `(${column.title},eq,${ch.temp_title})` }, { where: `(${column.column_name},eq,${ch.temp_title})` },
{ [column.column_name]: newOp.title }, { [column.column_name]: newOp.title },
{ cookie: req } { cookie: req }
); );
@ -1327,7 +1327,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
...colBody, ...colBody,
}); });
} }
Audit.insert({ await Audit.insert({
project_id: base.project_id, project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN, op_type: AuditOperationTypes.TABLE_COLUMN,
op_sub_type: AuditOperationSubTypes.UPDATED, op_sub_type: AuditOperationSubTypes.UPDATED,
@ -1526,7 +1526,7 @@ export async function columnDelete(req: Request, res: Response<TableType>) {
} }
} }
Audit.insert({ await Audit.insert({
project_id: base.project_id, project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN, op_type: AuditOperationTypes.TABLE_COLUMN,
op_sub_type: AuditOperationSubTypes.DELETED, op_sub_type: AuditOperationSubTypes.DELETED,

6
packages/nocodb/src/lib/meta/api/projectUserApis.ts

@ -87,7 +87,7 @@ async function userInvite(req, res, next): Promise<any> {
); );
} }
Audit.insert({ await Audit.insert({
project_id: req.params.projectId, project_id: req.params.projectId,
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE', op_sub_type: 'INVITE',
@ -188,7 +188,7 @@ async function projectUserUpdate(req, res, next): Promise<any> {
req.body.roles req.body.roles
); );
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'ROLES_MANAGEMENT', op_sub_type: 'ROLES_MANAGEMENT',
user: req.user.email, user: req.user.email,
@ -255,7 +255,7 @@ async function projectUserInviteResend(req, res): Promise<any> {
await sendInviteEmail(user.email, invite_token, req); await sendInviteEmail(user.email, invite_token, req);
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'RESEND_INVITE', op_sub_type: 'RESEND_INVITE',
user: user.email, user: user.email,

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

@ -178,7 +178,7 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
base_id: base.id, base_id: base.id,
}); });
Audit.insert({ await Audit.insert({
project_id: project.id, project_id: project.id,
op_type: AuditOperationTypes.TABLE, op_type: AuditOperationTypes.TABLE,
op_sub_type: AuditOperationSubTypes.CREATED, op_sub_type: AuditOperationSubTypes.CREATED,
@ -348,7 +348,7 @@ export async function tableDelete(req: Request, res: Response) {
}); });
} }
Audit.insert({ await Audit.insert({
project_id: project.id, project_id: project.id,
op_type: AuditOperationTypes.TABLE, op_type: AuditOperationTypes.TABLE,
op_sub_type: AuditOperationSubTypes.DELETED, op_sub_type: AuditOperationSubTypes.DELETED,

12
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -146,7 +146,7 @@ export async function signup(req: Request, res: Response<TableType>) {
user = (req as any).user; user = (req as any).user;
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'SIGNUP', op_sub_type: 'SIGNUP',
user: user.email, user: user.email,
@ -192,7 +192,7 @@ async function successfulSignIn({
}); });
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'SIGNIN', op_sub_type: 'SIGNIN',
user: user.email, user: user.email,
@ -291,7 +291,7 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
token_version: null, token_version: null,
}); });
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'PASSWORD_CHANGE', op_sub_type: 'PASSWORD_CHANGE',
user: user.email, user: user.email,
@ -341,7 +341,7 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
); );
} }
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'PASSWORD_FORGOT', op_sub_type: 'PASSWORD_FORGOT',
user: user.email, user: user.email,
@ -405,7 +405,7 @@ async function passwordReset(req, res): Promise<any> {
token_version: null, token_version: null,
}); });
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'PASSWORD_RESET', op_sub_type: 'PASSWORD_RESET',
user: user.email, user: user.email,
@ -433,7 +433,7 @@ async function emailVerification(req, res): Promise<any> {
email_verified: true, email_verified: true,
}); });
Audit.insert({ await Audit.insert({
op_type: 'AUTHENTICATION', op_type: 'AUTHENTICATION',
op_sub_type: 'EMAIL_VERIFICATION', op_sub_type: 'EMAIL_VERIFICATION',
user: user.email, user: user.email,

6
packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts

@ -57,7 +57,11 @@ export default function (handlerFn, permissionName) {
return ( return (
hasRole && hasRole &&
projectAcl[name] && projectAcl[name] &&
(projectAcl[name] === '*' || projectAcl[name][permissionName]) (projectAcl[name] === '*' ||
(projectAcl[name].exclude &&
!projectAcl[name].exclude[permissionName]) ||
(projectAcl[name].include &&
projectAcl[name].include[permissionName]))
); );
}); });
if (!isAllowed) { if (!isAllowed) {

17
packages/nocodb/src/lib/models/Audit.ts

@ -31,6 +31,7 @@ export default class Audit implements AuditType {
return audit && new Audit(audit); return audit && new Audit(audit);
} }
// Will only await for Audit insertion if `forceAwait` is true, which will be true in test environment by default
public static async insert( public static async insert(
audit: Partial< audit: Partial<
Audit & { Audit & {
@ -38,14 +39,19 @@ export default class Audit implements AuditType {
updated_at?; updated_at?;
} }
>, >,
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta,
{ forceAwait }: { forceAwait: boolean } = {
forceAwait: process.env['TEST'] === 'true',
}
) { ) {
const insertAudit = async () => {
if (!audit.project_id && audit.fk_model_id) { if (!audit.project_id && audit.fk_model_id) {
audit.project_id = ( audit.project_id = (
await Model.getByIdOrName({ id: audit.fk_model_id }, ncMeta) await Model.getByIdOrName({ id: audit.fk_model_id }, ncMeta)
).project_id; ).project_id;
} }
const auditRec = await ncMeta.metaInsert2(null, null, MetaTable.AUDIT, {
return await ncMeta.metaInsert2(null, null, MetaTable.AUDIT, {
user: audit.user, user: audit.user,
ip: audit.ip, ip: audit.ip,
base_id: audit.base_id, base_id: audit.base_id,
@ -60,8 +66,13 @@ export default class Audit implements AuditType {
created_at: audit.created_at, created_at: audit.created_at,
updated_at: audit.updated_at, updated_at: audit.updated_at,
}); });
};
return auditRec; if (forceAwait) {
return await insertAudit();
} else {
insertAudit();
}
} }
public static async commentsCount(args: { public static async commentsCount(args: {

34
packages/nocodb/src/lib/utils/projectAcl.ts

@ -1,8 +1,25 @@
export default { export default {
owner: '*', owner: {
creator: '*', exclude: {
pluginList: true,
pluginTest: true,
pluginRead: true,
pluginUpdate: true,
isPluginActive: true,
},
},
creator: {
exclude: {
pluginList: true,
pluginTest: true,
pluginRead: true,
pluginUpdate: true,
isPluginActive: true,
},
},
guest: {}, guest: {},
editor: { editor: {
include: {
hideAllColumns: true, hideAllColumns: true,
showAllColumns: true, showAllColumns: true,
auditRowUpdate: true, auditRowUpdate: true,
@ -138,7 +155,9 @@ export default {
upload: true, upload: true,
uploadViaURL: true, uploadViaURL: true,
}, },
},
commenter: { commenter: {
include: {
formViewGet: true, formViewGet: true,
passwordChange: true, passwordChange: true,
// project // project
@ -167,7 +186,7 @@ export default {
dataGroupBy: true, dataGroupBy: true,
commentsCount: true, commentsCount: true,
galleryViewGet: true, alleryViewGet: true,
kanbanViewGet: true, kanbanViewGet: true,
xcTableAndViewList: true, xcTableAndViewList: true,
@ -194,7 +213,9 @@ export default {
xcExportAsCsv: true, xcExportAsCsv: true,
dataCount: true, dataCount: true,
}, },
},
viewer: { viewer: {
include: {
formViewGet: true, formViewGet: true,
passwordChange: true, passwordChange: true,
// project // project
@ -244,14 +265,18 @@ export default {
indexList: true, indexList: true,
list: true, list: true,
xcExportAsCsv: true, xcExportAsCsv: true,
dataCount: true dataCount: true,
},
}, },
user_new: { user_new: {
include: {
passwordChange: true, passwordChange: true,
projectList: true, projectList: true,
}, },
},
super: '*', super: '*',
user: { user: {
include: {
upload: true, upload: true,
uploadViaURL: true, uploadViaURL: true,
passwordChange: true, passwordChange: true,
@ -272,6 +297,7 @@ export default {
xcMetaTablesExportDbToZip: true, xcMetaTablesExportDbToZip: true,
auditRowUpdate: true, auditRowUpdate: true,
}, },
},
}; };
/** /**

2
packages/nocodb/tests/unit/index.test.ts

@ -6,7 +6,7 @@ import TestDbMngr from './TestDbMngr'
import dotenv from 'dotenv'; import dotenv from 'dotenv';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
process.env.TEST = 'test'; process.env.TEST = 'true';
process.env.NC_DISABLE_CACHE = 'true'; process.env.NC_DISABLE_CACHE = 'true';
process.env.NC_DISABLE_TELE = 'true'; process.env.NC_DISABLE_TELE = 'true';

5
scripts/cypress/integration/common/00_pre_configurations.js

@ -187,7 +187,6 @@ export const genTest = (apiType, dbType) => {
} }
} else { } else {
projectsPage.createProject(proj.basic, proj.config); projectsPage.createProject(proj.basic, proj.config);
// cy.wait(5000);
if (dbType === "xcdb") { if (dbType === "xcdb") {
// store base URL- to re-visit and delete form view later // store base URL- to re-visit and delete form view later
let projId; let projId;
@ -240,11 +239,9 @@ export const genTest = (apiType, dbType) => {
cy_createProjectBlock(proj, apiType, dbType); cy_createProjectBlock(proj, apiType, dbType);
} }
// kludge: wait for page load to finish
cy.wait(2000);
// close team & auth tab // close team & auth tab
cy.get("button.ant-tabs-tab-remove").should("exist").click(); cy.get("button.ant-tabs-tab-remove").should("exist").click();
cy.wait(1000); cy.get("button.ant-tabs-tab-remove").should("not.exist");
// first instance of updating local storage information // first instance of updating local storage information
cy.saveLocalStorage(); cy.saveLocalStorage();

5
scripts/cypress/integration/common/1a_table_operations.js

@ -44,13 +44,16 @@ export const genTest = (apiType, dbType) => {
}; };
it("Open Audit tab", () => { it("Open Audit tab", () => {
// http://localhost:8080/api/v1/db/meta/projects/p_bxp57hmks0n5o2/audits?offset=0&limit=25
cy.intercept("/**/audits?offset=*&limit=*").as("waitForPageLoad");
// mainPage.navigationDraw(mainPage.AUDIT).click(); // mainPage.navigationDraw(mainPage.AUDIT).click();
settingsPage.openMenu(settingsPage.AUDIT); settingsPage.openMenu(settingsPage.AUDIT);
// wait for column headers to appear // wait for column headers to appear
// //
cy.get("thead > tr > th.ant-table-cell").should("have.length", 5); cy.get("thead > tr > th.ant-table-cell").should("have.length", 5);
cy.wait(3000); cy.wait("@waitForPageLoad");
// Audit table entries // Audit table entries
// [Header] Operation Type, Operation Sub Type, Description, User, Created // [Header] Operation Type, Operation Sub Type, Description, User, Created

1
scripts/cypress/integration/common/1b_table_column_operations.js

@ -73,6 +73,7 @@ export const genTest = (apiType, dbType) => {
.click(); .click();
// fix me! wait till the modal rendering (input highlight) is completed // fix me! wait till the modal rendering (input highlight) is completed
// focus shifts back to the input field to select text after the dropdown is rendered
cy.wait(500); cy.wait(500);
// change column type and verify // change column type and verify

2
scripts/cypress/integration/common/2b_table_with_m2m_column.js

@ -107,8 +107,6 @@ export const genTest = (apiType, dbType) => {
.contains("ACADEMY DINOSAUR", { timeout: 2000 }) .contains("ACADEMY DINOSAUR", { timeout: 2000 })
.click() .click()
.then(() => { .then(() => {
// wait to ensure pop up appears before we proceed further
cy.wait(1000);
// Link card validation // Link card validation
cy.getActiveDrawer(".nc-drawer-expanded-form") cy.getActiveDrawer(".nc-drawer-expanded-form")
.find(".text-lg") .find(".text-lg")

10
scripts/cypress/integration/common/3a_filter_sort_fields_operations.js

@ -46,9 +46,13 @@ export const genTest = (apiType, dbType) => {
// create new row using + button in header // create new row using + button in header
// //
it("Add row using tool header button", () => { it("Add row using tool header button", () => {
// http://localhost:8080/api/v1/db/meta/audits/comments/count?ids[]=101&ids[]=102&ids[]=103&ids[]=104&ids[]=105&ids[]=106&ids[]=107&ids[]=108&ids[]=109&fk_model_id=md_zfkb9v3mzky958
cy.intercept("/api/v1/db/meta/audits/comments/count*").as(
"waitForPageLoad"
);
// add a row to end of Country table // add a row to end of Country table
cy.get(".nc-add-new-row-btn").click(); cy.get(".nc-add-new-row-btn").click();
cy.wait(1000);
cy.get(".nc-expand-col-Country") cy.get(".nc-expand-col-Country")
.find(".nc-cell > input") .find(".nc-cell > input")
.first() .first()
@ -73,8 +77,8 @@ export const genTest = (apiType, dbType) => {
// verify // verify
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load cy.wait("@waitForPageLoad");
cy.wait(3000);
mainPage.getCell("Country", 10).contains("Test Country").should("exist"); mainPage.getCell("Country", 10).contains("Test Country").should("exist");
}); });

6
scripts/cypress/integration/common/3c_lookup_column.js

@ -19,11 +19,8 @@ export const genTest = (apiType, dbType) => {
// // open a table to work on views // // open a table to work on views
// // // //
// //
// // // kludge: wait for page load to finish
// // cy.wait(1000);
// // // close team & auth tab // // // close team & auth tab
// // cy.get('button.ant-tabs-tab-remove').should('exist').click(); // // cy.get('button.ant-tabs-tab-remove').should('exist').click();
// // cy.wait(1000);
// //
// cy.openTableTab("City", 25); // cy.openTableTab("City", 25);
// }); // });
@ -63,7 +60,8 @@ export const genTest = (apiType, dbType) => {
.contains("Lookup") .contains("Lookup")
.click(); .click();
// wait for re-rendering & title selection to re-appear // fix me! wait till the modal rendering (input highlight) is completed
// focus shifts back to the input field to select text after the dropdown is rendered
cy.wait(500); cy.wait(500);
// Configure Child table & column names // Configure Child table & column names

8
scripts/cypress/integration/common/3d_rollup_column.js

@ -9,7 +9,6 @@ export const genTest = (apiType, dbType) => {
// //
const fetchParentFromLabel = (label) => { const fetchParentFromLabel = (label) => {
cy.get("label").contains(label).parents(".ant-row").click(); cy.get("label").contains(label).parents(".ant-row").click();
cy.wait(500);
}; };
// Run once before test- create project (rest/graphql) // Run once before test- create project (rest/graphql)
@ -18,12 +17,8 @@ export const genTest = (apiType, dbType) => {
// cy.fileHook(); // cy.fileHook();
// mainPage.tabReset(); // mainPage.tabReset();
// //
// // // kludge: wait for page load to finish
// // cy.wait(1000);
// // // close team & auth tab // // // close team & auth tab
// // cy.get('button.ant-tabs-tab-remove').should('exist').click(); // // cy.get('button.ant-tabs-tab-remove').should('exist').click();
// // cy.wait(1000);
//
// // open a table to work on views // // open a table to work on views
// // // //
// cy.openTableTab("Country", 25); // cy.openTableTab("Country", 25);
@ -69,7 +64,8 @@ export const genTest = (apiType, dbType) => {
.contains("Rollup") .contains("Rollup")
.click(); .click();
// wait for re-rendering & title selection to re-appear // fix me! wait till the modal rendering (input highlight) is completed
// focus shifts back to the input field to select text after the dropdown is rendered
cy.wait(500); cy.wait(500);
// Configure Child table & column names // Configure Child table & column names

4
scripts/cypress/integration/common/3e_duration_column.js

@ -18,11 +18,8 @@ export const genTest = (apiType, dbType) => {
before(() => { before(() => {
mainPage.tabReset(); mainPage.tabReset();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab // // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click(); // cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
cy.createTable(tableName); cy.createTable(tableName);
}); });
@ -126,7 +123,6 @@ export const genTest = (apiType, dbType) => {
) => { ) => {
if (isNewRow) { if (isNewRow) {
cy.get(".nc-add-new-row-btn:visible").should("exist"); cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.wait(500);
cy.get(".nc-add-new-row-btn").click(); cy.get(".nc-add-new-row-btn").click();
} else { } else {
// mainPage.getRow(index).find(".nc-row-expand-icon").click({ force: true }); // mainPage.getRow(index).find(".nc-row-expand-icon").click({ force: true });

7
scripts/cypress/integration/common/3f_link_to_another_record.js

@ -94,6 +94,11 @@ export const genTest = (apiType, dbType) => {
// Unlink LTAR cell // Unlink LTAR cell
// //
function ltarUnlink(columnName, index) { function ltarUnlink(columnName, index) {
// http://localhost:8080/api/v1/db/meta/audits/comments/count?ids[]=1&fk_model_id=md_f4y7jp89pe8vkt
cy.intercept("GET", `/api/v1/db/meta/audits/comments/count?**`).as(
"unlinkCount"
);
// Click on cell to enable unlink icon // Click on cell to enable unlink icon
cy.get(`:nth-child(${index}) > [data-title="${columnName}"]`) cy.get(`:nth-child(${index}) > [data-title="${columnName}"]`)
.last() .last()
@ -107,7 +112,7 @@ export const genTest = (apiType, dbType) => {
.click(); .click();
// Glitch; hence wait // Glitch; hence wait
cy.wait(1000); cy.wait("@unlinkCount");
} }
// before(() => { // before(() => {

1
scripts/cypress/integration/common/4a_table_view_grid_gallery_form.js

@ -77,7 +77,6 @@ export const genTest = (apiType, dbType) => {
// click on delete icon (becomes visible on hovering mouse) // click on delete icon (becomes visible on hovering mouse)
cy.get(".nc-view-delete-icon").click({ force: true }); cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(300);
cy.getActiveModal(".nc-modal-view-delete") cy.getActiveModal(".nc-modal-view-delete")
.find(".ant-btn-dangerous") .find(".ant-btn-dangerous")
.click(); .click();

3
scripts/cypress/integration/common/4c_form_view_detailed.js

@ -324,7 +324,6 @@ export const genTest = (apiType, dbType) => {
// open form view & enable "email me" option // open form view & enable "email me" option
cy.openTableTab("Country", 25); cy.openTableTab("Country", 25);
cy.wait(1000);
cy.get(`.nc-view-item.nc-${viewType}-view-item`) cy.get(`.nc-view-item.nc-${viewType}-view-item`)
.contains("Form-1") .contains("Form-1")
@ -338,7 +337,6 @@ export const genTest = (apiType, dbType) => {
settingsPage.openMenu(settingsPage.APPSTORE); settingsPage.openMenu(settingsPage.APPSTORE);
mainPage.resetSMTP(); mainPage.resetSMTP();
cy.wait(300);
cy.openTableTab("Country", 25); cy.openTableTab("Country", 25);
}); });
@ -383,7 +381,6 @@ export const genTest = (apiType, dbType) => {
// click on delete icon (becomes visible on hovering mouse) // click on delete icon (becomes visible on hovering mouse)
cy.get(".nc-view-delete-icon").click({ force: true }); cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(1000);
cy.getActiveModal(".nc-modal-view-delete") cy.getActiveModal(".nc-modal-view-delete")
.find(".ant-btn-dangerous") .find(".ant-btn-dangerous")
.click(); .click();

3
scripts/cypress/integration/common/4e_form_view_share.js

@ -112,7 +112,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(linkText, { cy.visit(linkText, {
baseUrl: null, baseUrl: null,
}); });
// cy.wait(5000);
cy.wait(["@waitForPageLoad"], { times: 2 }); cy.wait(["@waitForPageLoad"], { times: 2 });
// wait for share view page to load! // wait for share view page to load!
@ -208,8 +207,6 @@ export const genTest = (apiType, dbType) => {
// // clean up newly added rows into Country table operations // // clean up newly added rows into Country table operations
// // this auto verifies successfull addition of rows to table as well // // this auto verifies successfull addition of rows to table as well
// mainPage.getPagination(25).click(); // mainPage.getPagination(25).click();
// // kludge: flicker on load
// cy.wait(3000)
// //
// cy.get(".nc-grid-row").should("have.length", 1); // cy.get(".nc-grid-row").should("have.length", 1);
// cy.get(".ant-checkbox").should('exist').eq(1).click({ force: true }); // cy.get(".ant-checkbox").should('exist').eq(1).click({ force: true });

8
scripts/cypress/integration/common/4f_pg_grid_view_share.js

@ -22,8 +22,6 @@ export const genTest = (apiType, dbType) => {
// .click(); // .click();
mainPage.shareView().click({ force: true }); mainPage.shareView().click({ force: true });
cy.wait(5000);
// wait, as URL initially will be /undefined // wait, as URL initially will be /undefined
cy.getActiveModal(".nc-modal-share-view") cy.getActiveModal(".nc-modal-share-view")
.find(".share-link-box") .find(".share-link-box")
@ -136,7 +134,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(viewURL["combined"], { cy.visit(viewURL["combined"], {
baseUrl: null, baseUrl: null,
}); });
cy.wait(5000);
// wait for page rendering to complete // wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 18); cy.get(".nc-grid-row").should("have.length", 18);
@ -353,7 +350,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(storedURL, { cy.visit(storedURL, {
baseUrl: null, baseUrl: null,
}); });
cy.wait(5000);
// number of view entries should be 2 before we delete // number of view entries should be 2 before we delete
cy.get(".nc-view-item").its("length").should("eq", 2); cy.get(".nc-view-item").its("length").should("eq", 2);
@ -390,12 +386,9 @@ export const genTest = (apiType, dbType) => {
cy.visit(storedURL, { cy.visit(storedURL, {
baseUrl: null, baseUrl: null,
}); });
cy.wait(5000);
// delete row // delete row
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000);
// wait for page rendering to complete // wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10); cy.get(".nc-grid-row").should("have.length", 10);
@ -439,7 +432,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(viewURL["rowColUpdate"], { cy.visit(viewURL["rowColUpdate"], {
baseUrl: null, baseUrl: null,
}); });
cy.wait(5000);
//5 //5
// wait for public view page to load! // wait for public view page to load!

14
scripts/cypress/integration/common/4g_table_view_expanded_form.js

@ -23,13 +23,18 @@ function verifyExpandFormHeader(title) {
export const genTest = (apiType, dbType) => { export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return; if (!isTestSuiteActive(apiType, dbType)) return;
let clear;
describe(`${apiType.toUpperCase()} api - Table views: Expanded form`, () => { describe(`${apiType.toUpperCase()} api - Table views: Expanded form`, () => {
before(() => { before(() => {
cy.restoreLocalStorage(); cy.restoreLocalStorage();
// open a table to work on views // open a table to work on views
// //
cy.openTableTab("Country", 25); cy.openTableTab('Country', 25);
clear = Cypress.LocalStorage.clear;
Cypress.LocalStorage.clear = () => {}
}); });
beforeEach(() => { beforeEach(() => {
@ -44,6 +49,7 @@ export const genTest = (apiType, dbType) => {
cy.restoreLocalStorage(); cy.restoreLocalStorage();
cy.closeTableTab("Country"); cy.closeTableTab("Country");
cy.saveLocalStorage(); cy.saveLocalStorage();
Cypress.LocalStorage.clear = clear;
}); });
// Common routine to create/edit/delete GRID & GALLERY view // Common routine to create/edit/delete GRID & GALLERY view
@ -66,7 +72,8 @@ export const genTest = (apiType, dbType) => {
.should("exist"); .should("exist");
if (viewType === "gallery") { if (viewType === "gallery") {
// cy.intercept('/api/v1/db/meta/galleries/*').as('getGalleryView'); // http://localhost:8080/api/v1/db/data/noco/p_4ufoizgrorwyey/md_g0zc9d40w8zpmy/views/vw_xauikhkm8r49fy?offset=0&limit=25
cy.intercept("/api/v1/db/data/noco/**").as("getGalleryViewData");
// mainPage.unhideField("City List"); // mainPage.unhideField("City List");
cy.get(".nc-fields-menu-btn").click(); cy.get(".nc-fields-menu-btn").click();
@ -75,9 +82,8 @@ export const genTest = (apiType, dbType) => {
.click(); .click();
cy.get(".nc-fields-menu-btn").click(); cy.get(".nc-fields-menu-btn").click();
cy.wait(["@getGalleryViewData"]);
cy.get('.ant-card-body [title="City List"]').should("exist"); cy.get('.ant-card-body [title="City List"]').should("exist");
cy.wait(1000);
// cy.wait(['@getGalleryView'])
} }
}); });

51
scripts/cypress/integration/common/5a_user_role.js

@ -102,7 +102,7 @@ export const genTest = (apiType, dbType) => {
disableTableAccess("CustomerList", "commenter"); disableTableAccess("CustomerList", "commenter");
disableTableAccess("CustomerList", "viewer"); disableTableAccess("CustomerList", "viewer");
cy.get("button.nc-acl-save").click({ force: true }); cy.get("button.nc-acl-save").click();
cy.toastWait("Updated UI ACL for tables successfully"); cy.toastWait("Updated UI ACL for tables successfully");
mainPage.closeMetaTab(); mainPage.closeMetaTab();
@ -110,13 +110,14 @@ export const genTest = (apiType, dbType) => {
}); });
const roleValidation = (roleType) => { const roleValidation = (roleType) => {
let clear;
describe(`User role validation`, () => { describe(`User role validation`, () => {
before(() => { before(() => {
cy.restoreLocalStorage(); cy.restoreLocalStorage();
cy.visit(mainPage.roleURL[roleType]); cy.visit(mainPage.roleURL[roleType]);
cy.wait(5000);
cy.get('button:contains("SIGN UP")').should("exist"); cy.get('button:contains("SIGN UP"):visible').should("exist");
cy.get('input[type="text"]', { timeout: 20000 }).type( cy.get('input[type="text"]', { timeout: 20000 }).type(
roles[roleType].credentials.username roles[roleType].credentials.username
); );
@ -124,12 +125,9 @@ export const genTest = (apiType, dbType) => {
roles[roleType].credentials.password roles[roleType].credentials.password
); );
cy.get('button:contains("SIGN UP")').click(); cy.get('button:contains("SIGN UP")').click();
cy.get(`.nc-project-page-title:contains("My Projects"):visible`).should(
cy.wait(3000); "exist"
);
cy.get(".nc-project-page-title")
.contains("My Projects")
.should("be.visible");
if (dbType === "xcdb") { if (dbType === "xcdb") {
if ("rest" == apiType) if ("rest" == apiType)
@ -149,12 +147,15 @@ export const genTest = (apiType, dbType) => {
if (roleType === "creator") { if (roleType === "creator") {
// kludge: wait for page load to finish // kludge: wait for page load to finish
// close team & auth tab // close team & auth tab
cy.wait(2000); cy.wait(500);
cy.get("button.ant-tabs-tab-remove").should("exist").click(); cy.get("button.ant-tabs-tab-remove").should("exist").click();
cy.wait(1000); cy.wait(500);
} }
cy.saveLocalStorage(); cy.saveLocalStorage();
clear = Cypress.LocalStorage.clear;
Cypress.LocalStorage.clear = () => {};
}); });
beforeEach(() => { beforeEach(() => {
@ -169,6 +170,8 @@ export const genTest = (apiType, dbType) => {
cy.restoreLocalStorage(); cy.restoreLocalStorage();
cy.signOut(); cy.signOut();
cy.saveLocalStorage(); cy.saveLocalStorage();
Cypress.LocalStorage.clear = clear;
}); });
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
@ -177,13 +180,17 @@ export const genTest = (apiType, dbType) => {
it(`[${roles[roleType].name}] Left navigation menu, New User add`, () => { it(`[${roles[roleType].name}] Left navigation menu, New User add`, () => {
// project configuration settings // project configuration settings
// //
if (roleType !== "owner") {
_advSettings(roleType, "userRole"); _advSettings(roleType, "userRole");
}
}); });
it(`[${roles[roleType].name}] Access control`, () => { it(`[${roles[roleType].name}] Access control`, () => {
// Access control validation // Access control validation
// //
if (roleType !== "owner") {
_accessControl(roleType, "userRole"); _accessControl(roleType, "userRole");
}
}); });
it(`[${roles[roleType].name}] Schema: create table, add/modify/delete column`, () => { it(`[${roles[roleType].name}] Schema: create table, add/modify/delete column`, () => {
@ -191,14 +198,18 @@ export const genTest = (apiType, dbType) => {
// - Add/delete table // - Add/delete table
// - Add/Update/delete column // - Add/Update/delete column
// //
if (roleType !== "owner") {
_editSchema(roleType, "userRole"); _editSchema(roleType, "userRole");
}
}); });
it(`[${roles[roleType].name}] Data: add/modify/delete row, update cell contents`, () => { it(`[${roles[roleType].name}] Data: add/modify/delete row, update cell contents`, () => {
// Table data related validations // Table data related validations
// - Add/delete/modify row // - Add/delete/modify row
// //
if (roleType !== "owner") {
_editData(roleType, "userRole"); _editData(roleType, "userRole");
}
}); });
it(`[${roles[roleType].name}] Comments: view/add`, () => { it(`[${roles[roleType].name}] Comments: view/add`, () => {
@ -206,19 +217,27 @@ export const genTest = (apiType, dbType) => {
// Viewer: only allowed to read // Viewer: only allowed to read
// Everyone else: read &/ update // Everyone else: read &/ update
// //
if (roleType !== "owner") {
_editComment(roleType, "userRole"); _editComment(roleType, "userRole");
}
}); });
it(`[${roles[roleType].name}] Right navigation menu, share view`, () => { it(`[${roles[roleType].name}] Right navigation menu, share view`, () => {
// right navigation menu bar // right navigation menu bar
// Editor/Viewer/Commenter : can only view 'existing' views // Editor/Viewer/Commenter : can only view 'existing' views
// Rest: can create/edit // Rest: can create/edit
if (roleType !== "owner") {
_viewMenu(roleType, "userRole"); _viewMenu(roleType, "userRole");
}
}); });
it(`[${roles[roleType].name}] Download files`, () => { it(`[${roles[roleType].name}] Download files`, () => {
// to be fixed // to be fixed
if (roleType === "commenter" || roleType === "viewer") { if (
roleType === "commenter" ||
roleType === "viewer" ||
roleType === "owner"
) {
} else { } else {
// viewer & commenter doesn't contain hideField option in ncv2 // viewer & commenter doesn't contain hideField option in ncv2
// #ID, City, LastUpdate, City => Address, Country <= City, + // #ID, City, LastUpdate, City => Address, Country <= City, +
@ -252,11 +271,17 @@ export const genTest = (apiType, dbType) => {
mainPage.unhideField("LastUpdate"); mainPage.unhideField("LastUpdate");
} }
}); });
it(`[${roles[roleType].name}] App store accessibility`, () => {
cy.visit("/#/apps").then((r) => {
cy.toastWait("You don't have enough permission to access the page.");
});
});
}); });
}; };
// skip owner validation as rest of the cases pretty much cover the same // skip owner validation as rest of the cases pretty much cover the same
// roleValidation('owner') // roleValidation("owner");
roleValidation("creator"); roleValidation("creator");
roleValidation("editor"); roleValidation("editor");
roleValidation("commenter"); roleValidation("commenter");

2
scripts/cypress/integration/common/5b_preview_role.js

@ -59,8 +59,6 @@ export const genTest = (apiType, dbType, roleType) => {
after(() => { after(() => {
// cy.get(".nc-preview-reset").click({ force: true }); // cy.get(".nc-preview-reset").click({ force: true });
cy.get(".mdi-exit-to-app").click(); cy.get(".mdi-exit-to-app").click();
// cy.wait(20000)
// wait for page rendering to complete // wait for page rendering to complete
cy.get(".nc-grid-row", { timeout: 25000 }).should("have.length", 25); cy.get(".nc-grid-row", { timeout: 25000 }).should("have.length", 25);

85
scripts/cypress/integration/common/5c_super_user_role.js

@ -0,0 +1,85 @@
import { loginPage } from '../../support/page_objects/navigation';
import { roles } from '../../support/page_objects/projectConstants';
export const genTest = (apiType, dbType) => {
describe(`${apiType.toUpperCase()} api - Super user test`, () => {
before(() => {
loginPage.signIn(roles.owner.credentials);
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
after(() => {
});
it(`Open App store page and check slack app`, () => {
cy.visit('/#/apps').then(win => {
cy.get('.nc-app-store-title').should('exist');
cy.get('.nc-app-store-card-Slack').should('exist');
// install slack app
cy.get('.nc-app-store-card-Slack .install-btn')
.invoke('attr', 'style', 'right: 10px')
cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-install')
.click();
cy.getActiveModal('.nc-modal-plugin-install')
.find('[placeholder="Channel Name"]')
.type('Test channel')
cy.getActiveModal('.nc-modal-plugin-install')
.find('[placeholder="Webhook URL"]')
.type('http://test.com')
cy.getActiveModal('.nc-modal-plugin-install')
.find('button:contains("Save")')
.click()
cy.toastWait('Successfully installed')
cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-install').should('not.exist');
// update slack app config
cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-edit').should('exist').click()
cy.getActiveModal('.nc-modal-plugin-install')
.should('exist')
.find('[placeholder="Channel Name"]')
.should('have.value', 'Test channel')
.clear()
.type('Test channel 2')
cy.getActiveModal('.nc-modal-plugin-install')
.get('button:contains("Save")')
.click()
cy.toastWait('Successfully installed')
// reset slack app
cy.get('.nc-app-store-card-Slack .install-btn .nc-app-store-card-reset').should('exist').click()
cy.getActiveModal('.nc-modal-plugin-uninstall')
.should('exist')
.find('button:contains("Confirm")')
.click()
cy.toastWait('Plugin uninstalled successfully')
});
});
});
}

14
scripts/cypress/integration/common/6f_attachments.js

@ -27,15 +27,9 @@ export const genTest = (apiType, dbType) => {
// clean up newly added rows into Country table operations // clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well // this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click(); mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000);
// wait for page rendering to complete // wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10); cy.get(".nc-grid-row").should("have.length", 10);
// mainPage
// .getRow(10)
// .find(".mdi-checkbox-blank-outline")
// .click({ force: true });
mainPage.getCell("Country", 10).rightclick(); mainPage.getCell("Country", 10).rightclick();
cy.getActiveMenu(".nc-dropdown-grid-context-menu") cy.getActiveMenu(".nc-dropdown-grid-context-menu")
@ -77,8 +71,6 @@ export const genTest = (apiType, dbType) => {
mainPage.shareView().click(); mainPage.shareView().click();
// cy.wait(5000);
// copy link text, visit URL // copy link text, visit URL
cy.getActiveModal(".nc-modal-share-view") cy.getActiveModal(".nc-modal-share-view")
.find(".share-link-box") .find(".share-link-box")
@ -97,7 +89,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(linkText, { cy.visit(linkText, {
baseUrl: null, baseUrl: null,
}); });
// cy.wait(5000);
cy.wait(["@waitForSharedViewLoad"]); cy.wait(["@waitForSharedViewLoad"]);
// wait for share view page to load! // wait for share view page to load!
@ -129,12 +120,7 @@ export const genTest = (apiType, dbType) => {
it(`Filter column which contain only attachments, download CSV`, () => { it(`Filter column which contain only attachments, download CSV`, () => {
// come back to main window // come back to main window
loginPage.loginAndOpenProject(apiType, dbType); loginPage.loginAndOpenProject(apiType, dbType);
// cy.visit('/')
// cy.wait(5000)
// projectsPage.openConfiguredProject(apiType, dbType);
cy.openTableTab("Country", 25); cy.openTableTab("Country", 25);
// cy.wait(1000);
mainPage.filterField("testAttach", "is not null", null); mainPage.filterField("testAttach", "is not null", null);
mainPage.hideField("LastUpdate"); mainPage.hideField("LastUpdate");

4
scripts/cypress/integration/common/6g_base_share.js

@ -21,12 +21,14 @@ export const genTest = (apiType, dbType) => {
const permissionValidation = (roleType) => { const permissionValidation = (roleType) => {
it(`${roleType}: Visit base shared URL`, () => { it(`${roleType}: Visit base shared URL`, () => {
cy.log(linkText); cy.log(linkText);
// http://localhost:8080/api/v1/db/meta/projects/p_4ufoizgrorwyey/tables?includeM2M=false
cy.intercept("/api/v1/db/meta/projects/**").as("waitForPageLoad");
// visit URL & wait for page load to complete // visit URL & wait for page load to complete
cy.visit(linkText, { cy.visit(linkText, {
baseUrl: null, baseUrl: null,
}); });
cy.wait(5000); cy.wait(["@waitForPageLoad"]);
}); });
it(`${roleType}: Validate access permissions: advance menu`, () => { it(`${roleType}: Validate access permissions: advance menu`, () => {

10
scripts/cypress/integration/common/7a_create_project_from_excel.js

@ -87,12 +87,11 @@ export const genTest = (apiType, dbType) => {
sheetData = rows; sheetData = rows;
}); });
cy.visit("/"); // cy.visit("/");
projectsPage.createProject( projectsPage.createProject(
{ dbType: "none", apiType: "REST", name: "importSample" }, { dbType: "none", apiType: "REST", name: "importSample" },
{} {}
); );
cy.wait(4000);
cy.saveLocalStorage(); cy.saveLocalStorage();
}); });
@ -122,6 +121,11 @@ export const genTest = (apiType, dbType) => {
}); });
it("File Upload: Verify pre-load template page", () => { it("File Upload: Verify pre-load template page", () => {
// http://localhost:8080/api/v1/db/meta/audits/comments/count?ids[]=1&ids[]=2&fk_model_id=md_fq1vxy2181bzp0
cy.intercept("/api/v1/db/meta/audits/comments/count*").as(
"waitForPageLoad"
);
cy.getActiveModal() cy.getActiveModal()
.find(".ant-collapse-item") .find(".ant-collapse-item")
.then((sheets) => { .then((sheets) => {
@ -171,7 +175,7 @@ export const genTest = (apiType, dbType) => {
cy.getActiveModal().find(".ant-btn-primary").click(); cy.getActiveModal().find(".ant-btn-primary").click();
// wait for page to get loaded (issue observed in CI-CD) // wait for page to get loaded (issue observed in CI-CD)
cy.wait(5000); cy.wait("@waitForPageLoad");
}); });
it("File Upload: Verify loaded data", () => { it("File Upload: Verify loaded data", () => {

7
scripts/cypress/integration/common/8a_webhook.js

@ -116,7 +116,6 @@ function configureWebhook(hook, test) {
.contains(new RegExp("^" + hook.condition.column + "$", "g")) .contains(new RegExp("^" + hook.condition.column + "$", "g"))
.should("exist") .should("exist")
.click(); .click();
cy.wait(1000);
cy.get(".nc-filter-operation-select").should("exist").last().click(); cy.get(".nc-filter-operation-select").should("exist").last().click();
cy.get(".ant-select-dropdown:visible") cy.get(".ant-select-dropdown:visible")
@ -162,8 +161,9 @@ function clearServerData() {
function addNewRow(index, cellValue) { function addNewRow(index, cellValue) {
cy.get(".nc-add-new-row-btn:visible").should("exist"); cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.get(".nc-add-new-row-btn").click(); cy.get(".nc-add-new-row-btn").click();
cy.wait(1000);
cy.get(".nc-expand-col-Title") cy.get(".nc-expand-col-Title")
.should("exist")
.find(".nc-cell > input") .find(".nc-cell > input")
.first() .first()
.type(cellValue); .type(cellValue);
@ -185,6 +185,7 @@ function updateRow(index, cellValue) {
.click({ force: true }); .click({ force: true });
cy.get(".nc-expand-col-Title") cy.get(".nc-expand-col-Title")
.should("exist")
.find(".nc-cell > input") .find(".nc-cell > input")
.should("exist") .should("exist")
.first() .first()
@ -207,7 +208,7 @@ function updateRow(index, cellValue) {
function verifyHookTrigger(count, lastValue) { function verifyHookTrigger(count, lastValue) {
// allow message to be received // allow message to be received
cy.wait(500); cy.wait(100);
cy.request("http://localhost:9090/hook/count").then((msg) => { cy.request("http://localhost:9090/hook/count").then((msg) => {
cy.log(msg.body); cy.log(msg.body);

7
scripts/cypress/integration/common/9a_QuickTest.js

@ -89,6 +89,8 @@ function verifyWebhook(config) {
} }
export const genTest = (apiType, dbType, testMode) => { export const genTest = (apiType, dbType, testMode) => {
let clear;
if (!isTestSuiteActive(apiType, dbType)) return; if (!isTestSuiteActive(apiType, dbType)) return;
describe(`Quick Tests`, () => { describe(`Quick Tests`, () => {
let cellIdx = 1; let cellIdx = 1;
@ -115,6 +117,9 @@ export const genTest = (apiType, dbType, testMode) => {
cy.openTableTab("Film", 3); cy.openTableTab("Film", 3);
cy.saveLocalStorage(); cy.saveLocalStorage();
clear = Cypress.LocalStorage.clear;
Cypress.LocalStorage.clear = () => {};
}); });
beforeEach(() => { beforeEach(() => {
@ -129,6 +134,8 @@ export const genTest = (apiType, dbType, testMode) => {
cy.restoreLocalStorage(); cy.restoreLocalStorage();
cy.signOut(); cy.signOut();
cy.saveLocalStorage(); cy.saveLocalStorage();
Cypress.LocalStorage.clear = clear;
}); });
it("Verify Schema", () => { it("Verify Schema", () => {

5
scripts/cypress/integration/spec/roleValidation.spec.js

@ -40,6 +40,7 @@ export function _advSettings(roleType, mode) {
.click(); .click();
cy.get(`[data-menu-id="teamAndAuth"]`).should("exist"); cy.get(`[data-menu-id="teamAndAuth"]`).should("exist");
if (roleType === "owner")
cy.get(`[data-menu-id="appStore"]`).should("exist"); cy.get(`[data-menu-id="appStore"]`).should("exist");
cy.get(`[data-menu-id="projMetaData"]`).should("exist"); cy.get(`[data-menu-id="projMetaData"]`).should("exist");
cy.get(`[data-menu-id="audit"]`).should("exist"); cy.get(`[data-menu-id="audit"]`).should("exist");
@ -203,8 +204,6 @@ export function _editComment(roleType, mode) {
cy.openTableTab(columnName, 25); cy.openTableTab(columnName, 25);
cy.wait(1000);
// click on comment icon & type comment // click on comment icon & type comment
// //
cy.get(".nc-row-expand").should("exist").eq(10).click({ force: true }); cy.get(".nc-row-expand").should("exist").eq(10).click({ force: true });
@ -214,8 +213,6 @@ export function _editComment(roleType, mode) {
// Everyone else: Comment added/read successfully // Everyone else: Comment added/read successfully
// //
cy.wait(3000);
if ("viewer" === roleType) { if ("viewer" === roleType) {
cy.getActiveDrawer(".nc-drawer-expanded-form") cy.getActiveDrawer(".nc-drawer-expanded-form")
.should("exist") .should("exist")

1
scripts/cypress/integration/test/explicitLogin.js

@ -27,7 +27,6 @@ export const genTest = (apiType, dbType) => {
.then(() => { .then(() => {
let query = `ALTER TABLE "actor" RENAME TO "${projId}actor"`; let query = `ALTER TABLE "actor" RENAME TO "${projId}actor"`;
cy.task("sqliteExec", query); cy.task("sqliteExec", query);
cy.wait(1000);
}); });
}); });
}); });

2
scripts/cypress/integration/test/pg-restRoles.js

@ -2,6 +2,7 @@ let t0 = require("./explicitLogin");
let t01 = require("../common/00_pre_configurations"); let t01 = require("../common/00_pre_configurations");
let t5a = require("../common/5a_user_role"); let t5a = require("../common/5a_user_role");
let t5b = require("../common/5b_preview_role"); let t5b = require("../common/5b_preview_role");
let t5c = require("../common/5c_super_user_role");
const { const {
setCurrentMode, setCurrentMode,
} = require("../../support/page_objects/projectConstants"); } = require("../../support/page_objects/projectConstants");
@ -12,6 +13,7 @@ const nocoTestSuite = (apiType, dbType) => {
t5a.genTest(apiType, dbType); t5a.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType); // t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
}; };
nocoTestSuite("rest", "postgres"); nocoTestSuite("rest", "postgres");

3
scripts/cypress/integration/test/restRoles.js

@ -2,6 +2,7 @@ let t0 = require("./explicitLogin");
let t01 = require("../common/00_pre_configurations"); let t01 = require("../common/00_pre_configurations");
let t5a = require("../common/5a_user_role"); let t5a = require("../common/5a_user_role");
let t5b = require("../common/5b_preview_role"); let t5b = require("../common/5b_preview_role");
let t5c = require("../common/5c_super_user_role");
const { const {
setCurrentMode, setCurrentMode,
} = require("../../support/page_objects/projectConstants"); } = require("../../support/page_objects/projectConstants");
@ -12,6 +13,8 @@ const nocoTestSuite = (apiType, dbType) => {
t5a.genTest(apiType, dbType); t5a.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType); // t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
}; };
nocoTestSuite("rest", "mysql"); nocoTestSuite("rest", "mysql");

2
scripts/cypress/integration/test/xcdb-restRoles.js

@ -2,6 +2,7 @@ let t0 = require("./explicitLogin");
let t01 = require("../common/00_pre_configurations"); let t01 = require("../common/00_pre_configurations");
let t5a = require("../common/5a_user_role"); let t5a = require("../common/5a_user_role");
let t5b = require("../common/5b_preview_role"); let t5b = require("../common/5b_preview_role");
let t5c = require("../common/5c_super_user_role");
const { const {
setCurrentMode, setCurrentMode,
} = require("../../support/page_objects/projectConstants"); } = require("../../support/page_objects/projectConstants");
@ -12,6 +13,7 @@ const nocoTestSuite = (apiType, dbType) => {
t5a.genTest(apiType, dbType); t5a.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType); // t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
}; };
nocoTestSuite("rest", "xcdb"); nocoTestSuite("rest", "xcdb");

24
scripts/cypress/support/commands.js

@ -47,16 +47,12 @@ Cypress.Commands.add("signinOrSignup", (_args) => {
_args _args
); );
cy.wait(1000);
// signin/signup // signin/signup
cy.get("body").then(($body) => { cy.get("body").then(($body) => {
// cy.wait(1000)
cy.url().then((url) => { cy.url().then((url) => {
if (!url.includes("/projects")) { if (!url.includes("/projects")) {
// handle initial load // handle initial load
if ($body.find(".welcome-page").length > 0) { if ($body.find(".welcome-page").length > 0) {
cy.wait(8000);
cy.get("body").trigger("mousemove"); cy.get("body").trigger("mousemove");
cy.snip("LetsBegin"); cy.snip("LetsBegin");
cy.contains("Let's Begin").click(); cy.contains("Let's Begin").click();
@ -173,7 +169,7 @@ Cypress.Commands.add("closeTableTab", (tn) => {
.click(); .click();
// subsequent tab open commands will fail if tab is not closed completely // subsequent tab open commands will fail if tab is not closed completely
cy.wait(1000); cy.wait(100);
}); });
Cypress.Commands.add("openOrCreateGqlProject", (_args) => { Cypress.Commands.add("openOrCreateGqlProject", (_args) => {
@ -288,9 +284,7 @@ Cypress.Commands.add("getActivePicker", (dropdownSelector) => {
Cypress.Commands.add("createTable", (name) => { Cypress.Commands.add("createTable", (name) => {
cy.task("log", `[createTableTab] ${name}`); cy.task("log", `[createTableTab] ${name}`);
cy.wait(1000);
cy.get(".nc-add-new-table").should("exist").click(); cy.get(".nc-add-new-table").should("exist").click();
cy.wait(1000);
cy.getActiveModal(".nc-modal-table-create") cy.getActiveModal(".nc-modal-table-create")
.find(`input[type="text"]:visible`) .find(`input[type="text"]:visible`)
.click() .click()
@ -300,12 +294,9 @@ Cypress.Commands.add("createTable", (name) => {
cy.getActiveModal(".nc-modal-table-create") cy.getActiveModal(".nc-modal-table-create")
.find("button.ant-btn-primary:visible") .find("button.ant-btn-primary:visible")
.click(); .click();
cy.wait(1000);
cy.get(".xc-row-table.nc-grid").should("exist"); cy.get(".xc-row-table.nc-grid").should("exist");
// cy.get('.ant-tabs-tab-active > .ant-tabs-tab-btn').contains(name).should("exist");
cy.url().should("contain", `table/${name}`); cy.url().should("contain", `table/${name}`);
cy.get(`.nc-project-tree-tbl-${name}`).should("exist"); cy.get(`.nc-project-tree-tbl-${name}`).should("exist");
cy.wait(1000);
}); });
Cypress.Commands.add("deleteTable", (name, dbType) => { Cypress.Commands.add("deleteTable", (name, dbType) => {
@ -426,7 +417,6 @@ Cypress.Commands.add("snip", (filename) => {
) { ) {
let storeName = `${screenShotDb.length}_${filename}`; let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename); screenShotDb.push(filename);
cy.wait(1000);
cy.screenshot(storeName, { overwrite: true }); cy.screenshot(storeName, { overwrite: true });
} }
}); });
@ -439,7 +429,6 @@ Cypress.Commands.add("snipActiveModal", (filename) => {
) { ) {
let storeName = `${screenShotDb.length}_${filename}`; let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename); screenShotDb.push(filename);
cy.wait(1000);
// cy.getActiveModal().screenshot(filename, { // cy.getActiveModal().screenshot(filename, {
// padding: 0, // padding: 0,
// overwrite: true, // overwrite: true,
@ -456,7 +445,6 @@ Cypress.Commands.add("snipActiveMenu", (filename) => {
) { ) {
let storeName = `${screenShotDb.length}_${filename}`; let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename); screenShotDb.push(filename);
cy.wait(1000);
// cy.getActiveMenu().screenshot(filename, { // cy.getActiveMenu().screenshot(filename, {
// padding: 0, // padding: 0,
// overwrite: true, // overwrite: true,
@ -479,13 +467,19 @@ Cypress.Commands.add("signOut", () => {
cy.get(".nc-menu-accounts", { timeout: 30000 }).should("exist").click(); cy.get(".nc-menu-accounts", { timeout: 30000 }).should("exist").click();
cy.getActiveMenu(".nc-dropdown-user-accounts-menu") cy.getActiveMenu(".nc-dropdown-user-accounts-menu")
.find(".ant-dropdown-menu-item") .find(".ant-dropdown-menu-item")
.eq(1) .last()
.click(); .click();
// cy.wait(5000);
cy.get('button:contains("SIGN IN")').should("exist"); cy.get('button:contains("SIGN IN")').should("exist");
}); });
// Navigation
//
Cypress.Commands.add("gotoProjectsPage", () => {
cy.get(".nc-noco-brand-icon").should("exist").click();
cy.get(`.nc-project-page-title:contains("My Projects")`).should("exist");
});
// Drag n Drop // Drag n Drop
// refer: https://stackoverflow.com/a/55409853 // refer: https://stackoverflow.com/a/55409853
/* /*

43
scripts/cypress/support/page_objects/mainPage.js

@ -119,18 +119,22 @@ export class _mainPage {
); );
// click on New User button, feed details // click on New User button, feed details
cy.get("button.nc-invite-team").click(); cy.getActiveModal(".nc-modal-settings")
.find("button.nc-invite-team")
.click();
// additional wait to ensure the modal is fully loaded // additional wait to ensure the modal is fully loaded
cy.getActiveModal(".nc-modal-invite-user-and-share-base").should("exist"); cy.getActiveModal(".nc-modal-invite-user-and-share-base").should("exist");
cy.getActiveModal(".nc-modal-invite-user-and-share-base") cy.getActiveModal(".nc-modal-invite-user-and-share-base")
.find('input[placeholder="E-mail"]') .find('input[placeholder="E-mail"]')
.should("exist"); .should("exist");
cy.wait(1000);
cy.get('input[placeholder="E-mail"]').type(userCred.username);
cy.get(".ant-select.nc-user-roles").click(); cy.getActiveModal(".nc-modal-invite-user-and-share-base")
.find('input[placeholder="E-mail"]')
.type(userCred.username);
cy.getActiveModal(".nc-modal-invite-user-and-share-base")
.find(".ant-select.nc-user-roles")
.click();
// opt-in requested role & submit // opt-in requested role & submit
// cy.getActiveSelection().contains(roleType).click({force: true}); // cy.getActiveSelection().contains(roleType).click({force: true});
@ -256,13 +260,18 @@ export class _mainPage {
.trigger("mouseover", { force: true }) .trigger("mouseover", { force: true })
.click({ force: true }); .click({ force: true });
cy.wait(500); // cy.get(".nc-column-delete").click();
cy.getActiveMenu(".nc-dropdown-column-operations")
.find(".nc-column-delete")
.click();
cy.get(".nc-column-delete").click(); // cy.get(".nc-column-delete").should("not.be.visible");
cy.wait(500); // cy.get(".ant-btn-dangerous:visible").contains("Delete").click();
cy.get(".nc-column-delete").should("not.be.visible");
cy.get(".ant-btn-dangerous:visible").contains("Delete").click(); cy.getActiveModal(".nc-modal-column-delete")
cy.wait(500); .find(".ant-btn-dangerous:visible")
.contains("Delete")
.click();
cy.get(`th:contains(${colName})`).should("not.exist"); cy.get(`th:contains(${colName})`).should("not.exist");
}; };
@ -549,9 +558,7 @@ export class _mainPage {
.find(".nc-project-menu-item") .find(".nc-project-menu-item")
.contains("Download") .contains("Download")
.click(); .click();
cy.wait(1000); cy.get(".nc-project-menu-item:contains('Download as CSV')")
cy.get(".nc-project-menu-item")
.contains("Download as CSV")
.should("exist") .should("exist")
.click(); .click();
} }
@ -624,9 +631,13 @@ export class _mainPage {
} }
metaSyncValidate(tbl, msg) { metaSyncValidate(tbl, msg) {
// http://localhost:8080/api/v1/db/meta/projects/p_bxp57hmks0n5o2/meta-diff
cy.intercept("GET", "/api/v1/db/meta/projects/**").as("metaSync");
cy.get(".nc-btn-metasync-reload").should("exist").click(); cy.get(".nc-btn-metasync-reload").should("exist").click();
cy.wait(2000); cy.wait("@metaSync");
cy.get(`.nc-metasync-row-${tbl}`).contains(msg).should("exist");
cy.get(`.nc-metasync-row-${tbl}:contains(${msg})`).should("exist");
cy.get(".nc-btn-metasync-sync-now") cy.get(".nc-btn-metasync-sync-now")
.should("exist") .should("exist")
.click() .click()

13
scripts/cypress/support/page_objects/navigation.js

@ -78,11 +78,8 @@ export class _loginPage {
// projectsPage.openProject(staticProjects.pgExternalREST.basic.name); // projectsPage.openProject(staticProjects.pgExternalREST.basic.name);
// } // }
// //
// // kludge: wait for page load to finish
// cy.wait(2000);
// // close team & auth tab // // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click(); // cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
} }
} }
@ -108,13 +105,11 @@ export class _projectsPage {
projectsPage.openProject(staticProjects.pgExternalREST.basic.name); projectsPage.openProject(staticProjects.pgExternalREST.basic.name);
} }
// kludge: wait for page load to finish
// cy.wait(4000);
cy.wait("@waitForPageLoad"); cy.wait("@waitForPageLoad");
// close team & auth tab // close team & auth tab
cy.get("button.ant-tabs-tab-remove").should("exist").click(); cy.get("button.ant-tabs-tab-remove").should("exist").click();
cy.wait(1000); cy.get("button.ant-tabs-tab-remove").should("not.exist");
} }
// Open existing project // Open existing project
@ -155,6 +150,8 @@ export class _projectsPage {
cy.get(".nc-metadb-project-name").should("exist"); cy.get(".nc-metadb-project-name").should("exist");
cy.contains("button", "Create").should("exist"); cy.contains("button", "Create").should("exist");
// fix me! wait till the modal rendering (input highlight) is completed
// focus shifts back to the input field to select text after the dropdown is rendered
cy.wait(1000); cy.wait(1000);
// feed project name // feed project name
@ -185,7 +182,8 @@ export class _projectsPage {
cy.get(".nc-extdb-proj-name").should("exist"); cy.get(".nc-extdb-proj-name").should("exist");
cy.get(".nc-extdb-btn-test-connection").should("exist"); cy.get(".nc-extdb-btn-test-connection").should("exist");
// CY goes too fast at times, so wait for the page to load // fix me! wait till the modal rendering (input highlight) is completed
// focus shifts back to the input field to select text after the dropdown is rendered
cy.wait(1000); cy.wait(1000);
cy.get(".nc-extdb-proj-name").clear().type(projectName); cy.get(".nc-extdb-proj-name").clear().type(projectName);
@ -208,7 +206,6 @@ export class _projectsPage {
// Create project // Create project
cy.contains("Ok & Save Project", { timeout: 20000 }).click(); cy.contains("Ok & Save Project", { timeout: 20000 }).click();
cy.wait(5000);
// takes a while to load project // takes a while to load project
this.waitHomePageLoad(); this.waitHomePageLoad();

Loading…
Cancel
Save