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. 32
      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. 95
      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. 12
      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. 55
      packages/nocodb/src/lib/models/Audit.ts
  30. 498
      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. 63
      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. 7
      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": [
"code"
]
},
{
"login": "chetanverma16",
"name": "Chetan Verma",
"avatar_url": "https://avatars.githubusercontent.com/u/16558205?v=4",
"profile": "http://chetanverma.com/",
"contributions": [
"code"
]
}
],
"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="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="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>
</tbody>
</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']
MdiStar: typeof import('~icons/mdi/star')['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']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['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%' }"
>
<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">
<MdiEditIcon class="pr-0.5" :height="12" />
Edit
@ -127,7 +127,7 @@ onMounted(async () => {
<div class="flex ml-0.5">Reset</div>
</div>
</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">
<MdiPlusIcon />
Install
@ -185,7 +185,6 @@ onMounted(async () => {
.caption {
font-size: 0.7rem;
color: #242f3e;
}
.avatar {

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

@ -77,20 +77,24 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth')
},
},
appStore: {
// App Store
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
new: {
title: 'Apps',
body: AppStore,
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
...(isUIAllowed('appStore')
? {
appStore: {
// App Store
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
new: {
title: 'Apps',
body: AppStore,
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
}
: {}),
projMetaData: {
// Project Metadata
title: t('title.projMeta'),

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

@ -23,6 +23,7 @@ const { getMeta } = useMetas()
const deleteColumn = () =>
Modal.confirm({
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'),
okType: 'danger',
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 }">
<div class="flex items-center items-center gap-1">
<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
:is="record.showPassword ? MdiVisibilityOffIcon : MdiVisibilityOnIcon"
@click="record.showPassword = !record.showPassword"

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

@ -75,14 +75,18 @@ const copyRecordUrl = () => {
<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" @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 placement="bottom">
<template #title>
<!-- todo: i18n -->
<div class="text-center w-full">Copy record URL</div>
</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 v-if="!isSqlView" placement="bottom">
<!-- Toggle comments draw -->
@ -92,7 +96,7 @@ const copyRecordUrl = () => {
<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"
class="cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1 min-w-4"
@click="commentsDrawer = !commentsDrawer"
/>
</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 ('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]
}
@ -20,9 +28,7 @@ export const useUIPermission = createSharedComposable(() => {
const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => {
if (previewAs.value && !skipPreviewAs) {
const hasPreviewPermission = hasPermission(previewAs.value, true, permission)
if (hasPreviewPermission) return true
return hasPermission(previewAs.value, true, permission)
}
return Object.entries(allRoles.value).some(([role, hasRole]) =>

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

@ -2,58 +2,81 @@ import { ProjectRole, Role } from '~/lib'
const rolePermissions = {
// 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
* limited permission due to unexpected behaviour in shared base if opened in same window */
[Role.Super]: {
projectTheme: true,
},
[Role.Admin]: {},
[Role.Guest]: {},
[Role.Super]: '*',
[Role.Admin]: {} as Record<string, boolean>,
[Role.Guest]: {} as Record<string, boolean>,
[Role.User]: {
projectCreate: true,
projectActions: true,
projectSettings: true,
include: {
projectCreate: true,
projectActions: true,
projectSettings: true,
},
},
// Project role permissions
[ProjectRole.Creator]: '*',
[ProjectRole.Owner]: '*',
[ProjectRole.Creator]: {
exclude: {
appStore: true,
},
},
[ProjectRole.Owner]: {
exclude: {
appStore: true,
},
},
[ProjectRole.Editor]: {
smartSheet: true,
xcDatatableEditable: true,
column: true,
tableAttachment: true,
tableRowUpdate: true,
dataInsert: true,
rowComments: true,
gridViewOptions: true,
sortSync: true,
fieldsSync: true,
gridColUpdate: true,
filterSync: true,
csvImport: true,
apiDocs: true,
projectSettings: true,
newUser: false,
include: {
smartSheet: true,
xcDatatableEditable: true,
column: true,
tableAttachment: true,
tableRowUpdate: true,
dataInsert: true,
rowComments: true,
gridViewOptions: true,
sortSync: true,
fieldsSync: true,
gridColUpdate: true,
filterSync: true,
csvImport: true,
apiDocs: true,
projectSettings: true,
newUser: false,
},
},
[ProjectRole.Commenter]: {
smartSheet: true,
column: true,
rowComments: true,
projectSettings: true,
include: {
smartSheet: true,
column: true,
rowComments: true,
projectSettings: true,
},
},
[ProjectRole.Viewer]: {
smartSheet: true,
column: true,
projectSettings: true,
include: {
smartSheet: true,
column: true,
projectSettings: true,
},
},
} 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]>
: never

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

@ -13,6 +13,8 @@ const hasSider = ref(false)
const sidebar = ref<HTMLDivElement>()
const { isUIAllowed } = useUIPermission()
const logout = () => {
signOut()
navigateTo('/signin')
@ -87,6 +89,19 @@ hooks.hook('page:finish', () => {
</nuxt-link>
</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-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 { defineNuxtRouteMiddleware, navigateTo } from '#app'
import { useApi, useGlobal } from '#imports'
import { useApi, useGlobal, useRoles } from '#imports'
/**
* Global auth middleware
@ -38,6 +38,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
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 (!state.signedIn && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth()
@ -68,6 +70,12 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
return navigateTo(from.path)
}
} 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 (to.params.projectId && from.params.projectId !== to.params.projectId) {
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
hideHeader?: boolean
title?: string
allowedRoles?: Role[]
}
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>

12
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">
### 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
- 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> {
const id = this._extractPksValues(data);
Audit.insert({
await Audit.insert({
fk_model_id: this.model.id,
row_id: id,
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();
Audit.insert({
await Audit.insert({
project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN,
op_sub_type: AuditOperationSubTypes.CREATED,
@ -911,7 +911,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
]);
} else {
await baseModel.bulkUpdateAll(
{ where: `(${column.title},eq,${option.title})` },
{ where: `(${column.column_name},eq,${option.title})` },
{ [column.column_name]: null },
{ cookie: req }
);
@ -966,7 +966,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
}
}
let interchange = [];
const interchange = [];
// Handle option update
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),
};
if (old_titles.includes(newOp.title)) {
let def_option = { ...newOp };
const def_option = { ...newOp };
let title_counter = 1;
while (old_titles.includes(newOp.title)) {
newOp.title = `${def_option.title}_${title_counter++}`;
@ -1077,7 +1077,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
]);
} else {
await baseModel.bulkUpdateAll(
{ where: `(${column.title},eq,${option.title})` },
{ where: `(${column.column_name},eq,${option.title})` },
{ [column.column_name]: newOp.title },
{ cookie: req }
);
@ -1138,7 +1138,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
}
for (const ch of interchange) {
let newOp = ch.def_option;
const newOp = ch.def_option;
if (column.uidt === UITypes.SingleSelect) {
if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [
@ -1150,7 +1150,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
]);
} else {
await baseModel.bulkUpdateAll(
{ where: `(${column.title},eq,${ch.temp_title})` },
{ where: `(${column.column_name},eq,${ch.temp_title})` },
{ [column.column_name]: newOp.title },
{ cookie: req }
);
@ -1327,7 +1327,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
...colBody,
});
}
Audit.insert({
await Audit.insert({
project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN,
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,
op_type: AuditOperationTypes.TABLE_COLUMN,
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,
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
@ -188,7 +188,7 @@ async function projectUserUpdate(req, res, next): Promise<any> {
req.body.roles
);
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'ROLES_MANAGEMENT',
user: req.user.email,
@ -255,7 +255,7 @@ async function projectUserInviteResend(req, res): Promise<any> {
await sendInviteEmail(user.email, invite_token, req);
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'RESEND_INVITE',
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,
});
Audit.insert({
await Audit.insert({
project_id: project.id,
op_type: AuditOperationTypes.TABLE,
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,
op_type: AuditOperationTypes.TABLE,
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;
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'SIGNUP',
user: user.email,
@ -192,7 +192,7 @@ async function successfulSignIn({
});
setTokenCookie(res, refreshToken);
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'SIGNIN',
user: user.email,
@ -291,7 +291,7 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
token_version: null,
});
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'PASSWORD_CHANGE',
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_sub_type: 'PASSWORD_FORGOT',
user: user.email,
@ -405,7 +405,7 @@ async function passwordReset(req, res): Promise<any> {
token_version: null,
});
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'PASSWORD_RESET',
user: user.email,
@ -433,7 +433,7 @@ async function emailVerification(req, res): Promise<any> {
email_verified: true,
});
Audit.insert({
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'EMAIL_VERIFICATION',
user: user.email,

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

@ -57,7 +57,11 @@ export default function (handlerFn, permissionName) {
return (
hasRole &&
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) {

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

@ -31,6 +31,7 @@ export default class Audit implements AuditType {
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(
audit: Partial<
Audit & {
@ -38,30 +39,40 @@ export default class Audit implements AuditType {
updated_at?;
}
>,
ncMeta = Noco.ncMeta
) {
if (!audit.project_id && audit.fk_model_id) {
audit.project_id = (
await Model.getByIdOrName({ id: audit.fk_model_id }, ncMeta)
).project_id;
ncMeta = Noco.ncMeta,
{ forceAwait }: { forceAwait: boolean } = {
forceAwait: process.env['TEST'] === 'true',
}
const auditRec = await ncMeta.metaInsert2(null, null, MetaTable.AUDIT, {
user: audit.user,
ip: audit.ip,
base_id: audit.base_id,
project_id: audit.project_id,
row_id: audit.row_id,
fk_model_id: audit.fk_model_id,
op_type: audit.op_type,
op_sub_type: audit.op_sub_type,
status: audit.status,
description: audit.description,
details: audit.details,
created_at: audit.created_at,
updated_at: audit.updated_at,
});
) {
const insertAudit = async () => {
if (!audit.project_id && audit.fk_model_id) {
audit.project_id = (
await Model.getByIdOrName({ id: audit.fk_model_id }, ncMeta)
).project_id;
}
return await ncMeta.metaInsert2(null, null, MetaTable.AUDIT, {
user: audit.user,
ip: audit.ip,
base_id: audit.base_id,
project_id: audit.project_id,
row_id: audit.row_id,
fk_model_id: audit.fk_model_id,
op_type: audit.op_type,
op_sub_type: audit.op_sub_type,
status: audit.status,
description: audit.description,
details: audit.details,
created_at: audit.created_at,
updated_at: audit.updated_at,
});
};
return auditRec;
if (forceAwait) {
return await insertAudit();
} else {
insertAudit();
}
}
public static async commentsCount(args: {

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

@ -1,276 +1,302 @@
export default {
owner: '*',
creator: '*',
owner: {
exclude: {
pluginList: true,
pluginTest: true,
pluginRead: true,
pluginUpdate: true,
isPluginActive: true,
},
},
creator: {
exclude: {
pluginList: true,
pluginTest: true,
pluginRead: true,
pluginUpdate: true,
isPluginActive: true,
},
},
guest: {},
editor: {
hideAllColumns: true,
showAllColumns: true,
auditRowUpdate: true,
passwordChange: true,
// new permissions
// project
projectGet: true,
projectList: true,
projectCost: true,
//table
tableList: true,
tableGet: true,
include: {
hideAllColumns: true,
showAllColumns: true,
auditRowUpdate: true,
passwordChange: true,
// new permissions
// project
projectGet: true,
projectList: true,
projectCost: true,
//table
tableList: true,
tableGet: true,
// data
dataList: true,
dataUpdate: true,
dataDelete: true,
dataInsert: true,
dataRead: true,
dataExist: true,
dataFindOne: true,
dataGroupBy: true,
commentsCount: true,
exportCsv: true,
exportExcel: true,
// data
dataList: true,
dataUpdate: true,
dataDelete: true,
dataInsert: true,
dataRead: true,
dataExist: true,
dataFindOne: true,
dataGroupBy: true,
commentsCount: true,
exportCsv: true,
exportExcel: true,
viewList: true,
columnList: true,
viewColumnUpdate: true,
viewList: true,
columnList: true,
viewColumnUpdate: true,
sortList: true,
sortCreate: true,
sortUpdate: true,
sortDelete: true,
sortList: true,
sortCreate: true,
sortUpdate: true,
sortDelete: true,
filterList: true,
filterCreate: true,
filterUpdate: true,
filterDelete: true,
filterGet: true,
filterChildrenRead: true,
filterList: true,
filterCreate: true,
filterUpdate: true,
filterDelete: true,
filterGet: true,
filterChildrenRead: true,
mmList: true,
hmList: true,
mmExcludedList: true,
hmExcludedList: true,
btExcludedList: true,
commentList: true,
commentRow: true,
mmList: true,
hmList: true,
mmExcludedList: true,
hmExcludedList: true,
btExcludedList: true,
commentList: true,
commentRow: true,
formViewGet: true,
projectInfoGet: true,
gridColumnUpdate: true,
galleryViewGet: true,
kanbanViewGet: true,
formViewGet: true,
projectInfoGet: true,
gridColumnUpdate: true,
galleryViewGet: true,
kanbanViewGet: true,
// old
xcTableAndViewList: true,
xcAuditCreate: true,
xcAttachmentUpload: true,
xcVirtualTableList: true,
rolesGet: true,
tableXcModelGet: true,
xcRelationsGet: true,
xcModelsList: true,
xcViewModelsList: true,
xcProcedureModelsList: true,
xcFunctionModelsList: true,
xcTableModelsList: true,
xcCronList: true,
xcRelationList: true,
tableMetaCreate: true,
tableMetaDelete: true,
tableMetaRecreate: true,
viewMetaCreate: true,
viewMetaDelete: true,
viewMetaRecreate: true,
procedureMetaCreate: true,
procedureMetaDelete: true,
procedureMetaRecreate: true,
functionMetaCreate: true,
functionMetaDelete: true,
functionMetaRecreate: true,
// old
xcTableAndViewList: true,
xcAuditCreate: true,
xcAttachmentUpload: true,
xcVirtualTableList: true,
rolesGet: true,
tableXcModelGet: true,
xcRelationsGet: true,
xcModelsList: true,
xcViewModelsList: true,
xcProcedureModelsList: true,
xcFunctionModelsList: true,
xcTableModelsList: true,
xcCronList: true,
xcRelationList: true,
tableMetaCreate: true,
tableMetaDelete: true,
tableMetaRecreate: true,
viewMetaCreate: true,
viewMetaDelete: true,
viewMetaRecreate: true,
procedureMetaCreate: true,
procedureMetaDelete: true,
procedureMetaRecreate: true,
functionMetaCreate: true,
functionMetaDelete: true,
functionMetaRecreate: true,
tableCreateStatement: true,
tableInsertStatement: true,
tableUpdateStatement: true,
tableSelectStatement: true,
tableDeleteStatement: true,
tableCreateStatement: true,
tableInsertStatement: true,
tableUpdateStatement: true,
tableSelectStatement: true,
tableDeleteStatement: true,
functionList: true,
sequenceList: true,
procedureList: true,
triggerList: true,
relationList: true,
relationListAll: true,
indexList: true,
list: true,
viewRead: true,
functionRead: true,
procedureRead: true,
functionList: true,
sequenceList: true,
procedureList: true,
triggerList: true,
relationList: true,
relationListAll: true,
indexList: true,
list: true,
viewRead: true,
functionRead: true,
procedureRead: true,
getKnexDataTypes: true,
DB_PROJECT_OPEN_BY_WEB: true,
PROJECT_READ_BY_WEB: true,
projectGenerateBackend: true,
projectGenerateBackendGql: true,
projectGetTsPolicyPath: true,
projectGetPolicyPath: true,
projectGetGqlPolicyPath: true,
handleApiCall: true,
executeRawQuery: true,
projectHasDb: true,
testConnection: true,
projectChangeEnv: true,
getKnexDataTypes: true,
DB_PROJECT_OPEN_BY_WEB: true,
PROJECT_READ_BY_WEB: true,
projectGenerateBackend: true,
projectGenerateBackendGql: true,
projectGetTsPolicyPath: true,
projectGetPolicyPath: true,
projectGetGqlPolicyPath: true,
handleApiCall: true,
executeRawQuery: true,
projectHasDb: true,
testConnection: true,
projectChangeEnv: true,
xcRoutesPolicyAllGet: true,
grpcProtoDownloadZip: true,
xcRoutesPolicyAllGet: true,
grpcProtoDownloadZip: true,
xcModelRowAuditAndCommentList: true,
xcAuditCommentInsert: true,
xcAuditModelCommentsCount: true,
xcExportAsCsv: true,
xcModelRowAuditAndCommentList: true,
xcAuditCommentInsert: true,
xcAuditModelCommentsCount: true,
xcExportAsCsv: true,
bulkDataInsert: true,
bulkDataUpdate: true,
bulkDataUpdateAll: true,
bulkDataDelete: true,
bulkDataDeleteAll: true,
relationDataRemove: true,
relationDataAdd: true,
dataCount: true,
upload: true,
uploadViaURL: true,
bulkDataInsert: true,
bulkDataUpdate: true,
bulkDataUpdateAll: true,
bulkDataDelete: true,
bulkDataDeleteAll: true,
relationDataRemove: true,
relationDataAdd: true,
dataCount: true,
upload: true,
uploadViaURL: true,
},
},
commenter: {
formViewGet: true,
passwordChange: true,
// project
projectGet: true,
exportCsv: true,
exportExcel: true,
include: {
formViewGet: true,
passwordChange: true,
// project
projectGet: true,
exportCsv: true,
exportExcel: true,
//table
tableGet: true,
// sort & filter
sortList: true,
viewList: true,
columnList: true,
//table
tableGet: true,
// sort & filter
sortList: true,
viewList: true,
columnList: true,
mmList: true,
hmList: true,
commentList: true,
commentRow: true,
projectInfoGet: true,
mmList: true,
hmList: true,
commentList: true,
commentRow: true,
projectInfoGet: true,
// data
dataList: true,
dataRead: true,
dataExist: true,
dataFindOne: true,
dataGroupBy: true,
commentsCount: true,
// data
dataList: true,
dataRead: true,
dataExist: true,
dataFindOne: true,
dataGroupBy: true,
commentsCount: true,
galleryViewGet: true,
kanbanViewGet: true,
alleryViewGet: true,
kanbanViewGet: true,
xcTableAndViewList: true,
xcVirtualTableList: true,
projectList: true,
projectCost: true,
PROJECT_READ_BY_WEB: true,
xcTableAndViewList: true,
xcVirtualTableList: true,
projectList: true,
projectCost: true,
PROJECT_READ_BY_WEB: true,
tableXcModelGet: true,
xcRelationList: true,
tableList: true,
functionList: true,
sequenceList: true,
procedureList: true,
triggerList: true,
relationList: true,
relationListAll: true,
indexList: true,
list: true,
tableXcModelGet: true,
xcRelationList: true,
tableList: true,
functionList: true,
sequenceList: true,
procedureList: true,
triggerList: true,
relationList: true,
relationListAll: true,
indexList: true,
list: true,
xcModelRowAuditAndCommentList: true,
xcAuditCommentInsert: true,
xcAuditModelCommentsCount: true,
xcExportAsCsv: true,
dataCount: true,
xcModelRowAuditAndCommentList: true,
xcAuditCommentInsert: true,
xcAuditModelCommentsCount: true,
xcExportAsCsv: true,
dataCount: true,
},
},
viewer: {
formViewGet: true,
passwordChange: true,
// project
projectGet: true,
//table
tableGet: true,
// data
dataList: true,
dataRead: true,
dataExist: true,
dataFindOne: true,
dataGroupBy: true,
commentsCount: true,
exportCsv: true,
exportExcel: true,
include: {
formViewGet: true,
passwordChange: true,
// project
projectGet: true,
//table
tableGet: true,
// data
dataList: true,
dataRead: true,
dataExist: true,
dataFindOne: true,
dataGroupBy: true,
commentsCount: true,
exportCsv: true,
exportExcel: true,
// sort & filter
sortList: true,
filterList: true,
projectInfoGet: true,
// sort & filter
sortList: true,
filterList: true,
projectInfoGet: true,
galleryViewGet: true,
kanbanViewGet: true,
galleryViewGet: true,
kanbanViewGet: true,
mmList: true,
hmList: true,
commentList: true,
commentRow: false,
mmList: true,
hmList: true,
commentList: true,
commentRow: false,
xcTableAndViewList: true,
xcVirtualTableList: true,
projectList: true,
projectCost: true,
PROJECT_READ_BY_WEB: true,
xcTableAndViewList: true,
xcVirtualTableList: true,
projectList: true,
projectCost: true,
PROJECT_READ_BY_WEB: true,
tableXcModelGet: true,
xcRelationList: true,
tableList: true,
viewList: true,
functionList: true,
sequenceList: true,
procedureList: true,
columnList: true,
triggerList: true,
relationList: true,
relationListAll: true,
indexList: true,
list: true,
xcExportAsCsv: true,
dataCount: true
tableXcModelGet: true,
xcRelationList: true,
tableList: true,
viewList: true,
functionList: true,
sequenceList: true,
procedureList: true,
columnList: true,
triggerList: true,
relationList: true,
relationListAll: true,
indexList: true,
list: true,
xcExportAsCsv: true,
dataCount: true,
},
},
user_new: {
passwordChange: true,
projectList: true,
include: {
passwordChange: true,
projectList: true,
},
},
super: '*',
user: {
upload: true,
uploadViaURL: true,
passwordChange: true,
pluginList: true,
pluginRead: true,
pluginTest: true,
isPluginActive: true,
pluginUpdate: true,
projectCreate: true,
projectList: true,
projectCost: true,
handleAxiosCall: true,
testConnection: true,
projectCreateByWeb: true,
projectCreateByWebWithXCDB: true,
xcPluginRead: true,
xcMetaTablesImportZipToLocalFsAndDb: true,
xcMetaTablesExportDbToZip: true,
auditRowUpdate: true,
include: {
upload: true,
uploadViaURL: true,
passwordChange: true,
pluginList: true,
pluginRead: true,
pluginTest: true,
isPluginActive: true,
pluginUpdate: true,
projectCreate: true,
projectList: true,
projectCost: true,
handleAxiosCall: true,
testConnection: true,
projectCreateByWeb: true,
projectCreateByWebWithXCDB: true,
xcPluginRead: true,
xcMetaTablesImportZipToLocalFsAndDb: true,
xcMetaTablesExportDbToZip: true,
auditRowUpdate: true,
},
},
};

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

@ -6,7 +6,7 @@ import TestDbMngr from './TestDbMngr'
import dotenv from 'dotenv';
process.env.NODE_ENV = 'test';
process.env.TEST = 'test';
process.env.TEST = 'true';
process.env.NC_DISABLE_CACHE = '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 {
projectsPage.createProject(proj.basic, proj.config);
// cy.wait(5000);
if (dbType === "xcdb") {
// store base URL- to re-visit and delete form view later
let projId;
@ -240,11 +239,9 @@ export const genTest = (apiType, dbType) => {
cy_createProjectBlock(proj, apiType, dbType);
}
// kludge: wait for page load to finish
cy.wait(2000);
// close team & auth tab
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
cy.saveLocalStorage();

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

@ -44,13 +44,16 @@ export const genTest = (apiType, dbType) => {
};
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();
settingsPage.openMenu(settingsPage.AUDIT);
// wait for column headers to appear
//
cy.get("thead > tr > th.ant-table-cell").should("have.length", 5);
cy.wait(3000);
cy.wait("@waitForPageLoad");
// Audit table entries
// [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();
// 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);
// 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 })
.click()
.then(() => {
// wait to ensure pop up appears before we proceed further
cy.wait(1000);
// Link card validation
cy.getActiveDrawer(".nc-drawer-expanded-form")
.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
//
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
cy.get(".nc-add-new-row-btn").click();
cy.wait(1000);
cy.get(".nc-expand-col-Country")
.find(".nc-cell > input")
.first()
@ -73,8 +77,8 @@ export const genTest = (apiType, dbType) => {
// verify
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000);
cy.wait("@waitForPageLoad");
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
// //
//
// // // kludge: wait for page load to finish
// // cy.wait(1000);
// // // close team & auth tab
// // cy.get('button.ant-tabs-tab-remove').should('exist').click();
// // cy.wait(1000);
//
// cy.openTableTab("City", 25);
// });
@ -63,7 +60,8 @@ export const genTest = (apiType, dbType) => {
.contains("Lookup")
.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);
// 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) => {
cy.get("label").contains(label).parents(".ant-row").click();
cy.wait(500);
};
// Run once before test- create project (rest/graphql)
@ -18,12 +17,8 @@ export const genTest = (apiType, dbType) => {
// cy.fileHook();
// mainPage.tabReset();
//
// // // kludge: wait for page load to finish
// // cy.wait(1000);
// // // close team & auth tab
// // cy.get('button.ant-tabs-tab-remove').should('exist').click();
// // cy.wait(1000);
//
// // open a table to work on views
// //
// cy.openTableTab("Country", 25);
@ -69,7 +64,8 @@ export const genTest = (apiType, dbType) => {
.contains("Rollup")
.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);
// Configure Child table & column names

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

@ -18,11 +18,8 @@ export const genTest = (apiType, dbType) => {
before(() => {
mainPage.tabReset();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
cy.createTable(tableName);
});
@ -126,7 +123,6 @@ export const genTest = (apiType, dbType) => {
) => {
if (isNewRow) {
cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.wait(500);
cy.get(".nc-add-new-row-btn").click();
} else {
// 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
//
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
cy.get(`:nth-child(${index}) > [data-title="${columnName}"]`)
.last()
@ -107,7 +112,7 @@ export const genTest = (apiType, dbType) => {
.click();
// Glitch; hence wait
cy.wait(1000);
cy.wait("@unlinkCount");
}
// 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)
cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(300);
cy.getActiveModal(".nc-modal-view-delete")
.find(".ant-btn-dangerous")
.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
cy.openTableTab("Country", 25);
cy.wait(1000);
cy.get(`.nc-view-item.nc-${viewType}-view-item`)
.contains("Form-1")
@ -338,7 +337,6 @@ export const genTest = (apiType, dbType) => {
settingsPage.openMenu(settingsPage.APPSTORE);
mainPage.resetSMTP();
cy.wait(300);
cy.openTableTab("Country", 25);
});
@ -383,7 +381,6 @@ export const genTest = (apiType, dbType) => {
// click on delete icon (becomes visible on hovering mouse)
cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(1000);
cy.getActiveModal(".nc-modal-view-delete")
.find(".ant-btn-dangerous")
.click();

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

@ -112,7 +112,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(linkText, {
baseUrl: null,
});
// cy.wait(5000);
cy.wait(["@waitForPageLoad"], { times: 2 });
// 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
// // this auto verifies successfull addition of rows to table as well
// mainPage.getPagination(25).click();
// // kludge: flicker on load
// cy.wait(3000)
//
// cy.get(".nc-grid-row").should("have.length", 1);
// 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();
mainPage.shareView().click({ force: true });
cy.wait(5000);
// wait, as URL initially will be /undefined
cy.getActiveModal(".nc-modal-share-view")
.find(".share-link-box")
@ -136,7 +134,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(viewURL["combined"], {
baseUrl: null,
});
cy.wait(5000);
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 18);
@ -353,7 +350,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(storedURL, {
baseUrl: null,
});
cy.wait(5000);
// number of view entries should be 2 before we delete
cy.get(".nc-view-item").its("length").should("eq", 2);
@ -390,12 +386,9 @@ export const genTest = (apiType, dbType) => {
cy.visit(storedURL, {
baseUrl: null,
});
cy.wait(5000);
// delete row
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000);
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10);
@ -439,7 +432,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(viewURL["rowColUpdate"], {
baseUrl: null,
});
cy.wait(5000);
//5
// 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) => {
if (!isTestSuiteActive(apiType, dbType)) return;
let clear;
describe(`${apiType.toUpperCase()} api - Table views: Expanded form`, () => {
before(() => {
cy.restoreLocalStorage();
// open a table to work on views
//
cy.openTableTab("Country", 25);
cy.openTableTab('Country', 25);
clear = Cypress.LocalStorage.clear;
Cypress.LocalStorage.clear = () => {}
});
beforeEach(() => {
@ -44,6 +49,7 @@ export const genTest = (apiType, dbType) => {
cy.restoreLocalStorage();
cy.closeTableTab("Country");
cy.saveLocalStorage();
Cypress.LocalStorage.clear = clear;
});
// Common routine to create/edit/delete GRID & GALLERY view
@ -66,7 +72,8 @@ export const genTest = (apiType, dbType) => {
.should("exist");
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");
cy.get(".nc-fields-menu-btn").click();
@ -75,9 +82,8 @@ export const genTest = (apiType, dbType) => {
.click();
cy.get(".nc-fields-menu-btn").click();
cy.wait(["@getGalleryViewData"]);
cy.get('.ant-card-body [title="City List"]').should("exist");
cy.wait(1000);
// cy.wait(['@getGalleryView'])
}
});

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

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

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

@ -59,8 +59,6 @@ export const genTest = (apiType, dbType, roleType) => {
after(() => {
// cy.get(".nc-preview-reset").click({ force: true });
cy.get(".mdi-exit-to-app").click();
// cy.wait(20000)
// wait for page rendering to complete
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
// this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click();
// kludge: flicker on load
cy.wait(3000);
// wait for page rendering to complete
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();
cy.getActiveMenu(".nc-dropdown-grid-context-menu")
@ -77,8 +71,6 @@ export const genTest = (apiType, dbType) => {
mainPage.shareView().click();
// cy.wait(5000);
// copy link text, visit URL
cy.getActiveModal(".nc-modal-share-view")
.find(".share-link-box")
@ -97,7 +89,6 @@ export const genTest = (apiType, dbType) => {
cy.visit(linkText, {
baseUrl: null,
});
// cy.wait(5000);
cy.wait(["@waitForSharedViewLoad"]);
// 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`, () => {
// come back to main window
loginPage.loginAndOpenProject(apiType, dbType);
// cy.visit('/')
// cy.wait(5000)
// projectsPage.openConfiguredProject(apiType, dbType);
cy.openTableTab("Country", 25);
// cy.wait(1000);
mainPage.filterField("testAttach", "is not null", null);
mainPage.hideField("LastUpdate");

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

@ -21,12 +21,14 @@ export const genTest = (apiType, dbType) => {
const permissionValidation = (roleType) => {
it(`${roleType}: Visit base shared URL`, () => {
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
cy.visit(linkText, {
baseUrl: null,
});
cy.wait(5000);
cy.wait(["@waitForPageLoad"]);
});
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;
});
cy.visit("/");
// cy.visit("/");
projectsPage.createProject(
{ dbType: "none", apiType: "REST", name: "importSample" },
{}
);
cy.wait(4000);
cy.saveLocalStorage();
});
@ -122,6 +121,11 @@ export const genTest = (apiType, dbType) => {
});
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()
.find(".ant-collapse-item")
.then((sheets) => {
@ -171,7 +175,7 @@ export const genTest = (apiType, dbType) => {
cy.getActiveModal().find(".ant-btn-primary").click();
// wait for page to get loaded (issue observed in CI-CD)
cy.wait(5000);
cy.wait("@waitForPageLoad");
});
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"))
.should("exist")
.click();
cy.wait(1000);
cy.get(".nc-filter-operation-select").should("exist").last().click();
cy.get(".ant-select-dropdown:visible")
@ -162,8 +161,9 @@ function clearServerData() {
function addNewRow(index, cellValue) {
cy.get(".nc-add-new-row-btn:visible").should("exist");
cy.get(".nc-add-new-row-btn").click();
cy.wait(1000);
cy.get(".nc-expand-col-Title")
.should("exist")
.find(".nc-cell > input")
.first()
.type(cellValue);
@ -185,6 +185,7 @@ function updateRow(index, cellValue) {
.click({ force: true });
cy.get(".nc-expand-col-Title")
.should("exist")
.find(".nc-cell > input")
.should("exist")
.first()
@ -207,7 +208,7 @@ function updateRow(index, cellValue) {
function verifyHookTrigger(count, lastValue) {
// allow message to be received
cy.wait(500);
cy.wait(100);
cy.request("http://localhost:9090/hook/count").then((msg) => {
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) => {
let clear;
if (!isTestSuiteActive(apiType, dbType)) return;
describe(`Quick Tests`, () => {
let cellIdx = 1;
@ -115,6 +117,9 @@ export const genTest = (apiType, dbType, testMode) => {
cy.openTableTab("Film", 3);
cy.saveLocalStorage();
clear = Cypress.LocalStorage.clear;
Cypress.LocalStorage.clear = () => {};
});
beforeEach(() => {
@ -129,6 +134,8 @@ export const genTest = (apiType, dbType, testMode) => {
cy.restoreLocalStorage();
cy.signOut();
cy.saveLocalStorage();
Cypress.LocalStorage.clear = clear;
});
it("Verify Schema", () => {

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

@ -40,7 +40,8 @@ export function _advSettings(roleType, mode) {
.click();
cy.get(`[data-menu-id="teamAndAuth"]`).should("exist");
cy.get(`[data-menu-id="appStore"]`).should("exist");
if (roleType === "owner")
cy.get(`[data-menu-id="appStore"]`).should("exist");
cy.get(`[data-menu-id="projMetaData"]`).should("exist");
cy.get(`[data-menu-id="audit"]`).should("exist");
@ -203,8 +204,6 @@ export function _editComment(roleType, mode) {
cy.openTableTab(columnName, 25);
cy.wait(1000);
// click on comment icon & type comment
//
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
//
cy.wait(3000);
if ("viewer" === roleType) {
cy.getActiveDrawer(".nc-drawer-expanded-form")
.should("exist")

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

@ -27,7 +27,6 @@ export const genTest = (apiType, dbType) => {
.then(() => {
let query = `ALTER TABLE "actor" RENAME TO "${projId}actor"`;
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 t5a = require("../common/5a_user_role");
let t5b = require("../common/5b_preview_role");
let t5c = require("../common/5c_super_user_role");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
@ -12,6 +13,7 @@ const nocoTestSuite = (apiType, dbType) => {
t5a.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
};
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 t5a = require("../common/5a_user_role");
let t5b = require("../common/5b_preview_role");
let t5c = require("../common/5c_super_user_role");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
@ -12,6 +13,8 @@ const nocoTestSuite = (apiType, dbType) => {
t5a.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
};
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 t5a = require("../common/5a_user_role");
let t5b = require("../common/5b_preview_role");
let t5c = require("../common/5c_super_user_role");
const {
setCurrentMode,
} = require("../../support/page_objects/projectConstants");
@ -12,6 +13,7 @@ const nocoTestSuite = (apiType, dbType) => {
t5a.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType);
t5c.genTest(apiType, dbType);
};
nocoTestSuite("rest", "xcdb");

24
scripts/cypress/support/commands.js

@ -47,16 +47,12 @@ Cypress.Commands.add("signinOrSignup", (_args) => {
_args
);
cy.wait(1000);
// signin/signup
cy.get("body").then(($body) => {
// cy.wait(1000)
cy.url().then((url) => {
if (!url.includes("/projects")) {
// handle initial load
if ($body.find(".welcome-page").length > 0) {
cy.wait(8000);
cy.get("body").trigger("mousemove");
cy.snip("LetsBegin");
cy.contains("Let's Begin").click();
@ -173,7 +169,7 @@ Cypress.Commands.add("closeTableTab", (tn) => {
.click();
// subsequent tab open commands will fail if tab is not closed completely
cy.wait(1000);
cy.wait(100);
});
Cypress.Commands.add("openOrCreateGqlProject", (_args) => {
@ -288,9 +284,7 @@ Cypress.Commands.add("getActivePicker", (dropdownSelector) => {
Cypress.Commands.add("createTable", (name) => {
cy.task("log", `[createTableTab] ${name}`);
cy.wait(1000);
cy.get(".nc-add-new-table").should("exist").click();
cy.wait(1000);
cy.getActiveModal(".nc-modal-table-create")
.find(`input[type="text"]:visible`)
.click()
@ -300,12 +294,9 @@ Cypress.Commands.add("createTable", (name) => {
cy.getActiveModal(".nc-modal-table-create")
.find("button.ant-btn-primary:visible")
.click();
cy.wait(1000);
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.get(`.nc-project-tree-tbl-${name}`).should("exist");
cy.wait(1000);
});
Cypress.Commands.add("deleteTable", (name, dbType) => {
@ -426,7 +417,6 @@ Cypress.Commands.add("snip", (filename) => {
) {
let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename);
cy.wait(1000);
cy.screenshot(storeName, { overwrite: true });
}
});
@ -439,7 +429,6 @@ Cypress.Commands.add("snipActiveModal", (filename) => {
) {
let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename);
cy.wait(1000);
// cy.getActiveModal().screenshot(filename, {
// padding: 0,
// overwrite: true,
@ -456,7 +445,6 @@ Cypress.Commands.add("snipActiveMenu", (filename) => {
) {
let storeName = `${screenShotDb.length}_${filename}`;
screenShotDb.push(filename);
cy.wait(1000);
// cy.getActiveMenu().screenshot(filename, {
// padding: 0,
// overwrite: true,
@ -479,13 +467,19 @@ Cypress.Commands.add("signOut", () => {
cy.get(".nc-menu-accounts", { timeout: 30000 }).should("exist").click();
cy.getActiveMenu(".nc-dropdown-user-accounts-menu")
.find(".ant-dropdown-menu-item")
.eq(1)
.last()
.click();
// cy.wait(5000);
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
// 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
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
cy.getActiveModal(".nc-modal-invite-user-and-share-base").should("exist");
cy.getActiveModal(".nc-modal-invite-user-and-share-base")
.find('input[placeholder="E-mail"]')
.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
// cy.getActiveSelection().contains(roleType).click({force: true});
@ -256,13 +260,18 @@ export class _mainPage {
.trigger("mouseover", { 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").should("not.be.visible");
// cy.get(".ant-btn-dangerous:visible").contains("Delete").click();
cy.get(".nc-column-delete").click();
cy.wait(500);
cy.get(".nc-column-delete").should("not.be.visible");
cy.get(".ant-btn-dangerous:visible").contains("Delete").click();
cy.wait(500);
cy.getActiveModal(".nc-modal-column-delete")
.find(".ant-btn-dangerous:visible")
.contains("Delete")
.click();
cy.get(`th:contains(${colName})`).should("not.exist");
};
@ -549,9 +558,7 @@ export class _mainPage {
.find(".nc-project-menu-item")
.contains("Download")
.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")
.click();
}
@ -624,9 +631,13 @@ export class _mainPage {
}
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.wait(2000);
cy.get(`.nc-metasync-row-${tbl}`).contains(msg).should("exist");
cy.wait("@metaSync");
cy.get(`.nc-metasync-row-${tbl}:contains(${msg})`).should("exist");
cy.get(".nc-btn-metasync-sync-now")
.should("exist")
.click()

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

@ -78,11 +78,8 @@ export class _loginPage {
// projectsPage.openProject(staticProjects.pgExternalREST.basic.name);
// }
//
// // kludge: wait for page load to finish
// cy.wait(2000);
// // close team & auth tab
// 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);
}
// kludge: wait for page load to finish
// cy.wait(4000);
cy.wait("@waitForPageLoad");
// close team & auth tab
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
@ -155,6 +150,8 @@ export class _projectsPage {
cy.get(".nc-metadb-project-name").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);
// feed project name
@ -185,7 +182,8 @@ export class _projectsPage {
cy.get(".nc-extdb-proj-name").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.get(".nc-extdb-proj-name").clear().type(projectName);
@ -208,7 +206,6 @@ export class _projectsPage {
// Create project
cy.contains("Ok & Save Project", { timeout: 20000 }).click();
cy.wait(5000);
// takes a while to load project
this.waitHomePageLoad();

Loading…
Cancel
Save