Browse Source

Merge branch 'develop' of https://github.com/nocodb/nocodb into geodata-prototyping-restart

pull/4723/head
flisowna 2 years ago
parent
commit
4360c1ad31
  1. 5
      .github/workflows/ci-cd.yml
  2. 1
      .github/workflows/dco-check.yml
  3. 7
      .github/workflows/release-nocodb.yml
  4. 6
      .gitignore
  5. 23
      markdown/readme/languages/chinese.md
  6. 24
      markdown/readme/languages/german.md
  7. 2174
      package-lock.json
  8. 19
      package.json
  9. 12
      packages/nc-cli/package-lock.json
  10. 2
      packages/nc-gui/app.vue
  11. 5
      packages/nc-gui/assets/style.scss
  12. 1
      packages/nc-gui/components.d.ts
  13. 4
      packages/nc-gui/components/account/UserList.vue
  14. 4
      packages/nc-gui/components/account/UsersModal.vue
  15. 44
      packages/nc-gui/components/cell/MultiSelect.vue
  16. 36
      packages/nc-gui/components/cell/SingleSelect.vue
  17. 7
      packages/nc-gui/components/smartsheet/Gallery.vue
  18. 7
      packages/nc-gui/components/smartsheet/Kanban.vue
  19. 9
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  20. 15
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  21. 3
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  22. 27
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  23. 9
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  24. 21
      packages/nc-gui/components/virtual-cell/Lookup.vue
  25. 12
      packages/nc-gui/composables/useColumnCreateStore.ts
  26. 34
      packages/nc-gui/composables/useFieldQuery.ts
  27. 19
      packages/nc-gui/composables/useSmartsheetStore.ts
  28. 2
      packages/nc-gui/lang/ar.json
  29. 3
      packages/nc-gui/lang/bn_IN.json
  30. 2
      packages/nc-gui/lang/da.json
  31. 2
      packages/nc-gui/lang/de.json
  32. 2
      packages/nc-gui/lang/es.json
  33. 2
      packages/nc-gui/lang/fa.json
  34. 2
      packages/nc-gui/lang/fi.json
  35. 2
      packages/nc-gui/lang/fr.json
  36. 2
      packages/nc-gui/lang/he.json
  37. 2
      packages/nc-gui/lang/hi.json
  38. 2
      packages/nc-gui/lang/hr.json
  39. 2
      packages/nc-gui/lang/id.json
  40. 2
      packages/nc-gui/lang/it.json
  41. 2
      packages/nc-gui/lang/ja.json
  42. 2
      packages/nc-gui/lang/ko.json
  43. 2
      packages/nc-gui/lang/lv.json
  44. 2
      packages/nc-gui/lang/nl.json
  45. 2
      packages/nc-gui/lang/no.json
  46. 2
      packages/nc-gui/lang/pl.json
  47. 2
      packages/nc-gui/lang/pt.json
  48. 2
      packages/nc-gui/lang/pt_BR.json
  49. 2
      packages/nc-gui/lang/ru.json
  50. 2
      packages/nc-gui/lang/sl.json
  51. 2
      packages/nc-gui/lang/sv.json
  52. 2
      packages/nc-gui/lang/th.json
  53. 2
      packages/nc-gui/lang/tr.json
  54. 74
      packages/nc-gui/lang/uk.json
  55. 76
      packages/nc-gui/lang/vi.json
  56. 2
      packages/nc-gui/lang/zh-Hans.json
  57. 298
      packages/nc-gui/lang/zh-Hant.json
  58. 2
      packages/nc-gui/layouts/default.vue
  59. 2
      packages/nc-gui/package-lock.json
  60. 1
      packages/nc-gui/pages/account/index.vue
  61. 5
      packages/nc-gui/pages/account/index/users.vue
  62. 30
      packages/nc-gui/pages/account/index/users/[[nestedPage]].vue
  63. 22
      packages/nc-gui/pages/index/index/[projectId].vue
  64. 19
      packages/nc-gui/pages/index/index/create.vue
  65. 156
      packages/nc-gui/pages/index/index/index.vue
  66. 5
      packages/nc-gui/pages/index/index/user.vue
  67. 2
      packages/nc-gui/windi.config.ts
  68. 2
      packages/nc-lib-gui/package.json
  69. 12
      packages/nc-plugin/package-lock.json
  70. 224
      packages/noco-docs/content/en/engineering/playwright.md
  71. 3
      packages/noco-docs/content/en/engineering/translation.md
  72. 161
      packages/noco-docs/content/en/engineering/unit-testing.md
  73. 12
      packages/noco-docs/package-lock.json
  74. 4
      packages/nocodb-sdk/package-lock.json
  75. 2
      packages/nocodb-sdk/package.json
  76. 15
      packages/nocodb/docker-compose.yml
  77. 357
      packages/nocodb/package-lock.json
  78. 13
      packages/nocodb/package.json
  79. 2
      packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts
  80. 473
      packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts
  81. 4
      packages/nocodb/src/lib/db/sql-client/lib/pg/PgClient.ts
  82. 26
      packages/nocodb/src/lib/db/sql-data-mapper/lib/BaseModel.ts
  83. 17
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts
  84. 65
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  85. 3
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts
  86. 4
      packages/nocodb/src/lib/meta/api/index.ts
  87. 4
      packages/nocodb/src/lib/meta/api/orgLicenseApis.ts
  88. 3
      packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts
  89. 7
      packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerColumnMetas.ts
  90. 21
      packages/nocodb/src/lib/meta/api/swagger/helpers/swagger-base.json
  91. 2
      packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts
  92. 12
      packages/nocodb/src/lib/meta/api/sync/helpers/job.ts
  93. 2
      packages/nocodb/src/lib/meta/api/testApis.ts
  94. 9
      packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts
  95. 6
      packages/nocodb/src/lib/models/SelectOption.ts
  96. 18
      packages/nocodb/src/lib/services/test/TestResetService/index.ts
  97. 10
      packages/nocodb/src/lib/utils/projectAcl.ts
  98. 53
      scripts/cypress/cypress.json
  99. 67
      scripts/cypress/docker-compose-cypress.yml
  100. 17
      scripts/cypress/docker-compose-pg-cy-quick.yml
  101. Some files were not shown because too many files have changed in this diff Show More

5
.github/workflows/ci-cd.yml

@ -8,7 +8,6 @@ on:
branches: [develop]
paths:
- "packages/nc-gui/**"
- "scripts/cypress/**"
- "packages/nocodb/**"
- ".github/workflows/ci-cd.yml"
pull_request:
@ -16,7 +15,6 @@ on:
branches: [develop]
paths:
- "packages/nc-gui/**"
- "scripts/cypress/**"
- "packages/nocodb/**"
- ".github/workflows/ci-cd.yml"
@ -60,9 +58,6 @@ jobs:
- name: Install dependencies
working-directory: ./packages/nocodb
run: npm install
- name: setup mysql
working-directory: ./
run: docker-compose -f ./scripts/cypress/docker-compose-cypress.yml up -d
- name: run unit tests
working-directory: ./packages/nocodb
run: npm run test:unit

1
.github/workflows/dco-check.yml

@ -10,7 +10,6 @@ on:
- "packages/nc-lib-gui/**"
- "packages/nc-plugin/**"
- "packages/nocodb/**"
- "scripts/cypress/**"
jobs:
commits_check_job:

7
.github/workflows/release-nocodb.yml

@ -117,12 +117,7 @@ jobs:
secrets:
GH_TOKEN: "${{ secrets.GH_TOKEN }}"
# Change nocodb-sdk back to local path
update-sdk-path:
needs: publish-docs
uses: ./.github/workflows/update-sdk-path.yml
# Sync changes to develop
sync-to-develop:
needs: update-sdk-path
needs: close-issues
uses: ./.github/workflows/sync-to-develop.yml

6
.gitignore vendored

@ -84,12 +84,6 @@ mongod
/packages/nocodb/docker/main.js.LICENSE.txt
/packages/nocodb/noco_log.db
# Cypress
#=========
shared.json
/scripts/Cypress/screenshots
/scripts/exp/
# NC_DBs
#=========
nc_minimal_dbs/

23
markdown/readme/languages/chinese.md

@ -255,29 +255,6 @@ npm run dev
> nocodb/packages/nocodb 包括 nc-lib-gui,它是 npm 源中托管的 nc-gui 的预构建版本。如果您只想修改后端,则可以在本地启动后端后在浏览器中访问 localhost:8000/dashboard
## 本地运行 Cypress 测试
```shell
# 安装依赖 (cypress)
npm install
# 使用 docker compose 运行带有所需数据库的 mysql 数据库
docker-compose -f ./scripts/cypress/docker-compose-cypress.yml up
# 使用以下命令运行后端 api
npm run start:api
# 使用以下命令运行前端 Web UI
npm run start:web
# 等待 3000 和 8080 端口都可用后
# 使用以下命令运行 cypress 测试
npm run cypress:run
# 或运行以下命令以使用 GUI 运行它
npm run cypress:open
```
# 贡献
参见[贡献指南](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md).

24
markdown/readme/languages/german.md

@ -236,30 +236,6 @@ npm run dev
> nocodb/packages/nocodb enthält nc-lib-gui, die entwickelte Version von nc-gui, die in der npm-Registry gehostet wird. Sie können localhost:8000/dashboard im Browser aufrufen, nachdem Sie das Backend lokal gestartet haben, wenn Sie nur das Backend ändern möchten.
## Cypress-Tests lokal ausführen
```shell
# install dependencies (cypress)
npm install
# MySQL-Datenbank mit der benötigten Datenbank mit Docker Compose ausführen
docker-compose -f ./scripts/cypress/docker-compose-cypress.yml up
# Backend API mit folgendem Befehl ausführen
npm run start:api
# Frontend Web-UI mit folgendem Befehl ausführen
npm run start:web
# Warten, bis die beiden Ports 3000 und 8000 verfügbar sind,
# dann Cypress Test mit diesem Befehl ausführen
npm run cypress:run
# Oder diesen Befehl ausführen, um die GUI auszuführen
npm run cypress:open
```
# Beiträge
Siehe [Contribution Guide](https://github.com/nocodb/nocodb/blob/master/.github/CONTRIBUTING.md).

2174
package-lock.json generated

File diff suppressed because it is too large Load Diff

19
package.json

@ -16,10 +16,6 @@
},
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@4tw/cypress-drag-drop": "^2.0.0",
"cypress": "^9.2.0",
"cypress-file-upload": "^5.0.8",
"cypress-iframe": "^1.0.1",
"fs": "0.0.1-security",
"lerna": "^3.20.1",
"husky": "^8.0.0",
@ -39,25 +35,16 @@
"lint:staged:playwright": "cd ./tests/playwright; npx lint-staged; cd -",
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"install:common": "cd ./packages/nocodb; npm install ../nocodb-sdk; cd ../nc-gui; npm install ../nocodb-sdk",
"start:api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:xcdb-api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_EXPORT_MAX_TIMEOUT=60000 NC_DISABLE_CACHE=true NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk;npm install; NC_EXPORT_MAX_TIMEOUT=60000 NC_DISABLE_TELE=true npm run watch:run:cypress",
"start:api:cache:pg": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg",
"start:api:cache:pg:cyquick": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_TELE=true npm run watch:run:cypress:pg:cyquick",
"start:xcdb-api:cache": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_EXPORT_MAX_TIMEOUT=60000 NC_DISABLE_TELE=true NC_INFLECTION=camelize DATABASE_URL=sqlite:../../../scripts/cypress/fixtures/sqlite-sakila/sakila.db npm run watch:run:cypress",
"start:web": "npm run build:common ; cd ./packages/nc-gui; npm install ../nocodb-sdk; npm install; ANT_MESSAGE_DURATION=0.5 npm run dev",
"cypress:run": "cypress run --config-file ./scripts/cypress/cypress.json",
"cypress:open": "cypress open --config-file ./scripts/cypress/cypress.json",
"cypress:clear": "cypress cache clear",
"test:travis": "git log --pretty=format:'%h' -n 1 --skip 1 | xargs lerna run test:travis --since",
"lerna:install": "git log --pretty=format:'%h' -n 1 --skip 1 | xargs lerna bootstrap --ignore nc-cli --since",
"updated:xc-migrator": "lerna run publish --scope xc-migrator && lerna run xc && lerna publish && npm install -f xc-cli",
"doc": "lerna run doc",
"install:local:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib;rm package-lock.json; npm i ../../../xc-lib-private; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i ../../../xc-lib-private;npm i ../xc-lib-gui",
"install:npm:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib; npm i -S xc-lib@latest; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i -S xc-lib@latest xc-lib-gui@latest;npm i ../xc-lib-gui",
"start:pg": "docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d",
"stop:pg": "docker-compose -f ./scripts/cypress/docker-compose-pg.yml down",
"prepare": "husky install"
"prepare": "husky install",
"start:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml up -d",
"stop:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml down"
},
"dependencies": {
"express": "^4.18.1",

12
packages/nc-cli/package-lock.json generated

@ -9465,9 +9465,9 @@
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -22773,9 +22773,9 @@
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}

2
packages/nc-gui/app.vue

@ -36,7 +36,7 @@ if (typeof window !== 'undefined') {
<template>
<a-config-provider>
<NuxtLayout :name="disableBaseLayout ? false : 'base'">
<NuxtPage :key="key" />
<NuxtPage :key="key" :transition="false" />
</NuxtLayout>
</a-config-provider>
</template>

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

@ -281,3 +281,8 @@ a {
.ant-modal {
@apply !top-[50px];
}
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
@apply bg-primary bg-opacity-20;
}

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

@ -125,7 +125,6 @@ declare module '@vue/runtime-core' {
MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
MdiCancel: typeof import('~icons/mdi/cancel')['default']
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']

4
packages/nc-gui/components/account/UserList.vue

@ -29,7 +29,9 @@ const searchText = ref<string>('')
const pagination = reactive({
total: 0,
pageSize: 10,
position: ['bottomCenter'],
})
const loadUsers = async (page = currentPage, limit = currentLimit) => {
currentPage = page
try {
@ -158,7 +160,7 @@ const copyPasswordResetUrl = async (user: User) => {
<a-table
:row-key="(record) => record.id"
:data-source="users"
:pagination="{ position: ['bottomCenter'] }"
:pagination="pagination"
:loading="isLoading"
size="small"
@change="loadUsers($event.current)"

4
packages/nc-gui/components/account/UsersModal.vue

@ -4,13 +4,13 @@ import {
Form,
computed,
extractSdkResponseErrorMsg,
isEmail,
message,
ref,
useCopy,
useDashboard,
useI18n,
useNuxtApp,
validateEmail,
} from '#imports'
import type { User } from '~/lib'
import { Role } from '~/lib'
@ -52,7 +52,7 @@ const validators = computed(() => {
callback('Email is required')
return
}
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !isEmail(e))
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !validateEmail(e))
if (invalidEmails.length > 0) {
callback(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `)
} else {

44
packages/nc-gui/components/cell/MultiSelect.vue

@ -33,8 +33,6 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -59,6 +57,8 @@ const { $api } = useNuxtApp()
const { getMeta } = useMetas()
const { isPg, isMysql } = useProject()
// a variable to keep newly created options value
// temporary until it's add the option to column meta
const tempSelectedOptsState = reactive<string[]>([])
@ -171,8 +171,19 @@ useSelectedCellKeyupListener(active, (e) => {
isOpen.value = true
}
break
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft':
case 'Delete':
// skip
break
default:
isOpen.value = true
// toggle only if char key pressed
if (e.key?.length === 1) {
e.stopPropagation()
isOpen.value = true
}
break
}
})
@ -195,9 +206,28 @@ async function addIfMissingAndSave() {
})
column.value.colOptions = { options: newOptions.map(({ value: _, ...rest }) => rest) }
await $api.dbTableColumn.update((column.value as { fk_column_id?: string })?.fk_column_id || (column.value?.id as string), {
...column.value,
})
const updatedColMeta = { ...column.value }
// todo: refactor and avoid repetition
if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg.value) {
updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`),
)
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql.value) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
await $api.dbTableColumn.update(
(column.value as { fk_column_id?: string })?.fk_column_id || (column.value?.id as string),
updatedColMeta,
)
activeOptCreateInProgress.value--
if (!activeOptCreateInProgress.value) {
@ -241,8 +271,8 @@ const onTagClick = (e: Event, onClose: Function) => {
class="w-full"
:bordered="false"
clear-icon
show-search
:show-arrow="!readOnly"
:show-search="active || editable"
:open="isOpen && (active || editable)"
:disabled="readOnly"
:class="{ '!ml-[-8px]': readOnly }"

36
packages/nc-gui/components/cell/SingleSelect.vue

@ -49,6 +49,8 @@ const searchVal = ref()
const { getMeta } = useMetas()
const { isPg, isMysql } = useProject()
// a variable to keep newly created option value
// temporary until it's add the option to column meta
const tempSelectedOptState = ref<string>()
@ -102,6 +104,13 @@ useSelectedCellKeyupListener(active, (e) => {
isOpen.value = true
}
break
default:
// toggle only if char key pressed
if (e.key?.length === 1) {
e.stopPropagation()
isOpen.value = true
}
break
}
})
@ -120,9 +129,28 @@ async function addIfMissingAndSave() {
})
column.value.colOptions = { options: options.value.map(({ value: _, ...rest }) => rest) }
await $api.dbTableColumn.update((column.value as { fk_column_id?: string })?.fk_column_id || (column.value?.id as string), {
...column.value,
})
const updatedColMeta = { ...column.value }
// todo: refactor and avoid repetition
if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg.value) {
updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`),
)
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql.value) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
await $api.dbTableColumn.update(
(column.value as { fk_column_id?: string })?.fk_column_id || (column.value?.id as string),
updatedColMeta,
)
vModel.value = newOptValue
await getMeta(column.value.fk_model_id!, true)
} catch (e) {
@ -161,7 +189,7 @@ const toggleMenu = (e: Event) => {
:disabled="readOnly"
:show-arrow="!readOnly && (active || editable || vModel === null)"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`"
:show-search="active || editable"
show-search
@select="isOpen = false"
@keydown.stop
@search="search"

7
packages/nc-gui/components/smartsheet/Gallery.vue

@ -80,7 +80,12 @@ const isRowEmpty = (record: any, col: any) => {
const attachments = (record: any): Attachment[] => {
try {
return coverImageColumn?.title && record.row[coverImageColumn.title] ? JSON.parse(record.row[coverImageColumn.title]) : []
if (coverImageColumn?.title && record.row[coverImageColumn.title]) {
return typeof record.row[coverImageColumn.title] === 'string'
? JSON.parse(record.row[coverImageColumn.title])
: record.row[coverImageColumn.title]
}
return []
} catch (e) {
return []
}

7
packages/nc-gui/components/smartsheet/Kanban.vue

@ -119,7 +119,12 @@ reloadViewDataHook?.on(async () => {
const attachments = (record: any): Attachment[] => {
try {
return coverImageColumn?.title && record.row[coverImageColumn.title] ? JSON.parse(record.row[coverImageColumn.title]) : []
if (coverImageColumn?.title && record.row[coverImageColumn.title]) {
return typeof record.row[coverImageColumn.title] === 'string'
? JSON.parse(record.row[coverImageColumn.title])
: record.row[coverImageColumn.title]
}
return []
} catch (e) {
return []
}

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

@ -174,13 +174,12 @@ watch(inputs, () => {
/>
</div>
</template>
<template #footer>
<div v-if="validateInfos?.['colOptions.options']?.help?.[0]?.[0]" class="text-error text-[10px] my-2">
{{ validateInfos['colOptions.options'].help[0][0] }}
</div>
</template>
</Draggable>
</div>
<div v-if="validateInfos?.['colOptions.options']?.help?.[0]?.[0]" class="text-error text-[10px] mb-1 mt-2">
{{ validateInfos['colOptions.options'].help[0][0] }}
</div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()">
<div class="flex items-center">
<MdiPlus />

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

@ -122,11 +122,13 @@ if (isKanban.value) {
}
}
const cellWrapperEl = (wrapperEl: HTMLElement) => {
nextTick(() => {
;(wrapperEl?.querySelector('input,select,textarea') as HTMLInputElement)?.focus()
const cellWrapperEl = ref<HTMLElement>()
onMounted(() => {
setTimeout(() => {
;(cellWrapperEl.value?.querySelector('input,select,textarea') as HTMLInputElement)?.focus()
})
}
})
</script>
<script lang="ts">
@ -163,7 +165,10 @@ export default {
<LazySmartsheetHeaderCell v-else :column="col" />
<div :ref="i ? null : cellWrapperEl" class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2">
<div
:ref="i ? null : (el) => (cellWrapperEl = el)"
class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2"
>
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<LazySmartsheetCell

3
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -20,7 +20,6 @@ import {
isJSON,
isPercent,
isPhoneNumber,
isPrimary,
isRating,
isSet,
isSingleSelect,
@ -58,7 +57,7 @@ import SpecificDBTypeIcon from '~icons/mdi/database-settings'
import DurationIcon from '~icons/mdi/timer-outline'
const renderIcon = (column: ColumnType, abstractType: any) => {
if (isPrimary(column)) {
if (isPrimaryKey(column)) {
return KeyIcon
} else if (isJSON(column)) {
return JSONIcon

27
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -1,9 +1,22 @@
<script lang="ts" setup>
import { ReloadViewDataHookInj, computed, inject, onClickOutside, ref, useSmartsheetStoreOrThrow } from '#imports'
import {
ActiveViewInj,
ReloadViewDataHookInj,
computed,
inject,
onClickOutside,
ref,
useFieldQuery,
useSmartsheetStoreOrThrow,
} from '#imports'
const reloadData = inject(ReloadViewDataHookInj)!
const { search, meta } = useSmartsheetStoreOrThrow()
const { meta } = useSmartsheetStoreOrThrow()
const activeView = inject(ActiveViewInj, ref())
const { search, loadFieldQuery } = useFieldQuery(activeView)
const isDropdownOpen = ref(false)
@ -18,6 +31,16 @@ const columns = computed(() =>
})),
)
watch(
() => activeView.value?.id,
(n, o) => {
if (n !== o) {
loadFieldQuery(activeView)
}
},
{ immediate: true },
)
function onPressEnter() {
reloadData.trigger()
}

9
packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue

@ -150,11 +150,16 @@ const emailField = (inputEl: typeof Input) => {
wrap-class-name="nc-modal-invite-user-and-share-base"
@cancel="emit('closed')"
>
<div class="flex flex-col">
<div class="flex flex-col" data-testid="invite-user-and-share-base-modal">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<a-button
type="text"
class="!rounded-md mr-1 -mt-1.5"
data-testid="invite-user-and-share-base-modal-close-btn"
@click="emit('closed')"
>
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>

21
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -27,14 +27,6 @@ const meta = inject(MetaInj, ref())
const cellValue = inject(CellValueInj, ref())
const arrValue = computed(() => {
if (!cellValue.value) return []
if (Array.isArray(cellValue.value)) return cellValue.value
return [cellValue.value]
})
const relationColumn = computed(
() =>
meta.value?.columns?.find((c) => c.id === (column.value?.colOptions as LookupType)?.fk_relation_column_id) as
@ -66,6 +58,19 @@ const lookupColumn = computed(
| undefined,
)
const arrValue = computed(() => {
if (!cellValue.value) return []
// if lookup column is Attachment and relation type is Belongs to wrap the value in an array
// since the attachment component expects an array or JSON string array
if (lookupColumn.value?.uidt === UITypes.Attachment && relationColumn.value?.colOptions?.type === RelationTypes.BELONGS_TO)
return [cellValue.value]
if (Array.isArray(cellValue.value)) return cellValue.value
return [cellValue.value]
})
provide(MetaInj, lookupTableMeta)
provide(CellUrlDisableOverlayInj, ref(true))

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

@ -195,9 +195,15 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
try {
if (!(await validate())) return
} catch (e) {
console.log(e)
console.trace()
message.error(t('msg.error.formValidationFailed'))
const errorMsgs = e.errorFields
?.map((e: any) => e.errors?.join(', '))
.filter(Boolean)
.join(', ')
if (errorMsgs) {
message.error(errorMsgs)
} else {
message.error(t('msg.error.formValidationFailed'))
}
return
}

34
packages/nc-gui/composables/useFieldQuery.ts

@ -0,0 +1,34 @@
import type { Ref } from 'vue'
import type { ViewType } from 'nocodb-sdk'
import { useState } from '#imports'
export function useFieldQuery(view: Ref<ViewType | undefined>) {
// initial search object
const emptyFieldQueryObj = {
field: '',
query: '',
}
// mapping view id (key) to corresponding emptyFieldQueryObj (value)
const searchMap = useState<Record<string, { field: string; query: string }>>('field-query-search-map', () => ({}))
// the fieldQueryObj under the current view
const search = useState<{ field: string; query: string }>('field-query-search', () => emptyFieldQueryObj)
// map current view id to emptyFieldQueryObj
if (view?.value?.id) {
searchMap.value[view!.value!.id] = search.value
}
// retrieve the fieldQueryObj of the given view id
// if it is not found in `searchMap`, init with emptyFieldQueryObj
const loadFieldQuery = (view: Ref<ViewType | undefined>) => {
if (!view.value?.id) return
if (!(view!.value!.id in searchMap.value)) {
searchMap.value[view!.value!.id!] = emptyFieldQueryObj
}
search.value = searchMap.value[view!.value!.id!]
}
return { search, loadFieldQuery }
}

19
packages/nc-gui/composables/useSmartsheetStore.ts

@ -1,7 +1,7 @@
import { ViewTypes } from 'nocodb-sdk'
import type { FilterType, KanbanType, SortType, TableType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { computed, reactive, ref, unref, useInjectionState, useNuxtApp, useProject } from '#imports'
import { computed, ref, unref, useFieldQuery, useInjectionState, useNuxtApp, useProject } from '#imports'
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
(
@ -17,12 +17,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const cellRefs = ref<HTMLTableDataCellElement[]>([])
// state
// todo: move to grid view store
const search = reactive({
field: '',
query: '',
})
const { search } = useFieldQuery(view)
// getters
const isLocked = computed(() => view.value?.lock_type === 'locked')
@ -36,21 +31,20 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const xWhere = computed(() => {
let where
const col =
(meta.value as TableType)?.columns?.find(({ id }) => id === search.field) ||
(meta.value as TableType)?.columns?.find(({ id }) => id === search.value.field) ||
(meta.value as TableType)?.columns?.find((v) => v.pv)
if (!col) return
if (!search.query.trim()) return
if (!search.value.query.trim()) return
if (['text', 'string'].includes(sqlUi.value.getAbstractType(col)) && col.dt !== 'bigint') {
where = `(${col.title},like,%${search.query.trim()}%)`
where = `(${col.title},like,%${search.value.query.trim()}%)`
} else {
where = `(${col.title},eq,${search.query.trim()})`
where = `(${col.title},eq,${search.value.query.trim()})`
}
return where
})
const isSqlView = computed(() => (meta.value as TableType)?.type === 'view')
const sorts = ref<SortType[]>(unref(initialSorts) ?? [])
const nestedFilters = ref<FilterType[]>(unref(initialFilters) ?? [])
@ -59,7 +53,6 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
meta,
isLocked,
$api,
search,
xWhere,
isPkAvail,
isForm,

2
packages/nc-gui/lang/ar.json

@ -368,6 +368,8 @@
"setPrimary": "تعيين كقيمة أساسية",
"addRow": "إضافة صف جديد",
"saveRow": "حفظ الصف",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "إدراج صف جديد",
"deleteRow": "حذف الصف",
"deleteSelectedRow": "حذف الصفوف المحددة",

3
packages/nc-gui/lang/bn_IN.json

@ -368,6 +368,8 @@
"setPrimary": "পথমিক মন হিট করন",
"addRow": "নতন সিত করন",
"saveRow": "সিরকষণ করন",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "নতন সিন",
"deleteRow": "সিন",
"deleteSelectedRow": "নিিত সিিন",
@ -601,7 +603,6 @@
"tableDeleted": "Deleted table successfully",
"generatePublicShareableReadonlyBase": "Generate publicly shareable readonly base",
"deleteViewConfirmation": "Are you sure you want to delete this view?",
"deleteTokenConfirmation": "Are you sure you want to delete this token?",
"deleteTableConfirmation": "Do you want to delete the table",
"showM2mTables": "Show M2M Tables",
"deleteKanbanStackConfirmation": "Deleting this stack will also remove the select option `{stackToBeDeleted}` from the `{groupingField}`. The records will move to the uncategorized stack."

2
packages/nc-gui/lang/da.json

@ -368,6 +368,8 @@
"setPrimary": "Indstil som primær værdi",
"addRow": "Tilføj ny række",
"saveRow": "Gem ro",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Indsæt ny række",
"deleteRow": "DELETE ROW.",
"deleteSelectedRow": "Slet de valgte rækker",

2
packages/nc-gui/lang/de.json

@ -369,6 +369,8 @@
"setPrimary": "Als Primärwert festlegen",
"addRow": "Neue Zeile hinzufügen",
"saveRow": "Zeile speichern",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Neue Zeile einfügen",
"deleteRow": "Zeile löschen",
"deleteSelectedRow": "Ausgewählte Zeilen löschen",

2
packages/nc-gui/lang/es.json

@ -368,6 +368,8 @@
"setPrimary": "Establecido como clave primaria",
"addRow": "Añadir nueva fila",
"saveRow": "Grabar la fila",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insertar nueva fila",
"deleteRow": "Borrar fila",
"deleteSelectedRow": "Eliminar filas seleccionadas",

2
packages/nc-gui/lang/fa.json

@ -368,6 +368,8 @@
"setPrimary": "تنظیم به عنوان مقدار اولیه",
"addRow": "اضافه کردن ردیف جدید",
"saveRow": "دخیره ردیف",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "وارد کردن ردیف جدید",
"deleteRow": "حذف ردیف جدید",
"deleteSelectedRow": "حذف ردیفهای انتخاب شده",

2
packages/nc-gui/lang/fi.json

@ -368,6 +368,8 @@
"setPrimary": "Aseta ensisijainen arvo",
"addRow": "Lisää uusi rivi",
"saveRow": "Tallenna rivi",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Lisää uusi rivi",
"deleteRow": "Poista rivi",
"deleteSelectedRow": "Poista valitut rivit",

2
packages/nc-gui/lang/fr.json

@ -368,6 +368,8 @@
"setPrimary": "Définir comme valeur primaire",
"addRow": "Ajouter une nouvelle ligne",
"saveRow": "Enregistrer la ligne",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insérer une nouvelle ligne",
"deleteRow": "Supprimer la ligne",
"deleteSelectedRow": "Supprimer les lignes sélectionnées",

2
packages/nc-gui/lang/he.json

@ -368,6 +368,8 @@
"setPrimary": "להגדיר כערך ראשי",
"addRow": "הוסף שורה חדשה",
"saveRow": "שמור שורה",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "הכנס שורה חדשה",
"deleteRow": "מחק שורה",
"deleteSelectedRow": "מחק את השורות שנבחרו",

2
packages/nc-gui/lang/hi.json

@ -368,6 +368,8 @@
"setPrimary": "पथमिक मय कप मट कर",
"addRow": "नई पि",
"saveRow": "पि सह",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "नई पि",
"deleteRow": "पि हट",
"deleteSelectedRow": "चयनित पि हट",

2
packages/nc-gui/lang/hr.json

@ -368,6 +368,8 @@
"setPrimary": "Postavite kao primarnu vrijednost",
"addRow": "Dodaj novi red",
"saveRow": "Spremanje retka",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Umetnite novi red",
"deleteRow": "Brisanje retka",
"deleteSelectedRow": "Izbrišite odabrane retke",

2
packages/nc-gui/lang/id.json

@ -368,6 +368,8 @@
"setPrimary": "Tetapkan sebagai nilai utama",
"addRow": "Tambahkan baris baru",
"saveRow": "Hemat Baris",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Masukkan baris baru.",
"deleteRow": "Hapus Baris",
"deleteSelectedRow": "Hapus baris yang dipilih",

2
packages/nc-gui/lang/it.json

@ -368,6 +368,8 @@
"setPrimary": "Impostare come valore primario",
"addRow": "Aggiungi nuova riga",
"saveRow": "Salva riga",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Inserisci nuova riga",
"deleteRow": "Elimina riga.",
"deleteSelectedRow": "Elimina righe selezionate",

2
packages/nc-gui/lang/ja.json

@ -368,6 +368,8 @@
"setPrimary": "プライマリ値として設定",
"addRow": "行を追加",
"saveRow": "行を保存",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "行を挿入",
"deleteRow": "行を削除",
"deleteSelectedRow": "選択行を削除",

2
packages/nc-gui/lang/ko.json

@ -368,6 +368,8 @@
"setPrimary": "Primary value로 설정",
"addRow": "행 추가",
"saveRow": "행 저장",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "행 삽입",
"deleteRow": "행 삭제",
"deleteSelectedRow": "선택한 행 삭제",

2
packages/nc-gui/lang/lv.json

@ -368,6 +368,8 @@
"setPrimary": "Uzstādīt kā primāro atslēgu",
"addRow": "Pievienot ierakstu",
"saveRow": "Saglabāt ierakstu",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Pievienot jaunu ierakstu",
"deleteRow": "Dzēst ierakstu",
"deleteSelectedRow": "Dzēst izvēlētos ierakstus",

2
packages/nc-gui/lang/nl.json

@ -368,6 +368,8 @@
"setPrimary": "Instellen als primaire waarde",
"addRow": "Nieuwe rij toevoegen",
"saveRow": "Sla rij op",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Voeg nieuwe rij toe",
"deleteRow": "Verwijder rij",
"deleteSelectedRow": "Verwijder geselecteerde rijen",

2
packages/nc-gui/lang/no.json

@ -368,6 +368,8 @@
"setPrimary": "Sett som primærverdi",
"addRow": "Legg til ny rad",
"saveRow": "Lagre rad",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Sett inn ny rad",
"deleteRow": "Slett rad",
"deleteSelectedRow": "Slett utvalgte rader",

2
packages/nc-gui/lang/pl.json

@ -368,6 +368,8 @@
"setPrimary": "Ustaw jako wartość podstawowa",
"addRow": "Dodaj nowy rząd",
"saveRow": "Zapisz wiersz",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Wstaw nowy rząd",
"deleteRow": "Usuń rząd",
"deleteSelectedRow": "Usuń wybrane wiersze",

2
packages/nc-gui/lang/pt.json

@ -368,6 +368,8 @@
"setPrimary": "Definido como valor primário",
"addRow": "Adicionar nova linha",
"saveRow": "Salvar linha",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insira a nova linha",
"deleteRow": "Excluir linha",
"deleteSelectedRow": "Excluir linhas selecionadas",

2
packages/nc-gui/lang/pt_BR.json

@ -368,6 +368,8 @@
"setPrimary": "Definido como valor primário",
"addRow": "Adicionar nova linha",
"saveRow": "Salvar linha",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insira a nova linha",
"deleteRow": "Excluir linha",
"deleteSelectedRow": "Excluir linhas selecionadas",

2
packages/nc-gui/lang/ru.json

@ -368,6 +368,8 @@
"setPrimary": "Установить в качестве основного значения",
"addRow": "Добавить новую строку",
"saveRow": "Сохранить строку",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Вставить новый строк",
"deleteRow": "Удалить строку",
"deleteSelectedRow": "Удалить выбранные строки",

2
packages/nc-gui/lang/sl.json

@ -368,6 +368,8 @@
"setPrimary": "Kot primarna vrednost",
"addRow": "Dodaj novo vrstico",
"saveRow": "Shrani vrstico",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Vstavite novo vrstico",
"deleteRow": "Izbriši vrstico",
"deleteSelectedRow": "Izbrišite izbrane vrstice",

2
packages/nc-gui/lang/sv.json

@ -368,6 +368,8 @@
"setPrimary": "Ange som primärt värde",
"addRow": "Lägg till ny rad",
"saveRow": "Spara rad",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Infoga ny rad",
"deleteRow": "Radera raden",
"deleteSelectedRow": "Ta bort valda rader",

2
packages/nc-gui/lang/th.json

@ -368,6 +368,8 @@
"setPrimary": "ตงคาเปนคาปฐมภ",
"addRow": "เพมแถวใหม",
"saveRow": "บนทกแถว",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "แทรกแถวใหม",
"deleteRow": "ลบแถว",
"deleteSelectedRow": "ลบแถวทเลอก",

2
packages/nc-gui/lang/tr.json

@ -368,6 +368,8 @@
"setPrimary": "Birincil değer yap",
"addRow": "Yeni satır ekle",
"saveRow": "Satırı kaydet",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Yeni Satır Ekle",
"deleteRow": "Satırı Sil",
"deleteSelectedRow": "Seçilen Satırları Sil",

74
packages/nc-gui/lang/uk.json

@ -16,7 +16,7 @@
"cancel": "Скасувати",
"submit": "Подавати",
"create": "Створювати",
"duplicate": "Duplicate",
"duplicate": "Дублювати",
"insert": "Вставляти",
"delete": "Видаляти",
"update": "Оновлення",
@ -56,19 +56,19 @@
"notification": "Сповіщення",
"reference": "Довідник",
"function": "Функція",
"confirm": "Confirm",
"generate": "Generate",
"copy": "Copy",
"misc": "Miscellaneous",
"lock": "Lock",
"unlock": "Unlock",
"credentials": "Credentials",
"help": "Help",
"questions": "Questions",
"confirm": "Підтвердити",
"generate": "Генерувати",
"copy": "Копіювати",
"misc": "Інше",
"lock": "Блокувати",
"unlock": "Розблокувати",
"credentials": "Дані доступу",
"help": "Довідка",
"questions": "Питання",
"reachOut": "Reach out here",
"betaNote": "This feature is currently in beta.",
"moreInfo": "More information can be found here",
"logs": "Logs",
"betaNote": "Ця функція знаходиться в стадії бета-версії.",
"moreInfo": "Тут можна знайти більше інформації",
"logs": "Логи",
"groupingField": "Grouping Field"
},
"objects": {
@ -84,8 +84,8 @@
"pages": "Сторінка",
"record": "Рекорд",
"records": "Записи",
"webhook": "Webhook",
"webhooks": "Webhooks",
"webhook": "Вебхук",
"webhooks": "Вебхуки",
"view": "Погляд",
"views": "Вигляд",
"viewType": {
@ -190,15 +190,15 @@
"headLogin": "Вхід | Нокодб",
"resetPassword": "Скинути пароль",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable",
"generateToken": "Generate Token",
"APIsAndSupport": "APIs & Support",
"helpCenter": "Help center",
"swaggerDocumentation": "Swagger Documentation",
"quickImportFrom": "Quick Import From",
"quickImport": "Quick Import",
"advancedSettings": "Advanced Settings",
"apiDocs": "Документи API",
"importFromAirtable": "Імпортувати з Airtable",
"generateToken": "Генерувати токен",
"APIsAndSupport": "API та Підтримка",
"helpCenter": "Довідковий центр",
"swaggerDocumentation": "Документація Swagger",
"quickImportFrom": "Швидкий імпорт з",
"quickImport": "Швидкий імпорт",
"advancedSettings": "Додаткові налаштування",
"codeSnippet": "Code Snippet"
},
"labels": {
@ -220,7 +220,7 @@
"port": "Номер порту",
"username": "Ім'я користувача",
"password": "Пароль",
"schemaName": "Schema name",
"schemaName": "Назва схеми",
"database": "База даних",
"action": "Дія",
"actions": "Акції",
@ -268,25 +268,25 @@
"childColumn": "Дитяча колонка",
"onUpdate": "На оновлення",
"onDelete": "На видалі",
"account": "Account",
"language": "Language",
"primaryColor": "Primary Color",
"accentColor": "Accent Color",
"customTheme": "Custom Theme",
"account": "Обліковий запис",
"language": "Мова",
"primaryColor": "Основний колір",
"accentColor": "Додатковий колір",
"customTheme": "Користувацька тема",
"requestDataSource": "Request a data source you need?",
"apiKey": "API Key",
"apiKey": "API ключ",
"sharedBase": "Shared Base",
"importData": "Import Data",
"importData": "Імпорт даних",
"importSecondaryViews": "Import Secondary Views",
"importRollupColumns": "Import Rollup Columns",
"importLookupColumns": "Import Lookup Columns",
"importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns",
"noData": "No Data",
"noData": "Немає даних",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",
"importing": "Імпортування",
"flattenNested": "Flatten Nested",
"downloadAllowed": "Download allowed",
"downloadAllowed": "Завантаження дозволене",
"weAreHiring": "We are Hiring!",
"primaryKey": "Primary key",
"hasMany": "has many",
@ -368,6 +368,8 @@
"setPrimary": "Встановлено як первинне значення",
"addRow": "Додати новий рядок",
"saveRow": "Рятувати рядок",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Вставте новий рядок",
"deleteRow": "Видалити рядок",
"deleteSelectedRow": "Видалити вибрані рядки",
@ -497,7 +499,7 @@
"excelURL": "Введіть URL-адресу Excel",
"csvURL": "Введіть URL-адресу CSV",
"footMsg": "# рядків, щоб розібрати для виведення даних",
"excelImport": "Лист (и) доступні для імпорту",
"excelImport": "лист(и) доступні для імпорту",
"exportMetadata": "Ви хочете експортувати метадані з мета-таблиць?",
"importMetadata": "Ви хочете імпортувати метадані з мета-таблиць?",
"clearMetadata": "Ви хочете очистити метадані з мета-таблиць?",

76
packages/nc-gui/lang/vi.json

@ -16,7 +16,7 @@
"cancel": "Hủy bỏ",
"submit": "Nộp",
"create": "Tạo ra",
"duplicate": "Duplicate",
"duplicate": "Tạo bản sao",
"insert": "Chèn",
"delete": "Xóa bỏ",
"update": "Cập nhật",
@ -56,20 +56,20 @@
"notification": "Thông báo",
"reference": "Thẩm quyền giải quyết",
"function": "Chức năng",
"confirm": "Confirm",
"confirm": "Xác nhận",
"generate": "Generate",
"copy": "Copy",
"misc": "Miscellaneous",
"lock": "Lock",
"unlock": "Unlock",
"credentials": "Credentials",
"help": "Help",
"questions": "Questions",
"reachOut": "Reach out here",
"betaNote": "This feature is currently in beta.",
"copy": "Sao Chép",
"misc": "Các tùy chọn khác",
"lock": "Khoá",
"unlock": "Mở khoá",
"credentials": "Thông tin đăng nhập",
"help": "Trợ giúp",
"questions": "Câu hỏi",
"reachOut": "Tiếp cận sau",
"betaNote": "Tính năng này hiện không được hỗ trợ.",
"moreInfo": "More information can be found here",
"logs": "Logs",
"groupingField": "Grouping Field"
"logs": "Nhật ký",
"groupingField": "Nhóm theo trường"
},
"objects": {
"project": "Dự định",
@ -189,20 +189,20 @@
"headCreateProject": "Tạo dự án |. NOCODB.",
"headLogin": "Đăng nhập |. NOCODB.",
"resetPassword": "Đặt lại mật khẩu của bạn",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable",
"generateToken": "Generate Token",
"APIsAndSupport": "APIs & Support",
"teamAndSettings": "Cài đặt nhóm",
"apiDocs": "Văn bản API",
"importFromAirtable": "Nhập khẩu từ CSDL Airtable",
"generateToken": "Tạo mã Token",
"APIsAndSupport": "APIS và Hỗ trợ",
"helpCenter": "Help center",
"swaggerDocumentation": "Swagger Documentation",
"quickImportFrom": "Quick Import From",
"swaggerDocumentation": "Tài liệu Swagger",
"quickImportFrom": "Kết nhập nhanh dữ liệu từ",
"quickImport": "Quick Import",
"advancedSettings": "Advanced Settings",
"codeSnippet": "Code Snippet"
"advancedSettings": "Cài đặt Nâng cao",
"codeSnippet": "Thư viện mã"
},
"labels": {
"createdBy": "Created By",
"createdBy": "Đã tạo bởi",
"notifyVia": "Thông báo qua",
"projName": "Tên dự án",
"tableName": "Tên bảng.",
@ -220,7 +220,7 @@
"port": "Cổng số",
"username": "tên tài khoản",
"password": "Mật khẩu",
"schemaName": "Schema name",
"schemaName": "Tên lược đồ",
"database": "Cơ sở dữ liệu",
"action": "Hoạt động",
"actions": "Hành động",
@ -233,7 +233,7 @@
"where": "Ở đâu",
"cache": "Cache.",
"chat": "Trò chuyện",
"email": "E-mail",
"email": "Thư điện tử",
"storage": "Kho",
"uiAcl": "UI-ACL",
"models": "Mô hình",
@ -258,7 +258,7 @@
"bookDemo": "Đặt một bản demo miễn phí",
"getAnswered": "Nhận câu hỏi của bạn được trả lời",
"joinDiscord": "Tham gia thông minh",
"joinCommunity": "Join NocoDB Community",
"joinCommunity": "Tham gia cộng đồng",
"joinReddit": "Tham gia /r/NocoDB",
"followNocodb": "Theo dõi NocoDB"
},
@ -268,21 +268,21 @@
"childColumn": "Cột trẻ con.",
"onUpdate": "Trên bản cập nhật",
"onDelete": "Trên xóa",
"account": "Account",
"account": "Tài khoản",
"language": "Language",
"primaryColor": "Primary Color",
"accentColor": "Accent Color",
"customTheme": "Custom Theme",
"primaryColor": "Màu chính",
"accentColor": "Màu phụ",
"customTheme": "Giao diện tùy chỉnh",
"requestDataSource": "Request a data source you need?",
"apiKey": "API Key",
"sharedBase": "Shared Base",
"importData": "Import Data",
"importSecondaryViews": "Import Secondary Views",
"importData": "Kết nhập dữ liệu",
"importSecondaryViews": "Kết nhập Hiển thị thứ hai",
"importRollupColumns": "Import Rollup Columns",
"importLookupColumns": "Import Lookup Columns",
"importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns",
"noData": "No Data",
"importAttachmentColumns": "Kết nhập cột tệp đính kèm",
"importFormulaColumns": "Kết nhập cột công thức",
"noData": "Không có dữ liệu",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",
"flattenNested": "Flatten Nested",
@ -368,6 +368,8 @@
"setPrimary": "Đặt dưới dạng giá trị chính",
"addRow": "Thêm hàng mới",
"saveRow": "Lưu hàng.",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Chèn hàng mới",
"deleteRow": "Xóa hàng",
"deleteSelectedRow": "Xóa các hàng đã chọn",
@ -424,13 +426,13 @@
"erd": {
"showColumns": "Show Columns",
"showPkAndFk": "Show Primary and Foreign Keys",
"showSqlViews": "Show SQL Views",
"showSqlViews": "Hiển thị truy vấn SQL",
"showMMTables": "Show Many to Many tables",
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"deleteStack": "Delete Stack",
"deleteStack": "Xóa ngăn xếp",
"stackedBy": "Stacked By",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"

2
packages/nc-gui/lang/zh-Hans.json

@ -368,6 +368,8 @@
"setPrimary": "设置为主要值",
"addRow": "添加新行",
"saveRow": "保存行",
"saveAndExit": "保存并退出",
"saveAndStay": "Save & Stay",
"insertRow": "插入新行",
"deleteRow": "删除行",
"deleteSelectedRow": "删除所选行",

298
packages/nc-gui/lang/zh-Hant.json

@ -6,7 +6,7 @@
"close": "關閉",
"yes": "是",
"no": "否",
"ok": "OK",
"ok": "確認",
"and": "和",
"or": "或",
"add": "新增",
@ -59,23 +59,23 @@
"confirm": "確認",
"generate": "Generate",
"copy": "複製",
"misc": "其他",
"misc": "Miscellaneous",
"lock": "鎖定",
"unlock": "解鎖",
"credentials": "憑證",
"help": "幫助",
"questions": "問題",
"reachOut": "Reach out here",
"betaNote": "此功能還在測試中",
"moreInfo": "More information can be found here",
"logs": "Logs",
"betaNote": "此功能目前是 Beta 測試版",
"moreInfo": "更多資訊能在這裡找到",
"logs": "日誌",
"groupingField": "Grouping Field"
},
"objects": {
"project": "專案",
"projects": "全部專案",
"table": "資料表",
"tables": "全部資料表",
"project": "項目",
"projects": "項目",
"table": "表",
"tables": "表",
"field": "欄位",
"fields": "欄位",
"column": "列",
@ -90,7 +90,7 @@
"views": "所有檢視",
"viewType": {
"grid": "網格",
"gallery": "圖庫",
"gallery": "相簿",
"form": "表單",
"kanban": "看板",
"calendar": "日曆"
@ -100,19 +100,19 @@
"role": "角色",
"roles": "角色",
"roleType": {
"owner": "有者",
"owner": "有者",
"creator": "創造者",
"editor": "編輯",
"commenter": "評論者",
"viewer": "檢視者",
"orgLevelCreator": "組織級建立者",
"orgLevelViewer": "組織級檢視者"
"orgLevelCreator": "組織級建立者",
"orgLevelViewer": "組織級檢視者"
},
"sqlVIew": "SQL View"
},
"datatype": {
"ID": "ID",
"ForeignKey": "外",
"ForeignKey": "外鑰匙",
"SingleLineText": "單行文本",
"LongText": "長篇文章",
"Attachment": "附件",
@ -134,8 +134,8 @@
"Rating": "評分",
"Formula": "公式",
"Rollup": "捲起",
"Count": "數",
"Lookup": "查找",
"Count": "數",
"Lookup": "抬頭",
"DateTime": "日期時間",
"CreateTime": "創建時間",
"LastModifiedTime": "最後修改時間",
@ -192,11 +192,11 @@
"teamAndSettings": "團隊 & 設定",
"apiDocs": "API 說明文件",
"importFromAirtable": "從 Airtable 匯入",
"generateToken": "Generate Token",
"APIsAndSupport": "APIs 與支援",
"generateToken": "產生 Token",
"APIsAndSupport": "APIs & Support",
"helpCenter": "幫助中心",
"swaggerDocumentation": "Swagger 文件",
"quickImportFrom": "Quick Import From",
"quickImportFrom": "快速匯入從",
"quickImport": "快速匯入",
"advancedSettings": "進階設定",
"codeSnippet": "程式碼片段"
@ -204,16 +204,16 @@
"labels": {
"createdBy": "Created By",
"notifyVia": "透過...通知",
"projName": "專案名",
"tableName": "資料表名稱",
"viewName": "查看名稱",
"projName": "項目名",
"tableName": "表名稱",
"viewName": "檢視名稱",
"viewLink": "查看鏈接",
"columnName": "欄位名稱",
"columnType": "欄位類型",
"columnName": "名稱",
"columnType": "類型",
"roleName": "角色名稱",
"roleDescription": "角色描述",
"databaseType": "數據庫類別",
"lengthValue": "長度 / 值",
"databaseType": "鍵入數據庫",
"lengthValue": "長度/值",
"dbType": "資料庫類型",
"sqliteFile": "SQLite 檔案",
"hostAddress": "主機位址",
@ -221,7 +221,7 @@
"username": "使用者名稱",
"password": "密碼",
"schemaName": "Schema 名稱",
"database": "資料庫",
"database": "數據庫",
"action": "行動",
"actions": "行動",
"operation": "操作",
@ -231,7 +231,7 @@
"authentication": "驗證",
"token": "權杖",
"where": "在哪裡",
"cache": "快取",
"cache": "緩存",
"chat": "聊天",
"email": "電子郵件",
"storage": "貯存",
@ -249,13 +249,13 @@
"requriedCa": "必填 - CA",
"requriedIdentity": "必填 - IDENTITY",
"inflection": {
"tableName": "屈折 - 資料表名稱",
"tableName": "屈折 - 表名稱",
"columnName": "屈折 - 欄位名稱"
},
"community": {
"starUs1": "在 Github 上",
"starUs2": "幫我們按讚",
"bookDemo": "預免費 Demo",
"bookDemo": "預免費 Demo",
"getAnswered": "解惑您的問題",
"joinDiscord": "加入 Discord",
"joinCommunity": "加入 NocoDB 社群",
@ -265,10 +265,10 @@
"docReference": "文件參考文獻",
"selectUserRole": "選擇使用者角色",
"childTable": "子表格",
"childColumn": "子欄",
"childColumn": "子欄",
"onUpdate": "更新",
"onDelete": "在刪除",
"account": "帳戶",
"account": "Account",
"language": "語言",
"primaryColor": "Primary Color",
"accentColor": "Accent Color",
@ -277,13 +277,13 @@
"apiKey": "API Key",
"sharedBase": "Shared Base",
"importData": "匯入資料",
"importSecondaryViews": "匯入 Secondary Views",
"importRollupColumns": "匯入 Rollup 欄位",
"importLookupColumns": "匯入 Lookup 欄位",
"importAttachmentColumns": "匯入 Attachment 欄位",
"importSecondaryViews": "Import Secondary Views",
"importRollupColumns": "Import Rollup Columns",
"importLookupColumns": "Import Lookup Columns",
"importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns",
"noData": "目前沒有資料",
"goToDashboard": "Go to Dashboard",
"noData": "沒有資料",
"goToDashboard": "前往儀表板",
"importing": "匯入中",
"flattenNested": "Flatten Nested",
"downloadAllowed": "允許下載",
@ -291,16 +291,16 @@
"primaryKey": "主鍵",
"hasMany": "has many",
"belongsTo": "belongs to",
"manyToMany": "have many to many relation",
"manyToMany": "有多對多關聯",
"extraConnectionParameters": "Extra connection parameters",
"commentsOnly": "Comments only",
"documentation": "文件",
"documentation": "Documentation",
"subscribeNewsletter": "Subscribe to our weekly newsletter",
"signUpWithGoogle": "使用 Google 帳號註冊",
"signInWithGoogle": "使用 Google 帳號登入",
"signUpWithGoogle": "Sign up with Google",
"signInWithGoogle": "Sign in with Google",
"agreeToTos": "By signing up, you agree to the Terms of Service",
"welcomeToNc": "Welcome to NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url"
"inviteOnlySignup": "只接受使用邀請連結進行註冊"
},
"activity": {
"createProject": "建立專案",
@ -313,7 +313,7 @@
"deleteProject": "刪除專案",
"refreshProject": "重新整理專案",
"saveProject": "儲存專案",
"deleteKanbanStack": "刪除 stack?",
"deleteKanbanStack": "Delete stack?",
"createProjectExtended": {
"extDB": "連線至外部資料庫來建立",
"excel": "從 Excel 建立專案",
@ -332,10 +332,10 @@
"projInfo": "複製專案資訊",
"themes": "主題"
},
"sort": "排序",
"addSort": "加排序選項",
"sort": "種類",
"addSort": "加排序選項",
"filter": "篩選",
"addFilter": "加過濾器",
"addFilter": "加過濾器",
"share": "分享",
"shareBase": {
"disable": "禁用共享基礎",
@ -352,7 +352,7 @@
"deleteUser": "從專案中刪除使用者",
"resendInvite": "重新發送邀請電子郵件",
"copyInviteURL": "複製邀請連結",
"copyPasswordResetURL": "Copy password reset URL",
"copyPasswordResetURL": "複製重設密碼連結",
"newRole": "新角色",
"reloadRoles": "重新載入角色",
"nextPage": "下一頁",
@ -360,14 +360,16 @@
"nextRecord": "下一步記錄",
"previousRecord": "之前的紀錄",
"copyApiURL": "複製 API 網址",
"createTable": "建立資料表",
"createTable": "表創造",
"refreshTable": "表刷新",
"renameTable": "重命名資料表",
"deleteTable": "刪除資料表",
"addField": "將新欄位增加到此資料表",
"renameTable": "重命名",
"deleteTable": "刪除",
"addField": "將新字段添加到此表",
"setPrimary": "設置為主要值",
"addRow": "新增行",
"saveRow": "儲存行",
"saveAndExit": "儲存並結束",
"saveAndStay": "Save & Stay",
"insertRow": "插入新行",
"deleteRow": "刪除行",
"deleteSelectedRow": "刪除所選行",
@ -389,12 +391,12 @@
"copyView": "複製檢視",
"renameView": "重新命名檢視",
"deleteView": "刪除檢視",
"createGrid": "創建網格視",
"createGallery": "創建畫廊視圖",
"createCalendar": "創建日曆視",
"createKanban": "創建尋呼視圖",
"createForm": "創建表單視",
"showSystemFields": "顯示系統欄位",
"createGrid": "創建網格視",
"createGallery": "創建相簿檢視",
"createCalendar": "創建日曆視",
"createKanban": "創建看板檢視",
"createForm": "創建表單視",
"showSystemFields": "顯示系統字段",
"copyUrl": "複製網址",
"openTab": "開啟新分頁",
"iFrame": "複製嵌入式 HTML 程式碼",
@ -412,9 +414,9 @@
"sponsorUs": "贊助我們",
"sendEmail": "傳送電子郵件",
"addUserToProject": "Add user to project",
"getApiSnippet": "Get API Snippet",
"clearCell": "Clear cell",
"addFilterGroup": "Add Filter Group",
"getApiSnippet": "取得 API 程式碼片段",
"clearCell": "清除儲存格",
"addFilterGroup": "增加過濾組",
"linkRecord": "Link record",
"addNewRecord": "Add new record",
"useConnectionUrl": "Use Connection URL",
@ -425,7 +427,7 @@
"showColumns": "顯示欄位",
"showPkAndFk": "顯示主鍵與外鍵",
"showSqlViews": "Show SQL Views",
"showMMTables": "Show Many to Many tables",
"showMMTables": "顯示多對多資料表",
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
@ -446,13 +448,13 @@
"dark": "它確實有黑色(^⇧b)",
"light": "它是黑色嗎?(^⇧b)"
},
"addTable": "建立資料表",
"inviteMore": "邀請更多使用者",
"addTable": "添加新表",
"inviteMore": "邀請更多用戶",
"toggleNavDraw": "切換導航抽屜",
"reloadApiToken": "重新載入 API 權杖",
"generateNewApiToken": "產生新 API 權杖",
"addRole": "添加新角色",
"reloadList": "重新載列表",
"reloadList": "重新載列表",
"metaSync": "同步中繼資料",
"sqlMigration": "重新加載遷移",
"updateRestart": "更新並重新啟動",
@ -468,15 +470,15 @@
"projName": "輸入專案名稱",
"password": {
"enter": "輸入密碼",
"current": "前密碼",
"current": "前密碼",
"new": "新密碼",
"save": "儲存密碼",
"confirm": "確認新密碼"
},
"searchProjectTree": "搜索專案樹",
"searchFields": "搜索欄位",
"searchColumn": "搜索 {search} 列",
"searchApps": "搜索應用程",
"searchProjectTree": "搜索",
"searchFields": "搜索字段",
"searchColumn": "搜索{search}列",
"searchApps": "搜索應用程",
"searchModels": "搜索模型",
"noItemsFound": "未找到任何項目",
"defaultValue": "預設值",
@ -487,13 +489,13 @@
"msg": {
"info": {
"roles": {
"orgCreator": "建立者可以建立專案與存取任何受邀請的專案.",
"orgViewer": "檢視者不可建立新專案但可以存取任何受邀請的專案."
"orgCreator": "建立者可以建立專案與存取任何受邀請的專案",
"orgViewer": "檢視者不能建立專案但可以存取任何受邀請的專案"
},
"footerInfo": "每頁行駛",
"upload": "選擇檔案以上傳",
"upload_sub": "或拖放檔案",
"excelSupport": "支:.xls,.xlsx,.xlsm,.ods,.ots",
"excelSupport": "支:.xls,.xlsx,.xlsm,.ods,.ots",
"excelURL": "輸入 Excel 檔案 URL",
"csvURL": "輸入 CSV 檔案 URL",
"footMsg": "要解析為推斷數據類型的行數",
@ -506,34 +508,34 @@
"startProject": "你想啟動這個專案嗎?",
"restartProject": "你想重新啟動專案嗎?",
"deleteProject": "你想刪除這個專案嗎?",
"shareBasePrivate": "產生公開享的 Readonly Base",
"shareBasePrivate": "產生公開享的 Readonly Base",
"shareBasePublic": "網路上的任何人都可以查看",
"userInviteNoSMTP": "看起來你還沒有配置郵件!請複上面的邀請鏈接並將其發送給",
"dragDropHide": "在此處拖放欄位以隱藏",
"userInviteNoSMTP": "看起來你還沒有配置郵件!請複上面的邀請鏈接並將其發送給",
"dragDropHide": "在此處拖放字段以隱藏",
"formInput": "輸入表單輸入標籤",
"formHelpText": "添加一些幫助文本",
"onlyCreator": "僅建立者可見",
"formDesc": "添加表單描述",
"beforeEnablePwd": "使用密碼限制存取權限",
"afterEnablePwd": "存取受密碼限制",
"privateLink": "此檢視通過私人連結共享",
"privateLinkAdditionalInfo": "具有私有連結的人只能看到此檢視中可見的儲存格",
"privateLink": "此檢視通過私人連結共享",
"privateLinkAdditionalInfo": "具有私有連結的人只能看到此檢視中可見的儲存格",
"afterFormSubmitted": "表格提交後",
"apiOptions": "存取專案方式",
"submitAnotherForm": "顯示 '提交另一個表格' 按鈕",
"submitAnotherForm": "顯示“提交另一個表格”按鈕",
"showBlankForm": "5 秒後顯示空白表格",
"emailForm": "發電子郵件給我",
"showSysFields": "顯示系統欄位",
"showSysFields": "顯示系統字段",
"filterAutoApply": "自動申請",
"showMessage": "顯示此消息",
"viewNotShared": "當前視不共享!",
"viewNotShared": "當前視不共享!",
"showAllViews": "顯示此表的所有共享視圖",
"collabView": "具有編輯權限或更高的合作者可以更改視圖配置。",
"lockedView": "沒有人可以編輯視圖配置,直到它被解鎖。",
"personalView": "只有您可以編輯視圖配置。默認情況下,其他合作者的個人視圖隱藏。",
"ownerDesc": "可以添加/刪除創建者。和完整編輯資料庫結構和欄位。",
"creatorDesc": "可以完全編輯資料庫結構和值。",
"editorDesc": "可以編輯記錄但無法更改資料庫/欄位的結構。",
"ownerDesc": "可以添加/刪除創建者。和完整編輯數據庫結構和字段。",
"creatorDesc": "可以完全編輯數據庫結構和值。",
"editorDesc": "可以編輯記錄但無法更改數據庫/字段的結構。",
"commenterDesc": "可以查看和評論記錄,但無法編輯任何內容",
"viewerDesc": "可以查看記錄但無法編輯任何內容",
"addUser": "新增使用者",
@ -551,7 +553,7 @@
},
"sponsor": {
"header": "你可以幫助我們!",
"message": "我們是一個小型團隊,全職打造 NocoDB 並且開源程式碼。我們相信像 NocoDB 這樣的工具應該在網際網路上自由提供給每位問題解決者。"
"message": "我們是一支小型團隊,全職工作,使Nocodb開放來源。我們相信一個像Nocodb這樣的工具應該在互聯網上的每個問題求解器上自由提供。"
},
"loginMsg": "登入 NocoDB",
"passwordRecovery": {
@ -568,16 +570,16 @@
"dontHaveAccount": "沒有帳號?"
},
"addView": {
"grid": "加入網格檢視",
"gallery": "加入圖庫檢視表",
"form": "加入表單檢視",
"kanban": "加入看板檢視",
"calendar": "加入日曆檢視"
"grid": "加入網格檢視",
"gallery": "加入相簿檢視",
"form": "加入表單檢視",
"kanban": "加入看板檢視",
"calendar": "加入日曆檢視"
},
"tablesMetadataInSync": "表元數據同步",
"addMultipleUsers": "您可以添加多個逗號(,)分隔的電子郵件",
"enterTableName": "輸入表名",
"addDefaultColumns": "建立預設欄位",
"addDefaultColumns": "添加默認列",
"tableNameInDb": "數據庫中保存的表名",
"airtable": {
"credentials": "Where to find this?"
@ -591,18 +593,18 @@
"copiedToClipboard": "複製到剪貼簿",
"requriedFieldsCantBeMoved": "Required field can't be moved",
"updateNotAllowedWithoutPK": "Update not allowed for table which doesn't have primary key",
"autoIncFieldNotEditable": "Auto increment field is not editable",
"editingPKnotSupported": "Editing primary key not supported",
"deletedCache": "Deleted cache successfully",
"autoIncFieldNotEditable": "自增欄位不可編輯",
"editingPKnotSupported": "不支援編輯主鍵",
"deletedCache": "刪除快取成功",
"cacheEmpty": "快取是空的",
"exportedCache": "Exported Cache Successfully",
"valueAlreadyInList": "This value is already in the list",
"valueAlreadyInList": "此值已在列表中",
"noColumnsToUpdate": "No columns to update",
"tableDeleted": "Deleted table successfully",
"tableDeleted": "刪除資料表成功",
"generatePublicShareableReadonlyBase": "Generate publicly shareable readonly base",
"deleteViewConfirmation": "Are you sure you want to delete this view?",
"deleteTableConfirmation": "Do you want to delete the table",
"showM2mTables": "顯示 M2M 資料表",
"deleteViewConfirmation": "是否確定要刪除此檢視?",
"deleteTableConfirmation": "你想刪除此資料表",
"showM2mTables": "顯示多對多資料表",
"deleteKanbanStackConfirmation": "Deleting this stack will also remove the select option `{stackToBeDeleted}` from the `{groupingField}`. The records will move to the uncategorized stack."
},
"error": {
@ -619,51 +621,51 @@
"passwdRequired": "密碼為必填",
"passwdLength": "您的密碼應至少有 8 個字元",
"passwdMismatch": "密碼不匹配",
"completeRuleSet": "At least 8 characters with one Uppercase, one number and one special character",
"atLeast8Char": "At least 8 characters",
"atLeastOneUppercase": "One Uppercase letter",
"atLeastOneNumber": "One Number",
"atLeastOneSpecialChar": "One special character",
"allowedSpecialCharList": "Allowed special character list"
"completeRuleSet": "密碼必須含有至少 8 個字元,其中有一個大寫字母、一個數字和一個特殊字元",
"atLeast8Char": "至少 8 個字元",
"atLeastOneUppercase": "一個大寫字母",
"atLeastOneNumber": "一個數字",
"atLeastOneSpecialChar": "一個特殊字元",
"allowedSpecialCharList": "允許特殊字元列表"
},
"invalidURL": "無效的 URL",
"internalError": "Some internal error occurred",
"invalidURL": "無效的連結",
"internalError": "發生內部錯誤",
"templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "檔案上傳失敗",
"fileUploadFailed": "上傳文件失敗",
"primaryColumnUpdateFailed": "Failed to update primary column",
"formDescriptionTooLong": "Data too long for Form Description",
"formDescriptionTooLong": "表單描述資料過長",
"columnsRequired": "Following columns are required",
"selectAtleastOneColumn": "At least one column has to be selected",
"selectAtleastOneColumn": "至少必須選擇一個欄位",
"columnDescriptionNotFound": "Cannot find the destination column for",
"duplicateMappingFound": "Duplicate mapping found, please remove one of the mapping",
"nullValueViolatesNotNull": "Null value violates not-null constraint",
"sourceHasInvalidNumbers": "Source data contains some invalid numbers",
"sourceHasInvalidBoolean": "Source data contains some invalid boolean values",
"invalidForm": "無效的表",
"nullValueViolatesNotNull": "Null 值違反不可為 Null 限制條件",
"sourceHasInvalidNumbers": "來源資料包含無效的數字",
"sourceHasInvalidBoolean": "來源資料包含無效的布林值",
"invalidForm": "無效的表",
"formValidationFailed": "Form validation failed",
"youHaveBeenSignedOut": "You have been signed out",
"youHaveBeenSignedOut": "您已登出",
"failedToLoadList": "Failed to load list",
"failedToLoadChildrenList": "Failed to load children list",
"deleteFailed": "Delete failed",
"deleteFailed": "刪除失敗",
"unlinkFailed": "Unlink failed",
"rowUpdateFailed": "Row update failed",
"deleteRowFailed": "Failed to delete row",
"rowUpdateFailed": "資料更新失敗",
"deleteRowFailed": "刪除資料失敗",
"setFormDataFailed": "Failed to set form data",
"formViewUpdateFailed": "Failed to update form view",
"tableNameRequired": "Table name is required",
"nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _",
"tableNameRequired": "資料表名稱必填",
"nameShouldStartWithAnAlphabetOr_": "名稱必須用 英文字母 或 _ 當開頭",
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"projectNameExceeds50Characters": "Project name exceeds 50 characters",
"projectNameCannotStartWithSpace": "Project name cannot start with space",
"requiredField": "Required field",
"columnNameRequired": "欄位名稱必填",
"projectNameExceeds50Characters": "專案名稱超過 50 個字元",
"projectNameCannotStartWithSpace": "專案名稱不能有空白開頭",
"requiredField": "必填欄位",
"ipNotAllowed": "IP not allowed",
"targetFileIsNotAnAcceptedFileType": "Target file is not an accepted file type",
"theAcceptedFileTypeIsCsv": "The accepted file type is .csv",
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key 不可為空",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} 不可為空.",
"fieldRequired": "{value} cannot be empty.",
"projectNotAccessible": "Project not accessible"
},
"toast": {
@ -677,7 +679,7 @@
"authToken": "驗證權杖已複製到剪貼簿",
"projInfo": "已將專案資訊複製到剪貼簿",
"inviteUrlCopy": "已將邀請連結複製到剪貼簿",
"createView": "成功建立檢視",
"createView": "成功建立檢視",
"formEmailSMTP": "請啟用 App Store 中的 SMTP 外掛程式以啟用電子郵件通知",
"collabView": "成功轉換為協作視圖",
"lockedView": "成功轉換為鎖定視圖",
@ -685,37 +687,37 @@
},
"success": {
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",
"pluginTested": "Successfully tested plugin settings",
"pluginUninstalled": "外掛移除安裝成功",
"pluginSettingsSaved": "外掛設定儲存成功",
"pluginTested": "外掛設定測試成功",
"tableRenamed": "資料表重新命名成功",
"viewDeleted": "View deleted successfully",
"viewDeleted": "檢視刪除成功",
"primaryColumnUpdated": "Successfully updated as primary column",
"tableDataExported": "Successfully exported all table data",
"updated": "Successfully updated",
"updated": "成功更新",
"sharedViewDeleted": "Deleted shared view successfully",
"userDeleted": "User deleted successfully",
"viewRenamed": "View renamed successfully",
"tokenGenerated": "Token generated successfully",
"tokenDeleted": "Token deleted successfully",
"userAddedToProject": "成功增加使用者到專案",
"userAdded": "成功增加使用者",
"userDeletedFromProject": "Successfully deleted user from project",
"inviteEmailSent": "Invite Email sent successfully",
"inviteURLCopied": "Invite URL 複製到剪貼簿",
"passwordResetURLCopied": "Password reset URL copied to 剪貼簿",
"shareableURLCopied": "Copied shareable base URL to 剪貼簿!",
"userDeleted": "使用者已成功删除",
"viewRenamed": "檢視重新命名成功",
"tokenGenerated": "Token 產生成功",
"tokenDeleted": "Token 刪除成功",
"userAddedToProject": "專案增加使用者成功",
"userAdded": "增加使用者成功",
"userDeletedFromProject": "專案移除使用者成功",
"inviteEmailSent": "邀請郵件發送成功",
"inviteURLCopied": "邀請連結已複製到剪貼簿",
"passwordResetURLCopied": "密碼重置連結已複製到剪貼簿",
"shareableURLCopied": "Copied shareable base URL to clipboard!",
"embeddableHTMLCodeCopied": "Copied embeddable HTML code!",
"userDetailsUpdated": "Successfully updated the user details",
"tableDataImported": "Successfully imported table data",
"userDetailsUpdated": "成功更新使用者資料",
"tableDataImported": "成功匯入資料表資料",
"webhookUpdated": "Webhook details updated successfully",
"webhookDeleted": "Hook deleted successfully",
"webhookTested": "Webhook tested successfully",
"columnUpdated": "欄位已更新",
"columnCreated": "欄位已建立",
"passwordChanged": "密碼變更成功. 請重新登入.",
"settingsSaved": "設定儲存成功",
"roleUpdated": "角色更新成功"
"passwordChanged": "密碼已更新,請重新登入。",
"settingsSaved": "設定已成功儲存",
"roleUpdated": "角色已成功更新"
}
}
}

2
packages/nc-gui/layouts/default.vue

@ -20,7 +20,7 @@ export default {
<template>
<div class="w-full h-full">
<Teleport :to="hasSidebar ? '#nc-sidebar-left' : null" :disabled="!hasSidebar">
<slot name="sidebar" />
<slot :key="$route.name" name="sidebar" />
</Teleport>
<a-layout-content>

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

@ -90,7 +90,7 @@
}
},
"../nocodb-sdk": {
"version": "0.98.4",
"version": "0.99.2",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

1
packages/nc-gui/pages/account/index.vue

@ -68,6 +68,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('appStore')"
key="apps"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
@click="navigateTo('/account/apps')"

5
packages/nc-gui/pages/account/index/users.vue

@ -1,5 +0,0 @@
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<NuxtPage />
</div>
</template>

30
packages/nc-gui/pages/account/index/users/[[nestedPage]].vue

@ -5,18 +5,20 @@ const { isUIAllowed } = useUIPermission()
</script>
<template>
<template
v-if="
$route.params.nestedPage === 'password-reset' ||
(!isUIAllowed('superAdminUserManagement') && !isUIAllowed('superAdminAppSettings'))
"
>
<LazyAccountResetPassword />
</template>
<template v-else-if="$route.params.nestedPage === 'settings'">
<LazyAccountSignupSettings />
</template>
<template v-else-if="isUIAllowed('superAdminUserManagement')">
<LazyAccountUserList />
</template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<template
v-if="
$route.params.nestedPage === 'password-reset' ||
(!isUIAllowed('superAdminUserManagement') && !isUIAllowed('superAdminAppSettings'))
"
>
<LazyAccountResetPassword />
</template>
<template v-else-if="$route.params.nestedPage === 'settings'">
<LazyAccountSignupSettings />
</template>
<template v-else-if="isUIAllowed('superAdminUserManagement')">
<LazyAccountUserList />
</template>
</div>
</template>

22
packages/nc-gui/pages/index/index/[projectId].vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import type { VNodeRef } from '@vue/runtime-core'
import {
extractSdkResponseErrorMsg,
message,
@ -8,14 +9,13 @@ import {
projectTitleValidator,
reactive,
ref,
tryOnMounted,
useProject,
useRoute,
} from '#imports'
const route = useRoute()
const { project, loadProject, updateProject, isLoading, projectLoadedHook } = useProject()
const { loadProject, updateProject, isLoading } = useProject()
loadProject(false)
@ -43,21 +43,7 @@ const renameProject = async () => {
}
}
// select and focus title field on load
projectLoadedHook(async () => {
formState.title = project.value.title as string
tryOnMounted(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.focus()
input.setSelectionRange(0, formState.title?.length)
}, 150)
})
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>
@ -89,7 +75,7 @@ projectLoadedHook(async () => {
@finish="renameProject"
>
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
<a-input :ref="focus" v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<div class="text-center">

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

@ -1,11 +1,10 @@
<script lang="ts" setup>
import type { Form } from 'ant-design-vue'
import type { VNodeRef } from '@vue/runtime-core'
import {
extractSdkResponseErrorMsg,
message,
navigateTo,
nextTick,
onMounted,
projectTitleValidator,
reactive,
ref,
@ -47,19 +46,7 @@ const createProject = async () => {
}
}
// select and focus title field on load
onMounted(async () => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>
@ -88,7 +75,7 @@ onMounted(async () => {
@finish="createProject"
>
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules" class="m-10">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
<a-input :ref="focus" v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<div class="text-center">

156
packages/nc-gui/pages/index/index/index.vue

@ -212,92 +212,94 @@ const copyProjectMeta = async () => {
</a-dropdown>
</div>
<Transition name="layout" mode="out-in">
<div v-if="isLoading">
<a-skeleton />
</div>
<a-table
v-else
:custom-row="customRow"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
:table-layout="md ? 'auto' : 'fixed'"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!--
TODO: bring back transition after fixing the bug with navigation
<Transition name="layout" mode="out-in"> -->
<div v-if="isLoading">
<a-skeleton />
</div>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text, record }">
<div class="flex items-center">
<div @click.stop>
<a-menu class="!border-0 !m-0 !p-0" trigger-sub-menu-action="click">
<template v-if="isUIAllowed('projectTheme')">
<a-sub-menu key="theme" popup-class-name="custom-color">
<template #title>
<div
class="color-selector"
:style="{
'background-color': getProjectPrimary(record),
'width': '8px',
'height': '100%',
}"
/>
</template>
<a-table
v-else
:custom-row="customRow"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
:table-layout="md ? 'auto' : 'fixed'"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text, record }">
<div class="flex items-center">
<div @click.stop>
<a-menu class="!border-0 !m-0 !p-0" trigger-sub-menu-action="click">
<template v-if="isUIAllowed('projectTheme')">
<a-sub-menu key="theme" popup-class-name="custom-color">
<template #title>
<div
class="color-selector"
:style="{
'background-color': getProjectPrimary(record),
'width': '8px',
'height': '100%',
}"
/>
</template>
<template #expandIcon></template>
<template #expandIcon></template>
<LazyGeneralColorPicker
:model-value="getProjectPrimary(record)"
:colors="projectThemeColors"
:row-size="9"
:advanced="false"
@input="handleProjectColor(record.id, $event)"
/>
<LazyGeneralColorPicker
:model-value="getProjectPrimary(record)"
:colors="projectThemeColors"
:row-size="9"
:advanced="false"
@input="handleProjectColor(record.id, $event)"
/>
<a-sub-menu key="pick-primary">
<template #title>
<div class="nc-project-menu-item group !py-0">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Custom Color
</div>
</template>
<a-sub-menu key="pick-primary">
<template #title>
<div class="nc-project-menu-item group !py-0">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Custom Color
</div>
</template>
<template #expandIcon></template>
<template #expandIcon></template>
<LazyGeneralChromeWrapper @input="handleProjectColor(record.id, $event)" />
</a-sub-menu>
<LazyGeneralChromeWrapper @input="handleProjectColor(record.id, $event)" />
</a-sub-menu>
</template>
</a-menu>
</div>
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
>
{{ text }}
</div>
</a-sub-menu>
</template>
</a-menu>
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<MdiEditOutline v-e="['c:project:edit:rename']" class="nc-action-btn" @click.stop="navigateTo(`/${text}`)" />
<MdiDeleteOutline
class="nc-action-btn"
:data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
>
{{ text }}
</div>
</template>
</a-table-column>
</a-table>
</Transition>
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<MdiEditOutline v-e="['c:project:edit:rename']" class="nc-action-btn" @click.stop="navigateTo(`/${text}`)" />
<MdiDeleteOutline
class="nc-action-btn"
:data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
</div>
</template>
</a-table-column>
</a-table>
<!-- </Transition> -->
</div>
</template>

5
packages/nc-gui/pages/index/index/user.vue

@ -68,7 +68,10 @@ const resetError = () => {
</script>
<template>
<div class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)">
<div
class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"
data-testid="user-change-password"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" />
<div

2
packages/nc-gui/windi.config.ts

@ -20,7 +20,7 @@ export default defineConfig({
},
darkMode: 'class',
safelist: ['text-yellow-500', 'text-sky-500', 'text-red-500'],
plugins: [
scrollbar,
animations,

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.98.4",
"version": "0.99.2",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

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

@ -8353,9 +8353,9 @@
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
@ -18648,9 +18648,9 @@
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"

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

@ -0,0 +1,224 @@
---
title: "Playwright E2E Testing"
description: "Overview to playwright based e2e tests"
position: 3260
category: "Engineering"
menuTitle: "Playwright E2E Testing"
---
## How to run tests
All the tests reside in `tests/playwright` folder.
Make sure to install the dependencies(in the playwright folder):
```bash
npm install
npx playwright install chromium --with-deps
```
### Run Test Server
Start the backend test server (in `packages/nocodb` folder):
```bash
npm run watch:run:playwright
```
Start the frontend test server (in `packages/nc-gui` folder):
```bash
NUXT_PAGE_TRANSITION_DISABLE=true npm run dev
```
### Running all tests
For selecting db type, rename `.env.example` to `.env` and set `E2E_DEV_DB_TYPE` to `sqlite`(default), `mysql` or `pg`.
headless mode(without opening browser):
```bash
npm run test
```
with browser:
```bash
npm run test:debug
```
</br>
</br>
For setting up mysql(sakila):
```bash
docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d
```
For setting up postgres(sakila):
```bash
docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml
```
### Running individual tests
Add `.only` to the test you want to run:
```js
test.only('should login', async ({ page }) => {
// ...
})
```
```bash
npm run test
```
## Concepts
### Independent tests
- All tests are independent of each other.
- Each test starts with a fresh project with a fresh sakila database(option to not use sakila db is also there).
- Each test creates a new user(email as `user@nocodb.com`) and logs in with that user to the dashboard.
Caveats:
- Some stuffs are shared i.e, users, plugins etc. So be catious while writing tests touching that. A fix for this is in the works.
- In test, we prefix email and project with the test id, which will be deleted after the test is done.
### What to test
- UI verification. This includes verifying the state of the UI element, i.e if the element is visible, if the element has a particular text etc.
- Test should verify all user flow. A test has a default timeout of 60 seconds. If a test is taking more than 60 seconds, it is a sign that the test should be broken down into smaller tests.
- Test should also verify all the side effects the feature(i.e. On adding a new column type, should verify column deletion as well) will have, and also error cases.
- Test name should be descriptive. It should be easy to understand what the test is doing by just reading the test name.
### Playwright
- Playwright is a nodejs library for automating chromium, firefox and webkit.
- For each test, a new browser context is created. This means that each test runs in a new incognito window.
- For assertion always use `expect` from `@playwright/test` library. This library provides a lot of useful assertions, which also has retry logic built in.
## Page Objects
- Page objects are used to abstract over the components/page. This makes the tests more readable and maintainable.
- All page objects are in `tests/playwright/pages` folder.
- All the test related code should be in page objects.
- Methods should be as thin as possible and its better to have multiple methods than one big method, which improves reusability.
The methods of a page object can be classified into 2 categories:
- Actions: Performs an UI actions like click, type, select etc. Is also responsible for waiting for the element to be ready and the action to be performed. This included waiting for API calls to complete.
- Assertions: Asserts the state of the UI element, i.e if the element is visible, if the element has a particular text etc. Use `expect` from `@playwright/test` and if not use `expect.poll` to wait for the assertion to pass.
## Writing a test
Let's write a test for testing filter functionality.
For simplicity, we will have `DashboardPage` implemented, which will have all the methods related to dashboard page and also its child components like Grid, etc.
### Create a test suite
Create a new file `filter.spec.ts` in `tests/playwright/tests` folder and use `setup` method to create a new project and user.
```js
import { test, expect } from '@playwright/test';
import setup, { NcContext } from '../setup';
test.describe('Filter', () => {
let context: NcContext;
test.beforeEach(async ({ page }) => {
context = await setup({ page });
})
test('should filter', async ({ page }) => {
// ...
});
});
```
### Create a page object
Since filter is UI wise scoped to a `Toolbar` , we will add filter page object to `ToolbarPage` page object.
```js
export class ToolbarPage extends BasePage {
readonly parent: GridPage | GalleryPage | FormPage | KanbanPage;
readonly filter: ToolbarFilterPage;
constructor(parent: GridPage | GalleryPage | FormPage | KanbanPage) {
super(parent.rootPage);
this.parent = parent;
this.filter = new ToolbarFilterPage(this);
}
}
```
We will create `ToolbarFilterPage` page object, which will have all the methods related to filter.
```js
export class ToolbarFilterPage extends BasePage {
readonly toolbar: ToolbarPage;
constructor(toolbar: ToolbarPage) {
super(toolbar.rootPage);
this.toolbar = toolbar;
}
}
```
Here `BasePage` is an abstract class, which used to enforce structure for all page objects. Thus all page object *should* inherit `BasePage`.
- Helper methods like `waitForResponse` and `getClipboardText` (this can be access on any page object, with `this.waitForResponse`)
- Provides structure for page objects, enforces all Page objects to have `rootPage` property, which is the page object created in the test setup.
- Enforces all pages to have a `get` method which will return the locator of the main container of that page, hence we can have focused dom selection, i.e.
```js
// This will only select the button inside the container of the concerned page
await this.get().querySelector('button').count();
```
### Writing an action method
This a method which will reset/clear all the filters. Since this is an action method, it will also wait for the `delete` filter API to return. Ignoring this API call will cause flakiness in the test, down the line.
```js
async resetFilter() {
await this.waitForResponse({
uiAction: this.get().locator('.nc-filter-item-remove-btn').click(),
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: '/api/v1/db/meta/filters/',
});
}
```
### Writing an assertion/verification method
Here we use `expect` from `@playwright/test` library, which has retry logic built in.
```js
import { expect } from '@playwright/test';
async verifyFilter({ title }: { title: string }) {
await expect(
this.get().locator(`[data-testid="nc-fields-menu-${title}"]`).locator('input[type="checkbox"]')
).toBeChecked();
}
```
## Tips to avoid flakiness
- If an UI action, causes an API call or the UI state change, then wait for that API call to complete or the UI state to change.
- What to wait out can be situation specific, but in general, is best to wait for the final state to be reached, i.e. in the case of creating filter, while it seems like waiting for the filter API to complete is enough, but after its return the table rows are reloaded and the UI state changes, so its better to wait for the table rows to be reloaded.
## Accessing playwright report in the CI
- Open `Summary` tab in the CI workflow in github actions.
- Scroll down to `Artifacts` section.
- Access reports which suffixed with the db type and shard number(corresponding to the CI workerflow name). i.e `playwright-report-mysql-2` is for `playwright-mysql-2` workflow.
- Download it and run `npm install -D @playwright/test && npx playwright show-report ./` inside the downloaded folder.

3
packages/noco-docs/content/en/engineering/translation.md

@ -53,12 +53,11 @@ Refer following articles to get additional details about Crowdin Portal usage
#### GitHub changes
- Update enumeration in `enums.ts` [packages/nc-gui/lib/enums.ts]
- Map JSON path in `a.i18n.ts` [packages/nc-gui/plugins/a.i18n.ts]
- Update array in `6d_language_validation.js` [scripts/cypress/integration/common/6d_language_validation.js]
#### Crowdin changes [admin only]
- Open `NocoDB` project
- Click on `Language` on the home tab
- Select target language, `Update`
- Update array in `tests/playwright/tests/language.spec.ts`
![Screenshot 2022-09-08 at 10 52 59 PM](https://user-images.githubusercontent.com/86527202/189186570-5c1c7cad-6d3f-4937-ab4d-fa7ebe022cb1.png)

161
packages/noco-docs/content/en/engineering/testing.md → packages/noco-docs/content/en/engineering/unit-testing.md

@ -1,9 +1,9 @@
---
title: "Writing Tests"
description: "Overview to testing"
title: "Writing Unit Tests"
description: "Overview to Unit Testing"
position: 3250
category: "Engineering"
menuTitle: "Writing Tests"
menuTitle: "Unit Testing"
---
## Unit Tests
@ -189,158 +189,3 @@ function tableTest() {
});
}
```
## Cypress Tests
### End-to-end (E2E) Tests
Cypress tests are divided into 4 suites
- SQLite tests
- Postgres tests
- MySQL tests
- Quick import tests
First 3 suites, each have 4 test category
- Table operations (create, delete, rename, add column, delete column, rename column)
- Views (Grid, Gallery, Form)
- Roles (user profiles, access control & preview)
- Miscellaneous (Import, i18n, etc)
### SQLite Tests (XCDB Project)
```shell
# install dependencies(cypress)
npm install
# start MySQL database using docker compose
docker-compose -f ./scripts/cypress/docker-compose-cypress.yml up
# Run backend api using following command
npm run start:xcdb-api:cache
# Run frontend web UI using following command
npm run start:web
# wait until both 3000 and 8080 ports are available
# or run following command to run it with GUI
npm run cypress:open
# run one of 4 test scripts
# - Table operations : xcdb-restTableOps.js
# - Views : xcdb-restViews.js
# - Roles & access control : xcdb-restRoles.js
# - Miscellaneous : xcdb-restMisc.js
```
### MySQL Tests (External DB Project)
```shell
# install dependencies(cypress)
npm install
# start MySQL database using docker compose
docker-compose -f ./scripts/cypress/docker-compose-cypress.yml up
# Run backend api using following command
npm run start:api:cache
# Run frontend web UI using following command
npm run start:web
# wait until both 3000 and 8080 ports are available
# or run following command to run it with GUI
npm run cypress:open
# run one of 4 test scripts
# - Table operations : restTableOps.js
# - Views : restViews.js
# - Roles & access control : restRoles.js
# - Miscellaneous : restMisc.js
```
### Postgres Tests (External DB Project)
```shell
# install dependencies(cypress)
npm install
# start Postgres database using docker compose
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
# Run backend api using following command
npm run start:api:cache
# Run frontend web UI using following command
npm run start:web
# wait until both 3000 and 8080 ports are available
# or run following command to run it with GUI
npm run cypress:open
# run one of 4 test scripts
# - Table operations : pg-restTableOps.js
# - Views : pg-restViews.js
# - Roles & access control : pg-restRoles.js
# - Miscellaneous : pg-restMisc.js
```
### Quick Import Tests (SQLite Project)
```shell
# install dependencies(cypress)
npm install
# start MySQL database using docker compose
docker-compose -f ./scripts/cypress/docker-compose-cypress.yml up
# copy existing xcdb (v0.91.7) database to ./packages/nocodb/
cp ./scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./packages/nocodb/noco.db
# Run backend api using following command
npm run start:api:cache
# Run frontend web UI using following command
npm run start:web
# wait until both 3000 and 8080 ports are available
# or run following command to run it with GUI
npm run cypress:open
# run test script
# - quickTest.js
```
### Quick import tests (Postgres)
```shell
# install dependencies(cypress)
npm install
# start PG database using docker compose
docker-compose -f ./scripts/cypress/docker-compose-pg.yml up -d
# copy existing xcdb (v0.91.7) database to ./packages/nocodb/
cp ./scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./packages/nocodb/noco.db
# Run backend api using following command
npm run start:api:cache
# Run frontend web UI using following command
npm run start:web
# wait until both 3000 and 8080 ports are available
# or run following command to run it with GUI
npm run cypress:open
# run test script
# - quickTest.js
```
## Accessing CI-CD CY Screenshots
1. On Jobs link, click on `Summary`
![Screenshot 2022-10-31 at 9 25 23 PM](https://user-images.githubusercontent.com/86527202/199052696-af0bf066-d82f-487a-b487-602f55594fd7.png)
2. Click on `Artifacts`
![Screenshot 2022-10-31 at 9 26 01 PM](https://user-images.githubusercontent.com/86527202/199052712-04508921-32b1-4926-8291-396c804f7c3b.png)
3. Download logs for desired suite
![Screenshot 2022-10-31 at 9 26 34 PM](https://user-images.githubusercontent.com/86527202/199052727-9aebbdd1-749e-4bda-ab00-3cdd0e3f48fe.png)

12
packages/noco-docs/package-lock.json generated

@ -10047,9 +10047,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -24670,9 +24670,9 @@
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}

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

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

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.98.4",
"version": "0.99.2",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

15
packages/nocodb/docker-compose.yml

@ -447,18 +447,3 @@ services:
cp /home/app/tests/sqlite-dump/sakila.db /home/sakila.db
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"#
cd /home/app/ && npm i && npm run test:graphql
xc-cypress-test:
image: node:12.22.1-slim
ports:
- 8080:8080
volumes:
- ./:/home/app
command:
- /bin/bash
- -c
- |
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
cd /home/app/ && npm i && npm run run

357
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.98.4",
"version": "0.99.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.98.4",
"version": "0.99.2",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -64,7 +64,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.79",
"nc-lib-gui": "0.98.4",
"nc-lib-gui": "0.99.2",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -119,7 +119,7 @@
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^8.1.1",
"mocha": "^10.1.0",
"nodemon": "^2.0.7",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
@ -152,7 +152,7 @@
}
},
"../nocodb-sdk": {
"version": "0.98.4",
"version": "0.99.2",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -1629,12 +1629,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/promise-all-settled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==",
"dev": true
},
"node_modules/@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@ -7369,15 +7363,6 @@
"graphql": ">=0.8.0"
}
},
"node_modules/growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true,
"engines": {
"node": ">=4.x"
}
},
"node_modules/gtoken": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz",
@ -8473,6 +8458,18 @@
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@ -9300,15 +9297,19 @@
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="
},
"node_modules/log-symbols": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
"integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"dev": true,
"dependencies": {
"chalk": "^4.0.0"
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/chalk": {
@ -9957,43 +9958,39 @@
}
},
"node_modules/mocha": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",
"integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz",
"integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==",
"dev": true,
"dependencies": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.1",
"debug": "4.3.1",
"chokidar": "3.5.3",
"debug": "4.3.4",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.1.6",
"growl": "1.10.5",
"glob": "7.2.0",
"he": "1.2.0",
"js-yaml": "4.0.0",
"log-symbols": "4.0.0",
"minimatch": "3.0.4",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "5.0.1",
"ms": "2.1.3",
"nanoid": "3.1.20",
"serialize-javascript": "5.0.1",
"nanoid": "3.3.3",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"which": "2.0.2",
"wide-align": "1.1.3",
"workerpool": "6.1.0",
"workerpool": "6.2.1",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
},
"bin": {
"_mocha": "bin/_mocha",
"mocha": "bin/mocha"
"mocha": "bin/mocha.js"
},
"engines": {
"node": ">= 10.12.0"
"node": ">= 14.0.0"
},
"funding": {
"type": "opencollective",
@ -10015,50 +10012,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/mocha/node_modules/chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.1"
}
},
"node_modules/mocha/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/mocha/node_modules/debug/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/mocha/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -10088,9 +10041,9 @@
}
},
"node_modules/mocha/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
@ -10107,10 +10060,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mocha/node_modules/glob/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/mocha/node_modules/js-yaml": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
"integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
@ -10135,21 +10100,30 @@
}
},
"node_modules/mocha/node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "*"
"node": ">=10"
}
},
"node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/mocha/node_modules/nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
@ -10173,16 +10147,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mocha/node_modules/readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"node_modules/mocha/node_modules/serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"randombytes": "^2.1.0"
}
},
"node_modules/mocha/node_modules/supports-color": {
@ -10632,9 +10603,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.98.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.98.4.tgz",
"integrity": "sha512-6E8NCOm8nRbJ+9WIE+5YOFas6OIINj95X0wVKVScMGSpPZ9xIHXLKXZYjIoqEFT2Q94wAV8vPPX2iZhvqXDeZw==",
"version": "0.99.2",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.99.2.tgz",
"integrity": "sha512-ghjsyPGHGMMx4zvHqX5mhJhW/DZuZd1pYh8+zm9eru6v/xx6QUQAfcpZrMCfy9MadIY8haiva1khGAZMnY6grQ==",
"dependencies": {
"express": "^4.17.1"
}
@ -17458,9 +17429,9 @@
}
},
"node_modules/workerpool": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz",
"integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
"dev": true
},
"node_modules/wrap-ansi": {
@ -18954,12 +18925,6 @@
"eslint-visitor-keys": "^2.0.0"
}
},
"@ungap/promise-all-settled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@ -23542,12 +23507,6 @@
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==",
"requires": {}
},
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true
},
"gtoken": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz",
@ -24325,6 +24284,12 @@
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
},
"is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"dev": true
},
"is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@ -24989,12 +24954,13 @@
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="
},
"log-symbols": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
"integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"dev": true,
"requires": {
"chalk": "^4.0.0"
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"dependencies": {
"chalk": {
@ -25534,33 +25500,29 @@
}
},
"mocha": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",
"integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz",
"integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==",
"dev": true,
"requires": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.1",
"debug": "4.3.1",
"chokidar": "3.5.3",
"debug": "4.3.4",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.1.6",
"growl": "1.10.5",
"glob": "7.2.0",
"he": "1.2.0",
"js-yaml": "4.0.0",
"log-symbols": "4.0.0",
"minimatch": "3.0.4",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "5.0.1",
"ms": "2.1.3",
"nanoid": "3.1.20",
"serialize-javascript": "5.0.1",
"nanoid": "3.3.3",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"which": "2.0.2",
"wide-align": "1.1.3",
"workerpool": "6.1.0",
"workerpool": "6.2.1",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
@ -25578,39 +25540,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
}
},
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
},
"dependencies": {
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -25628,9 +25557,9 @@
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@ -25639,12 +25568,23 @@
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"dependencies": {
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
}
}
},
"js-yaml": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
"integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": {
"argparse": "^2.0.1"
@ -25660,18 +25600,29 @@
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0"
}
}
}
},
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true
},
"p-locate": {
@ -25683,13 +25634,13 @@
"p-limit": "^3.0.2"
}
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
"randombytes": "^2.1.0"
}
},
"supports-color": {
@ -26064,9 +26015,9 @@
}
},
"nc-lib-gui": {
"version": "0.98.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.98.4.tgz",
"integrity": "sha512-6E8NCOm8nRbJ+9WIE+5YOFas6OIINj95X0wVKVScMGSpPZ9xIHXLKXZYjIoqEFT2Q94wAV8vPPX2iZhvqXDeZw==",
"version": "0.99.2",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.99.2.tgz",
"integrity": "sha512-ghjsyPGHGMMx4zvHqX5mhJhW/DZuZd1pYh8+zm9eru6v/xx6QUQAfcpZrMCfy9MadIY8haiva1khGAZMnY6grQ==",
"requires": {
"express": "^4.17.1"
}
@ -31434,9 +31385,9 @@
}
},
"workerpool": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz",
"integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
"dev": true
},
"wrap-ansi": {

13
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.98.4",
"version": "0.99.2",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -37,11 +37,8 @@
"watch:build": "nodemon -e ts,js -w ./src -x npm run build",
"watch:run": "cross-env NC_DISABLE_TELE1=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright": "rm -f ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright:quick": "rm -f ./test_noco.db; cp ../../scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright:pg:cyquick": "rm -f ./test_noco.db; cp ../../scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./test_noco.db; cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"watch:run:cypress": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:cypress:pg": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"watch:run:cypress:pg:cyquick": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"watch:run:playwright:quick": "rm -f ./test_noco.db; cp ../../tests/playwright/fixtures/noco_0_91_7.db ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright:pg:cyquick": "rm -f ./test_noco.db; cp ../../tests/playwright/fixtures/noco_0_91_7.db ./test_noco.db; cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"watch:run:mysql": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunMysql --log-error --project tsconfig.json\"",
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"run": "ts-node src/run/docker",
@ -107,7 +104,7 @@
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.79",
"nc-lib-gui": "0.98.4",
"nc-lib-gui": "0.99.2",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -162,7 +159,7 @@
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^8.1.1",
"mocha": "^10.1.0",
"nodemon": "^2.0.7",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",

2
packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts

@ -892,7 +892,7 @@ class KnexClient extends SqlClient {
getMinMax(_columnObject) {}
async mockDb(_args) {
// todo: remove method
// todo: remove method
}
async dbCacheInitAsyncKnex(_cbk = null) {

473
packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts

@ -183,7 +183,7 @@ class MssqlClient extends KnexClient {
log.api(`${_func}:args:`, args);
try {
await this.sqlClient.raw('SELECT 1+1 as data');
await this.sqlClient.raw('SELECT 1+1 AS data');
} catch (e) {
log.ppe(e);
result.code = -1;
@ -246,7 +246,7 @@ class MssqlClient extends KnexClient {
try {
const rows = await this.sqlClient.raw(
`SELECT SERVERPROPERTY('productversion') as version, SERVERPROPERTY ('productlevel') as level, SERVERPROPERTY ('edition') as edition, @@version as versionD`
`SELECT SERVERPROPERTY('productversion') AS version, SERVERPROPERTY ('productlevel') AS level, SERVERPROPERTY ('edition') AS edition, @@version AS versionD`
);
result.data.object = {};
@ -284,7 +284,7 @@ class MssqlClient extends KnexClient {
const tempSqlClient = knex(connectionParamsWithoutDb);
const rows = await tempSqlClient.raw(
`select name from sys.databases where name = '${args.database}'`
`SELECT name from sys.databases WHERE name = '${args.database}'`
);
if (rows.length === 0) {
@ -356,12 +356,14 @@ class MssqlClient extends KnexClient {
try {
/** ************** START : create _evolution table if not exists *************** */
const exists = await this.sqlClient.schema.withSchema(this.schema).hasTable(args.tn);
const exists = await this.sqlClient.schema
.withSchema(this.schema)
.hasTable(args.tn);
if (!exists) {
await this.sqlClient.schema.withSchema(this.schema).createTable(
args.tn,
function (table) {
await this.sqlClient.schema
.withSchema(this.schema)
.createTable(args.tn, function (table) {
table.increments();
table.string('title').notNullable();
table.string('titleDown').nullable();
@ -371,8 +373,7 @@ class MssqlClient extends KnexClient {
table.integer('status').nullable();
table.dateTime('created');
table.timestamps();
}
);
});
log.debug('Table created:', `${this.getTnPath(args.tn)}`);
} else {
log.debug(`${this.getTnPath(args.tn)} tables exists`);
@ -394,7 +395,9 @@ class MssqlClient extends KnexClient {
log.api(`${_func}:args:`, args);
try {
result.data.value = await this.sqlClient.schema.withSchema(this.schema).hasTable(args.tn);
result.data.value = await this.sqlClient.schema
.withSchema(this.schema)
.hasTable(args.tn);
} catch (e) {
log.ppe(e, _func);
throw e;
@ -412,7 +415,7 @@ class MssqlClient extends KnexClient {
try {
const rows = await this.sqlClient.raw(
`select name from sys.databases where name = '${args.databaseName}'`
`SELECT name FROM sys.databases WHERE name = '${args.databaseName}'`
);
result.data.value = rows.length > 0;
} catch (e) {
@ -438,7 +441,7 @@ class MssqlClient extends KnexClient {
try {
result.data.list = await this.sqlClient.raw(
`SELECT name as database_name, database_id, create_date from sys.databases order by name`
`SELECT name AS database_name, database_id, create_date FROM sys.databases ORDER BY name`
);
} catch (e) {
log.ppe(e, _func);
@ -463,8 +466,8 @@ class MssqlClient extends KnexClient {
try {
result.data.list = await this.sqlClient.raw(
`select schema_name(t.schema_id) as schema_name,
t.name as tn, t.create_date, t.modify_date from sys.tables t WHERE schema_name(t.schema_id) = ? order by schema_name,tn `,
`SELECT schema_name(t.schema_id) AS schema_name,
t.name AS tn, t.create_date, t.modify_date FROM sys.tables t WHERE schema_name(t.schema_id) = ? ORDER BY schema_name,tn `,
[this.schema || 'dbo']
);
} catch (e) {
@ -484,7 +487,7 @@ class MssqlClient extends KnexClient {
try {
result.data.list = await this.sqlClient.raw(
`SELECT name as schema_name FROM master.${this.schema}.sysdatabases where name not in ('master', 'tempdb', 'model', 'msdb');`
`SELECT name AS schema_name FROM master.${this.schema}.sysdatabases WHERE name not in ('master', 'tempdb', 'model', 'msdb');`
);
} catch (e) {
log.ppe(e, _func);
@ -532,53 +535,53 @@ class MssqlClient extends KnexClient {
try {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(`select
c.table_name as tn,
case WHEN trg1.trigger_name IS NULL THEN CAST(0 as BIT) ELSE CAST(1 as BIT) END as au,
c.column_name as cn,
c.ordinal_position as cop,
pk.constraint_type as ck,
case WHEN COLUMNPROPERTY(object_id(CONCAT('${this.schema}.', c.TABLE_NAME)), c.COLUMN_NAME, 'IsIdentity') = 1
const response = await this.sqlClient.raw(`SELECT
c.table_name AS tn,
CASE WHEN trg1.trigger_name IS NULL THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT) END AS au,
c.column_name AS cn,
c.ordinal_position AS cop,
pk.constraint_type AS ck,
CASE WHEN COLUMNPROPERTY(object_id(CONCAT('${this.schema}.', c.TABLE_NAME)), c.COLUMN_NAME, 'IsIdentity') = 1
THEN
1
ELSE
0
END as ai,
c.is_nullable as nrqd,
c.data_type as dt,
c.column_default as cdf,c.character_maximum_length as clen,
c.character_octet_length,c.numeric_precision as np,c.numeric_scale as ns,c.datetime_precision as dp,c.character_set_name as csn,
c.collation_name as clnn,
pk.constraint_type as cst, pk.ordinal_position as op, pk.constraint_name as pk_constraint_name,
fk.parent_table as rtn, fk.parent_column as rcn,
v.table_name as is_view,
END AS ai,
c.is_nullable AS nrqd,
c.data_type AS dt,
c.column_default AS cdf,c.character_maximum_length AS clen,
c.character_octet_length,c.numeric_precision AS np,c.numeric_scale AS ns,c.datetime_precision AS dp,c.character_set_name AS csn,
c.collation_name AS clnn,
pk.constraint_type AS cst, pk.ordinal_position AS op, pk.constraint_name AS pk_constraint_name,
fk.parent_table AS rtn, fk.parent_column AS rcn,
v.table_name AS is_view,
df.default_constraint_name
from information_schema.columns c
FROM INFORMATION_SCHEMA.COLUMNS c
left join
( select kc.constraint_name, kc.table_name,kc.column_name, kc.ordinal_position,tc.constraint_type
from information_schema.key_column_usage kc
inner join information_schema.table_constraints as tc
on kc.constraint_name = tc.constraint_name and tc.constraint_type in ('primary key')
where kc.table_catalog='${args.databaseName}' and kc.table_schema='${this.schema}'
( SELECT kc.constraint_name, kc.table_name,kc.column_name, kc.ordinal_position,tc.constraint_type
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kc
INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
ON kc.constraint_name = tc.constraint_name AND tc.constraint_type in ('primary key')
WHERE kc.table_catalog='${args.databaseName}' AND kc.table_schema='${this.schema}'
) pk
on
pk.table_name = c.table_name and pk.column_name=c.column_name
ON
pk.table_name = c.table_name AND pk.column_name=c.column_name
left join
( select
ccu.table_name as child_table
,ccu.column_name as child_column
,kcu.table_name as parent_table
,kcu.column_name as parent_column
( SELECT
ccu.table_name AS child_table
,ccu.column_name AS child_column
,kcu.table_name AS parent_table
,kcu.column_name AS parent_column
,ccu.constraint_name
from information_schema.constraint_column_usage ccu
inner join information_schema.referential_constraints rc
on ccu.constraint_name = rc.constraint_name
inner join information_schema.key_column_usage kcu
on kcu.constraint_name = rc.unique_constraint_name ) fk
on
fk.child_table = c.table_name and fk.child_column=c.column_name
left join information_schema.views v
on v.table_name=c.table_name
FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu
INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
ON ccu.constraint_name = rc.constraint_name
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
ON kcu.constraint_name = rc.unique_constraint_name ) fk
ON
fk.child_table = c.table_name AND fk.child_column=c.column_name
left join INFORMATION_SCHEMA.VIEWS v
ON v.table_name=c.table_name
left join (
SELECT
default_constraints.name default_constraint_name, all_columns.name name
@ -595,17 +598,17 @@ class MssqlClient extends KnexClient {
ON all_columns.default_object_id = default_constraints.object_id
WHERE
schemas.name = '${this.schema}'
AND tables.name = '${args.tn}') df on df.name = c.column_name
AND tables.name = '${args.tn}') df ON df.name = c.column_name
left join ( select trg.name as trigger_name,
tab.name as [table1]
from sys.triggers trg
left join ( SELECT trg.name AS trigger_name,
tab.name AS [table1]
FROM sys.triggers trg
left join sys.objects tab
on trg.parent_id = tab.object_id
where tab.name = '${args.tn}') trg1 on trg1.trigger_name = CONCAT('xc_trigger_${args.tn}_' , c.column_name)
ON trg.parent_id = tab.object_id
WHERE tab.name = '${args.tn}') trg1 ON trg1.trigger_name = CONCAT('xc_trigger_${args.tn}_' , c.column_name)
where c.table_catalog='${args.databaseName}' and c.table_schema='${this.schema}' and c.table_name = '${args.tn}'
order by c.table_name, c.ordinal_position`);
WHERE c.table_catalog='${args.databaseName}' AND c.table_schema='${this.schema}' AND c.table_name = '${args.tn}'
ORDER BY c.table_name, c.ordinal_position`);
for (let i = 0; i < response.length; i++) {
const el = response[i];
@ -657,39 +660,39 @@ class MssqlClient extends KnexClient {
log.api(`${_func}:args:`, args);
try {
const response = await this.sqlClient.raw(
`select t.[name] as table_view,
case when t.[type] = 'U' then 'Table'
when t.[type] = 'V' then 'View'
end as [object_type],
`SELECT t.[name] AS table_view,
CASE WHEN t.[type] = 'U' THEN 'Table'
WHEN t.[type] = 'V' THEN 'View'
END AS [object_type],
i.index_id,
case when i.is_primary_key = 1 then 'Primary key'
when i.is_unique = 1 then 'Unique'
else 'Not Unique' end as [type],
i.[name] as index_name,
substring(column_names, 1, len(column_names)-1) as [columns],
case when i.[type] = 1 then 'Clustered index'
when i.[type] = 2 then 'Nonclustered unique index'
when i.[type] = 3 then 'XML index'
when i.[type] = 4 then 'Spatial index'
when i.[type] = 5 then 'Clustered columnstore index'
when i.[type] = 6 then 'Nonclustered columnstore index'
when i.[type] = 7 then 'Nonclustered hash index'
end as index_type
from sys.objects t
inner join sys.indexes i
on t.object_id = i.object_id
cross apply (select col.[name] + ',' + CAST(ic.key_ordinal as varchar) + ','
from sys.index_columns ic
inner join sys.columns col
on ic.object_id = col.object_id
and ic.column_id = col.column_id
where ic.object_id = t.object_id
and ic.index_id = i.index_id
order by col.column_id
CASE WHEN i.is_primary_key = 1 THEN 'Primary key'
WHEN i.is_unique = 1 THEN 'Unique'
else 'Not Unique' END AS [type],
i.[name] AS index_name,
substring(column_names, 1, len(column_names)-1) AS [columns],
CASE WHEN i.[type] = 1 THEN 'Clustered index'
WHEN i.[type] = 2 THEN 'Nonclustered unique index'
WHEN i.[type] = 3 THEN 'XML index'
WHEN i.[type] = 4 THEN 'Spatial index'
WHEN i.[type] = 5 THEN 'Clustered columnstore index'
WHEN i.[type] = 6 THEN 'Nonclustered columnstore index'
WHEN i.[type] = 7 THEN 'Nonclustered hash index'
END AS index_type
FROM sys.objects t
INNER JOIN sys.indexes i
ON t.object_id = i.object_id
cross apply (SELECT col.[name] + ',' + CAST(ic.key_ordinal AS varchar) + ','
FROM sys.index_columns ic
INNER JOIN sys.columns col
ON ic.object_id = col.object_id
AND ic.column_id = col.column_id
WHERE ic.object_id = t.object_id
AND ic.index_id = i.index_id
ORDER BY col.column_id
for xml path ('') ) D (column_names)
where t.is_ms_shipped <> 1
and index_id > 0 and t.name = '${this.getTnPath(args.tn)}'
order by schema_name(t.schema_id) + '.' + t.[name], i.index_id`
WHERE t.is_ms_shipped <> 1
AND index_id > 0 AND t.name = '${this.getTnPath(args.tn)}'
ORDER BY schema_name(t.schema_id) + '.' + t.[name], i.index_id`
);
const rows = [];
for (let i = 0, rowCount = 0; i < response.length; ++i, ++rowCount) {
@ -748,39 +751,39 @@ class MssqlClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select t.[name] as table_view,
case when t.[type] = 'U' then 'Table'
when t.[type] = 'V' then 'View'
end as [object_type],
`SELECT t.[name] AS table_view,
CASE WHEN t.[type] = 'U' THEN 'Table'
WHEN t.[type] = 'V' THEN 'View'
END AS [object_type],
i.index_id,
case when i.is_primary_key = 1 then 'Primary key'
when i.is_unique = 1 then 'Unique'
else 'Not Unique' end as [type],
i.[name] as index_name,
substring(column_names, 1, len(column_names)-1) as [columns],
case when i.[type] = 1 then 'Clustered index'
when i.[type] = 2 then 'Nonclustered unique index'
when i.[type] = 3 then 'XML index'
when i.[type] = 4 then 'Spatial index'
when i.[type] = 5 then 'Clustered columnstore index'
when i.[type] = 6 then 'Nonclustered columnstore index'
when i.[type] = 7 then 'Nonclustered hash index'
end as index_type
from sys.objects t
inner join sys.indexes i
on t.object_id = i.object_id
cross apply (select col.[name] + ', ' + CAST(ic.key_ordinal as varchar) + ', '
from sys.index_columns ic
inner join sys.columns col
on ic.object_id = col.object_id
and ic.column_id = col.column_id
where ic.object_id = t.object_id
and ic.index_id = i.index_id
order by col.column_id
CASE WHEN i.is_primary_key = 1 THEN 'Primary key'
WHEN i.is_unique = 1 THEN 'Unique'
else 'Not Unique' END AS [type],
i.[name] AS index_name,
substring(column_names, 1, len(column_names)-1) AS [columns],
CASE WHEN i.[type] = 1 THEN 'Clustered index'
WHEN i.[type] = 2 THEN 'Nonclustered unique index'
WHEN i.[type] = 3 THEN 'XML index'
WHEN i.[type] = 4 THEN 'Spatial index'
WHEN i.[type] = 5 THEN 'Clustered columnstore index'
WHEN i.[type] = 6 THEN 'Nonclustered columnstore index'
WHEN i.[type] = 7 THEN 'Nonclustered hash index'
END AS index_type
FROM sys.objects t
INNER JOIN sys.indexes i
ON t.object_id = i.object_id
cross apply (SELECT col.[name] + ', ' + CAST(ic.key_ordinal AS varchar) + ', '
FROM sys.index_columns ic
INNER JOIN sys.columns col
ON ic.object_id = col.object_id
AND ic.column_id = col.column_id
WHERE ic.object_id = t.object_id
AND ic.index_id = i.index_id
ORDER BY col.column_id
for xml path ('') ) D (column_names)
where t.is_ms_shipped <> 1
and index_id > 0 and t.name = '${this.getTnPath(args.tn)}'
order by schema_name(t.schema_id) + '.' + t.[name], i.index_id`
WHERE t.is_ms_shipped <> 1
AND index_id > 0 AND t.name = '${this.getTnPath(args.tn)}'
ORDER BY schema_name(t.schema_id) + '.' + t.[name], i.index_id`
);
const rows = [];
for (let i = 0, rowCount = 0; i < response.length; ++i, ++rowCount) {
@ -838,25 +841,25 @@ class MssqlClient extends KnexClient {
log.api(`${_func}:args:`, args);
try {
const response = await this.sqlClient
.raw(`select fk_tab.name as tn, '>-' as rel, pk_tab.name as rtn,
fk_cols.constraint_column_id as no, fk_col.name as cn, ' = ' as [join],
pk_col.name as rcn, fk.name as cstn,
fk.update_referential_action_desc as ur, fk.delete_referential_action_desc as dr
from sys.foreign_keys fk
inner join sys.tables fk_tab
on fk_tab.object_id = fk.parent_object_id
inner join sys.tables pk_tab
on pk_tab.object_id = fk.referenced_object_id
inner join sys.foreign_key_columns fk_cols
on fk_cols.constraint_object_id = fk.object_id
inner join sys.columns fk_col
on fk_col.column_id = fk_cols.parent_column_id
and fk_col.object_id = fk_tab.object_id
inner join sys.columns pk_col
on pk_col.column_id = fk_cols.referenced_column_id
and pk_col.object_id = pk_tab.object_id
where fk_tab.name = '${this.getTnPath(args.tn)}'
order by fk_tab.name, pk_tab.name, fk_cols.constraint_column_id`);
.raw(`SELECT fk_tab.name AS tn, '>-' AS rel, pk_tab.name AS rtn,
fk_cols.constraint_column_id AS no, fk_col.name AS cn, ' = ' AS [join],
pk_col.name AS rcn, fk.name AS cstn,
fk.update_referential_action_desc AS ur, fk.delete_referential_action_desc AS dr
FROM sys.foreign_keys fk
INNER JOIN sys.tables fk_tab
ON fk_tab.object_id = fk.parent_object_id
INNER JOIN sys.tables pk_tab
ON pk_tab.object_id = fk.referenced_object_id
INNER JOIN sys.foreign_key_columns fk_cols
ON fk_cols.constraint_object_id = fk.object_id
INNER JOIN sys.columns fk_col
ON fk_col.column_id = fk_cols.parent_column_id
AND fk_col.object_id = fk_tab.object_id
INNER JOIN sys.columns pk_col
ON pk_col.column_id = fk_cols.referenced_column_id
AND pk_col.object_id = pk_tab.object_id
WHERE fk_tab.name = '${this.getTnPath(args.tn)}'
ORDER BY fk_tab.name, pk_tab.name, fk_cols.constraint_column_id`);
const ruleMapping = {
NO_ACTION: 'NO ACTION',
@ -904,24 +907,24 @@ class MssqlClient extends KnexClient {
log.api(`${_func}:args:`, args);
try {
const response = await this
.raw(`select fk_tab.name as tn, '>-' as rel, pk_tab.name as rtn,
fk_cols.constraint_column_id as no, fk_col.name as cn, ' = ' as [join],
pk_col.name as rcn, fk.name as cstn,
fk.update_referential_action_desc as ur, fk.delete_referential_action_desc as dr
from sys.foreign_keys fk
inner join sys.tables fk_tab
on fk_tab.object_id = fk.parent_object_id
inner join sys.tables pk_tab
on pk_tab.object_id = fk.referenced_object_id
inner join sys.foreign_key_columns fk_cols
on fk_cols.constraint_object_id = fk.object_id
inner join sys.columns fk_col
on fk_col.column_id = fk_cols.parent_column_id
and fk_col.object_id = fk_tab.object_id
inner join sys.columns pk_col
on pk_col.column_id = fk_cols.referenced_column_id
and pk_col.object_id = pk_tab.object_id
order by fk_tab.name, pk_tab.name, fk_cols.constraint_column_id`);
.raw(`SELECT fk_tab.name AS tn, '>-' AS rel, pk_tab.name AS rtn,
fk_cols.constraint_column_id AS no, fk_col.name AS cn, ' = ' AS [join],
pk_col.name AS rcn, fk.name AS cstn,
fk.update_referential_action_desc AS ur, fk.delete_referential_action_desc AS dr
FROM sys.foreign_keys fk
INNER JOIN sys.tables fk_tab
ON fk_tab.object_id = fk.parent_object_id
INNER JOIN sys.tables pk_tab
ON pk_tab.object_id = fk.referenced_object_id
INNER JOIN sys.foreign_key_columns fk_cols
ON fk_cols.constraint_object_id = fk.object_id
INNER JOIN sys.columns fk_col
ON fk_col.column_id = fk_cols.parent_column_id
AND fk_col.object_id = fk_tab.object_id
INNER JOIN sys.columns pk_col
ON pk_col.column_id = fk_cols.referenced_column_id
AND pk_col.object_id = pk_tab.object_id
ORDER BY fk_tab.name, pk_tab.name, fk_cols.constraint_column_id`);
const ruleMapping = {
NO_ACTION: 'NO ACTION',
@ -969,31 +972,31 @@ class MssqlClient extends KnexClient {
const result = new Result();
log.api(`${_func}:args:`, args);
try {
const query = `select trg.name as trigger_name,
tab.name as [table],
case when is_instead_of_trigger = 1 then 'Instead of'
else 'After' end as [activation],
(case when objectproperty(trg.object_id, 'ExecIsUpdateTrigger') = 1
then 'Update' else '' end
+ case when objectproperty(trg.object_id, 'ExecIsDeleteTrigger') = 1
then 'Delete' else '' end
+ case when objectproperty(trg.object_id, 'ExecIsInsertTrigger') = 1
then 'Insert' else '' end
) as [event],
case when trg.parent_class = 1 then 'Table trigger'
when trg.parent_class = 0 then 'Database trigger'
end [class],
case when trg.[type] = 'TA' then 'Assembly (CLR) trigger'
when trg.[type] = 'TR' then 'SQL trigger'
else '' end as [type],
case when is_disabled = 1 then 'Disabled'
else 'Active' end as [status],
object_definition(trg.object_id) as [definition]
from sys.triggers trg
const query = `SELECT trg.name AS trigger_name,
tab.name AS [table],
CASE WHEN is_instead_of_trigger = 1 THEN 'Instead of'
else 'After' END AS [activation],
(CASE WHEN objectproperty(trg.object_id, 'ExecIsUpdateTrigger') = 1
THEN 'Update' else '' end
+ CASE WHEN objectproperty(trg.object_id, 'ExecIsDeleteTrigger') = 1
THEN 'Delete' else '' end
+ CASE WHEN objectproperty(trg.object_id, 'ExecIsInsertTrigger') = 1
THEN 'Insert' else '' end
) AS [event],
CASE WHEN trg.parent_class = 1 THEN 'Table trigger'
WHEN trg.parent_class = 0 THEN 'Database trigger'
END [class],
CASE WHEN trg.[type] = 'TA' THEN 'Assembly (CLR) trigger'
WHEN trg.[type] = 'TR' THEN 'SQL trigger'
else '' END AS [type],
CASE WHEN is_disabled = 1 THEN 'Disabled'
else 'Active' END AS [status],
object_definition(trg.object_id) AS [definition]
FROM sys.triggers trg
left join sys.objects tab
on trg.parent_id = tab.object_id
where tab.name = '${this.getTnPath(args.tn)}'
order by trg.name;`;
ON trg.parent_id = tab.object_id
WHERE tab.name = '${this.getTnPath(args.tn)}'
ORDER BY trg.name;`;
const response = await this.sqlClient.raw(query);
@ -1038,7 +1041,7 @@ class MssqlClient extends KnexClient {
log.api(`${_func}:args:`, args);
try {
const response = await this.sqlClient.raw(
`SELECT o.name as function_name,definition, o.create_date as created, o.modify_date as modified,o.*
`SELECT o.name AS function_name,definition, o.create_date AS created, o.modify_date AS modified,o.*
FROM sys.sql_modules AS m
JOIN sys.objects AS o ON m.object_id = o.object_id
AND type IN ('FN', 'IF', 'TF')`
@ -1083,8 +1086,8 @@ class MssqlClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select SPECIFIC_NAME as procedure_name, ROUTINE_TYPE as [type],LAST_ALTERED as modified, CREATED as created,ROUTINE_DEFINITION as definition ,pc.*
from ${args.databaseName}.information_schema.routines as pc where routine_type = 'PROCEDURE'`
`SELECT SPECIFIC_NAME AS procedure_name, ROUTINE_TYPE AS [type],LAST_ALTERED AS modified, CREATED AS created,ROUTINE_DEFINITION AS definition ,pc.*
FROM ${args.databaseName}.INFORMATION_SCHEMA.ROUTINES AS pc WHERE routine_type = 'PROCEDURE'`
);
result.data.list = response;
@ -1116,8 +1119,8 @@ class MssqlClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SELECT v.name as view_name,v.*,m.* FROM sys.views v inner join sys.schemas s on s.schema_id = v.schema_id
inner join sys.sql_modules as m on m.object_id = v.object_id`
`SELECT v.name AS view_name,v.*,m.* FROM sys.views v INNER JOIN sys.schemas s ON s.schema_id = v.schema_id
INNER JOIN sys.sql_modules AS m ON m.object_id = v.object_id`
);
result.data.list = response;
@ -1150,10 +1153,10 @@ class MssqlClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SELECT o.name as function_name,definition as create_function, o.create_date as created, o.modify_date as modified,o.*
`SELECT o.name AS function_name,definition AS create_function, o.create_date AS created, o.modify_date AS modified,o.*
FROM sys.sql_modules AS m
JOIN sys.objects AS o ON m.object_id = o.object_id
AND type IN ('FN', 'IF', 'TF') and o.name = '${args.function_name}'`
AND type IN ('FN', 'IF', 'TF') AND o.name = '${args.function_name}'`
);
for (let i = 0; i < response.length; i++) {
@ -1191,8 +1194,8 @@ class MssqlClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`select SPECIFIC_NAME as procedure_name, ROUTINE_TYPE as [type],LAST_ALTERED as modified, CREATED as created,ROUTINE_DEFINITION as create_procedure ,pc.*
from ${args.databaseName}.information_schema.routines as pc where routine_type = 'PROCEDURE' and SPECIFIC_NAME='${args.procedure_name}'`
`SELECT SPECIFIC_NAME AS procedure_name, ROUTINE_TYPE AS [type],LAST_ALTERED AS modified, CREATED AS created,ROUTINE_DEFINITION AS create_procedure ,pc.*
FROM ${args.databaseName}.INFORMATION_SCHEMA.ROUTINES AS pc WHERE routine_type = 'PROCEDURE' AND SPECIFIC_NAME='${args.procedure_name}'`
);
result.data.list = response;
@ -1221,8 +1224,8 @@ class MssqlClient extends KnexClient {
args.databaseName = this.connectionConfig.connection.database;
const response = await this.sqlClient.raw(
`SELECT v.name as view_name,v.*,m.*, m.definition as view_definition FROM sys.views v inner join sys.schemas s on s.schema_id = v.schema_id
inner join sys.sql_modules as m on m.object_id = v.object_id where v.name = '${args.view_name}'`
`SELECT v.name AS view_name,v.*,m.*, m.definition AS view_definition FROM sys.views v INNER JOIN sys.schemas s ON s.schema_id = v.schema_id
INNER JOIN sys.sql_modules AS m ON m.object_id = v.object_id WHERE v.name = '${args.view_name}'`
);
result.data.list = response;
@ -1256,9 +1259,9 @@ class MssqlClient extends KnexClient {
FROM sysobjects AS [so]
INNER JOIN sys.sql_modules AS df ON object_id = so.id
INNER JOIN sysobjects AS so2 ON so.parent_obj = so2.Id
WHERE [so].[type] = 'TR' and so2.name = '${this.getTnPath(
WHERE [so].[type] = 'TR' AND so2.name = '${this.getTnPath(
args.tn
)}' and [so].[name] = '${args.trigger_name}'`
)}' AND [so].[name] = '${args.trigger_name}'`
);
for (let i = 0; i < response.length; i++) {
@ -1288,7 +1291,7 @@ class MssqlClient extends KnexClient {
try {
const rows = await this.sqlClient.raw(
`select name from sys.databases where name = '${args.database_name}'`
`SELECT name FROM sys.databases WHERE name = '${args.database_name}'`
);
if (rows.length === 0) {
@ -1596,7 +1599,7 @@ class MssqlClient extends KnexClient {
log.api(`${func}:args:`, args);
try {
const query = this.genQuery(
`CREATE TRIGGER ?? on ?? \n${args.timing} ${args.event}\n as\n${args.statement}`,
`CREATE TRIGGER ?? ON ?? \n${args.timing} ${args.event}\n as\n${args.statement}`,
[args.trigger_name, this.getTnPath(args.tn)]
);
await this.sqlClient.raw(query);
@ -1814,7 +1817,10 @@ class MssqlClient extends KnexClient {
const downStatement =
this.querySeparator() +
this.sqlClient.schema.withSchema(this.schema).dropTable(args.table).toString();
this.sqlClient.schema
.withSchema(this.schema)
.dropTable(args.table)
.toString();
this.emit(`Success : ${upQuery}`);
@ -1855,8 +1861,16 @@ class MssqlClient extends KnexClient {
AS
BEGIN
SET NOCOUNT ON;
UPDATE ?? Set ?? = GetDate() where ?? in (SELECT ?? FROM Inserted)
END;`, [triggerName, this.getTnPath(args.table_name), this.getTnPath(args.table_name), column.column_name, pk.column_name, pk.column_name]
UPDATE ?? Set ?? = GetDate() WHERE ?? in (SELECT ?? FROM Inserted)
END;`,
[
triggerName,
this.getTnPath(args.table_name),
this.getTnPath(args.table_name),
column.column_name,
pk.column_name,
pk.column_name,
]
);
upQuery += triggerCreateQuery;
@ -1891,7 +1905,7 @@ class MssqlClient extends KnexClient {
AS
BEGIN
SET NOCOUNT ON;
UPDATE [${this.schema}].[${args.table_name}] Set [${column.column_name}] = GetDate() where [${pk.column_name}] in (SELECT [${pk.column_name}] FROM Inserted)
UPDATE [${this.schema}].[${args.table_name}] Set [${column.column_name}] = GetDate() WHERE [${pk.column_name}] in (SELECT [${pk.column_name}] FROM Inserted)
END;`;
upQuery += triggerCreateQuery;
@ -2058,7 +2072,10 @@ class MssqlClient extends KnexClient {
/** ************** create up & down statements *************** */
const upStatement =
this.querySeparator() +
this.sqlClient.schema.withSchema(this.schema).dropTable(args.tn).toString();
this.sqlClient.schema
.withSchema(this.schema)
.dropTable(args.tn)
.toString();
let downQuery = this.querySeparator() + this.createTable(args.tn, args);
this.emit(`Success : ${upStatement}`);
@ -2072,8 +2089,9 @@ class MssqlClient extends KnexClient {
for (const relation of relationsList) {
downQuery +=
this.querySeparator() +
(await this.sqlClient.withSchema(this.schema).schema
.table(relation.tn, (table) => {
(await this.sqlClient
.withSchema(this.schema)
.schema.table(relation.tn, (table) => {
table = table
.foreign(relation.cn, null)
.references(relation.rcn)
@ -2116,7 +2134,8 @@ class MssqlClient extends KnexClient {
)) {
downQuery +=
this.querySeparator() +
this.sqlClient.schema.withSchema(this.schema)
this.sqlClient.schema
.withSchema(this.schema)
.table(tn, function (table) {
if (non_unique) {
table.index(columns, key_name);
@ -2152,7 +2171,7 @@ class MssqlClient extends KnexClient {
* @param {String} - args.childTable
* @returns {Promise<{upStatement, downStatement}>}
*/
async relationCreate(args) {
async relationCreate(args) {
const _func = this.relationCreate.name;
const result = new Result();
log.api(`${_func}:args:`, args);
@ -2161,19 +2180,22 @@ class MssqlClient extends KnexClient {
try {
const self = this;
await this.sqlClient.schema.table(this.getTnPath(args.childTable), function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(self.getTnPath(args.parentTable));
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
await this.sqlClient.schema.table(
this.getTnPath(args.childTable),
function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(self.getTnPath(args.parentTable));
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
}
}
});
);
const upStatement =
this.querySeparator() +
@ -2234,9 +2256,12 @@ class MssqlClient extends KnexClient {
try {
const self = this;
await this.sqlClient.schema.table(this.getTnPath(args.childTable), function (table) {
table.dropForeign(args.childColumn, foreignKeyName);
});
await this.sqlClient.schema.table(
this.getTnPath(args.childTable),
function (table) {
table.dropForeign(args.childColumn, foreignKeyName);
}
);
const upStatement =
this.querySeparator() +
@ -2379,7 +2404,7 @@ class MssqlClient extends KnexClient {
const result = new Result();
log.api(`${_func}:args:`, args);
try {
result.data = `DELETE FROM ${this.getTnPath(args.tn)} where ;`;
result.data = `DELETE FROM ${this.getTnPath(args.tn)} WHERE ;`;
} catch (e) {
log.ppe(e, _func);
throw e;

4
packages/nocodb/src/lib/db/sql-client/lib/pg/PgClient.ts

@ -468,7 +468,9 @@ class PGClient extends KnexClient {
}
if (rows.length === 0) {
log.debug('creating database:', args);
await tempSqlClient.raw(`CREATE DATABASE ?? ENCODING 'UTF8'`, [args.database]);
await tempSqlClient.raw(`CREATE DATABASE ?? ENCODING 'UTF8'`, [
args.database,
]);
}
// if (this.connectionConfig.searchPath && this.connectionConfig.searchPath[0]) {

26
packages/nocodb/src/lib/db/sql-data-mapper/lib/BaseModel.ts

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/ban-types,prefer-const */
import { Knex } from 'knex';
import { Knex } from 'knex';
import Filter from '../../../models/Filter';
import Sort from '../../../models/Sort';
@ -220,10 +220,7 @@ abstract class BaseModel {
const query = this.$db.insert(data);
if (
this.dbDriver.client === 'pg' ||
this.dbDriver.client === 'mssql'
) {
if (this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql') {
query.returning('*');
response = await this._run(query);
} else {
@ -265,10 +262,7 @@ abstract class BaseModel {
const query = this.$db.insert(data);
if (
this.dbDriver.client === 'pg' ||
this.dbDriver.client === 'mssql'
) {
if (this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql') {
query.returning('*');
response = await this._run(query);
} else {
@ -302,13 +296,13 @@ abstract class BaseModel {
for (const d of data) {
await this.validate(d);
}
const response = (this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql') ?
this.dbDriver
.batchInsert(this.tn, data, 50)
.returning(this.pks?.[0]?.cn || '*') :
this.dbDriver
.batchInsert(this.tn, data, 50);
const response =
this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql'
? this.dbDriver
.batchInsert(this.tn, data, 50)
.returning(this.pks?.[0]?.cn || '*')
: this.dbDriver.batchInsert(this.tn, data, 50);
await this.afterInsertb(data);

17
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSql.ts

@ -417,10 +417,7 @@ class BaseModelSql extends BaseModel {
const query = dbDriver(this.tnPath).insert(insertObj);
if (
this.dbDriver.client === 'pg' ||
this.dbDriver.client === 'mssql'
) {
if (this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql') {
query.returning(this.selectQuery(''));
response = await this._run(query);
}
@ -608,12 +605,12 @@ class BaseModelSql extends BaseModel {
await this.validate(d1);
}
const response = (this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql') ?
await this.dbDriver
.batchInsert(this.tn, insertDatas, 50)
.returning(this.pks[0].cn) :
await this.dbDriver
.batchInsert(this.tn, insertDatas, 50);
const response =
this.dbDriver.client === 'pg' || this.dbDriver.client === 'mssql'
? await this.dbDriver
.batchInsert(this.tn, insertDatas, 50)
.returning(this.pks[0].cn)
: await this.dbDriver.batchInsert(this.tn, insertDatas, 50);
await this.afterInsertb(insertDatas, null);

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

@ -99,9 +99,10 @@ class BaseModelSqlv2 {
qb.where(_wherePk(this.model.primaryKeys, id));
const data = (await this.extractRawQueryAndExec(qb))?.[0];
let data = (await this.extractRawQueryAndExec(qb))?.[0];
if (data) {
data = this.convertAttachmentType(data);
const proto = await this.getProto();
data.__proto__ = proto;
}
@ -157,9 +158,10 @@ class BaseModelSqlv2 {
qb.orderBy(this.model.primaryKey.column_name);
}
const data = await qb.first();
let data = await qb.first();
if (data) {
data = this.convertAttachmentType(data);
const proto = await this.getProto();
data.__proto__ = proto;
}
@ -251,7 +253,8 @@ class BaseModelSqlv2 {
if (!ignoreViewFilterAndSort) applyPaginate(qb, rest);
const proto = await this.getProto();
const data = await this.extractRawQueryAndExec(qb);
let data = await this.extractRawQueryAndExec(qb);
data = this.convertAttachmentType(data);
return data?.map((d) => {
d.__proto__ = proto;
@ -363,8 +366,7 @@ class BaseModelSqlv2 {
qb.groupBy(args.column_name);
if (sorts) await sortV2(sorts, qb, this.dbDriver);
applyPaginate(qb, rest);
const data = await qb;
return data;
return this.convertAttachmentType(await qb);
}
async multipleHmList({ colId, ids }, args: { limit?; offset? } = {}) {
@ -423,7 +425,8 @@ class BaseModelSqlv2 {
.as('list')
);
const children = await this.extractRawQueryAndExec(childQb);
let children = await this.extractRawQueryAndExec(childQb);
children = this.convertAttachmentType(children);
const proto = await (
await Model.getBaseModelSQL({
id: childTable.id,
@ -550,7 +553,8 @@ class BaseModelSqlv2 {
await childModel.selectObject({ qb });
const children = await this.extractRawQueryAndExec(qb);
let children = await this.extractRawQueryAndExec(qb);
children = this.convertAttachmentType(children);
const proto = await (
await Model.getBaseModelSQL({
@ -671,6 +675,7 @@ class BaseModelSqlv2 {
if (this.isMySQL) {
children = children[0];
}
children = this.convertAttachmentType(children);
const proto = await (
await Model.getBaseModelSQL({
id: rtnId,
@ -735,7 +740,8 @@ class BaseModelSqlv2 {
qb.limit(+rest?.limit || 25);
qb.offset(+rest?.offset || 0);
const children = await this.extractRawQueryAndExec(qb);
let children = await this.extractRawQueryAndExec(qb);
children = this.convertAttachmentType(children);
const proto = await (
await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver })
).getProto();
@ -961,8 +967,8 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await childModel.getProto();
const data = await qb;
let data = await qb;
data = this.convertAttachmentType(data);
return data.map((c) => {
c.__proto__ = proto;
return c;
@ -1076,7 +1082,8 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await childModel.getProto();
const data = await this.extractRawQueryAndExec(qb);
let data = await this.extractRawQueryAndExec(qb);
data = this.convertAttachmentType(data);
return data.map((c) => {
c.__proto__ = proto;
@ -1194,7 +1201,8 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await parentModel.getProto();
const data = await this.extractRawQueryAndExec(qb);
let data = await this.extractRawQueryAndExec(qb);
data = this.convertAttachmentType(data);
return data.map((c) => {
c.__proto__ = proto;
@ -2609,7 +2617,9 @@ class BaseModelSqlv2 {
const proto = await this.getProto();
const result = (await groupedQb)?.map((d) => {
let data = await groupedQb;
data = this.convertAttachmentType(data);
const result = data?.map((d) => {
d.__proto__ = proto;
return d;
});
@ -2726,6 +2736,35 @@ class BaseModelSqlv2 {
)
: await this.dbDriver.raw(query);
}
private _convertAttachmentType(attachmentColumns, d) {
attachmentColumns.forEach((col) => {
if (d[col.title] && typeof d[col.title] === 'string') {
d[col.title] = JSON.parse(d[col.title]);
}
});
return d;
}
private convertAttachmentType(data) {
// attachment is stored in text and parse in UI
// convertAttachmentType is used to convert the response in string to array of object in API response
if (data) {
const attachmentColumns = this.model.columns.filter(
(c) => c.uidt === UITypes.Attachment
);
if (attachmentColumns.length) {
if (Array.isArray(data)) {
data = data.map((d) =>
this._convertAttachmentType(attachmentColumns, d)
);
} else {
this._convertAttachmentType(attachmentColumns, data);
}
}
}
return data;
}
}
function extractSortsObject(

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

@ -273,7 +273,8 @@ async function getGroupedDataList(model, view: View, req) {
data = data.map((item) => {
// todo: use map to avoid loop
const count =
countArr.find((countItem: any) => countItem.key === item.key)?.count ?? 0;
countArr.find((countItem: any) => countItem.key === item.key)?.count ??
0;
item.value = new PagedResponseImpl(item.value, {
...req.query,

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

@ -1,5 +1,5 @@
import { Tele } from 'nc-help';
import orgLicenseApis from './orgLicenseApis'
import orgLicenseApis from './orgLicenseApis';
import orgTokenApis from './orgTokenApis';
import orgUserApis from './orgUserApis';
import projectApis from './projectApis';
@ -62,7 +62,7 @@ export default function (router: Router, server) {
projectApis(router);
utilApis(router);
if(process.env['PLAYWRIGHT_TEST'] === 'true') {
if (process.env['PLAYWRIGHT_TEST'] === 'true') {
router.use(testApis);
}
router.use(columnApis);

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

@ -1,12 +1,10 @@
import { Router } from 'express';
import { OrgUserRoles } from 'nocodb-sdk';
import { NC_LICENSE_KEY } from '../../constants'
import { NC_LICENSE_KEY } from '../../constants';
import Store from '../../models/Store';
import { metaApiMetrics } from '../helpers/apiMetrics';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
async function licenseGet(_req, res) {
const license = await Store.get(NC_LICENSE_KEY);

3
packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts

@ -157,7 +157,8 @@ async function getGroupedDataList(model, view: View, req) {
data = data.map((item) => {
// todo: use map to avoid loop
const count =
countArr.find((countItem: any) => countItem.key === item.key)?.count ?? 0;
countArr.find((countItem: any) => countItem.key === item.key)?.count ??
0;
item.value = new PagedResponseImpl(item.value, {
...req.query,

7
packages/nocodb/src/lib/meta/api/swagger/helpers/getSwaggerColumnMetas.ts

@ -40,6 +40,12 @@ export default async (
case UITypes.Rollup:
field.type = 'number';
break;
case UITypes.Attachment:
field.type = 'array';
field.items = {
$ref: `#/components/schemas/Attachment`,
};
break;
default:
field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType);
@ -58,4 +64,5 @@ export interface SwaggerColumn {
virtual?: boolean;
$ref?: any;
column: Column;
items?: any;
}

21
packages/nocodb/src/lib/meta/api/swagger/helpers/swagger-base.json

@ -34,6 +34,27 @@
}
}
},
"Attachment": {
"title": "Attachment",
"type": "object",
"properties": {
"mimetype": {
"type": "string"
},
"size": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"icon": {
"type": "string"
}
}
},
"Groupby": {
"title": "Groupby",
"type": "object",

2
packages/nocodb/src/lib/meta/api/swagger/swaggerApis.ts

@ -2,7 +2,7 @@
import catchError, { NcError } from '../../helpers/catchError';
import { Router } from 'express';
import Model from '../../../models/Model';
import ncMetaAclMw from '../../helpers/ncMetaAclMw'
import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import getSwaggerJSON from './helpers/getSwaggerJSON';
import Project from '../../../models/Project';
import swaggerHtml from './swaggerHtml';

12
packages/nocodb/src/lib/meta/api/sync/helpers/job.ts

@ -2237,9 +2237,12 @@ export default async (
for (let i = 0; i < ncTblList.list.length; i++) {
// not a migrated table, skip
if (undefined === aTblSchema.find((x) => x.name === ncTblList.list[i].title))
if (
undefined ===
aTblSchema.find((x) => x.name === ncTblList.list[i].title)
)
continue;
const _perfStart = recordPerfStart();
const ncTbl = await api.dbTable.read(ncTblList.list[i].id);
recordPerfStats(_perfStart, 'dbTable.read');
@ -2265,7 +2268,10 @@ export default async (
logBasic('Configuring Record Links...');
for (let i = 0; i < ncTblList.list.length; i++) {
// not a migrated table, skip
if (undefined === aTblSchema.find((x) => x.name === ncTblList.list[i].title))
if (
undefined ===
aTblSchema.find((x) => x.name === ncTblList.list[i].title)
)
continue;
const ncTbl = await api.dbTable.read(ncTblList.list[i].id);

2
packages/nocodb/src/lib/meta/api/testApis.ts

@ -6,7 +6,7 @@ export async function reset(req: Request<any, any>, res) {
parallelId: req.body.parallelId,
dbType: req.body.dbType,
isEmptyProject: req.body.isEmptyProject,
workerId: req.body.workerId
workerId: req.body.workerId,
});
res.json(await service.process());

9
packages/nocodb/src/lib/meta/helpers/populateSamplePayload.ts

@ -94,7 +94,14 @@ async function getSampleColumnValue(column: Column): Promise<any> {
break;
case UITypes.Attachment:
{
return '[{"url":"https://nocodb.com/dummy.png","title":"image.png","mimetype":"image/png","size":0}]';
return [
{
url: 'https://nocodb.com/dummy.png',
title: 'image.png',
mimetype: 'image/png',
size: 0,
},
];
}
break;
case UITypes.Checkbox:

6
packages/nocodb/src/lib/models/SelectOption.ts

@ -77,9 +77,9 @@ export default class SelectOption {
return options?.length
? {
options: options.map(
({ created_at, updated_at, ...c }) => new SelectOption(c)
),
options: options
.map(({ created_at, updated_at, ...c }) => new SelectOption(c))
.sort((x, y) => x.order - y.order),
}
: null;
}

18
packages/nocodb/src/lib/services/test/TestResetService/index.ts

@ -23,7 +23,7 @@ const loginRootUser = async () => {
const projectTitleByType = {
sqlite: 'sampleREST',
mysql: 'externalREST',
pg: 'pgExtREST'
pg: 'pgExtREST',
};
export class TestResetService {
@ -37,7 +37,7 @@ export class TestResetService {
parallelId,
dbType,
isEmptyProject,
workerId
workerId,
}: {
parallelId: string;
dbType: string;
@ -73,7 +73,7 @@ export class TestResetService {
token,
dbType: this.dbType,
parallelId: this.parallelId,
workerId: this.workerId
workerId: this.workerId,
});
try {
@ -96,7 +96,7 @@ export class TestResetService {
token,
dbType,
parallelId,
workerId
workerId,
}: {
token: string;
dbType: string;
@ -123,7 +123,7 @@ export class TestResetService {
token,
title,
parallelId,
isEmptyProject: this.isEmptyProject
isEmptyProject: this.isEmptyProject,
});
} else if (dbType == 'mysql') {
await resetMysqlSakilaProject({
@ -131,7 +131,7 @@ export class TestResetService {
title,
parallelId,
oldProject: project,
isEmptyProject: this.isEmptyProject
isEmptyProject: this.isEmptyProject,
});
} else if (dbType == 'pg') {
await resetPgSakilaProject({
@ -139,12 +139,12 @@ export class TestResetService {
title,
parallelId: workerId,
oldProject: project,
isEmptyProject: this.isEmptyProject
isEmptyProject: this.isEmptyProject,
});
}
return {
project: await Project.getByTitle(title)
project: await Project.getByTitle(title),
};
}
}
@ -177,7 +177,7 @@ const removeProjectUsersFromCache = async (project: Project) => {
const projectUsers: ProjectUser[] = await ProjectUser.getUsersList({
project_id: project.id,
limit: 1000,
offset: 0
offset: 0,
});
for (const projectUser of projectUsers) {

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

@ -157,7 +157,7 @@ export default {
dataCount: true,
upload: true,
uploadViaURL: true,
swaggerJson:true
swaggerJson: true,
},
},
commenter: {
@ -217,7 +217,7 @@ export default {
xcAuditModelCommentsCount: true,
xcExportAsCsv: true,
dataCount: true,
swaggerJson:true
swaggerJson: true,
},
},
viewer: {
@ -273,7 +273,7 @@ export default {
list: true,
xcExportAsCsv: true,
dataCount: true,
swaggerJson:true
swaggerJson: true,
},
},
[OrgUserRoles.VIEWER]: {
@ -294,11 +294,7 @@ export default {
upload: true,
uploadViaURL: true,
passwordChange: true,
pluginList: true,
pluginRead: true,
pluginTest: true,
isPluginActive: true,
pluginUpdate: true,
projectCreate: true,
projectList: true,
projectCost: true,

53
scripts/cypress/cypress.json

@ -1,53 +0,0 @@
{
"baseUrl": "http://localhost:3000/",
"testFiles": [
"test/restTableOps.js",
"test/restViews.js",
"test/restRoles.js",
"test/restMisc.js",
"test/xcdb-restTableOps.js",
"test/xcdb-restViews.js",
"test/xcdb-restRoles.js",
"test/xcdb-restMisc.js",
"test/pg-restTableOps.js",
"test/pg-restViews.js",
"test/pg-restRoles.js",
"test/pg-restMisc.js",
"test/quickTest.js",
"test/db-independent.js"
],
"defaultCommandTimeout": 13000,
"pageLoadTimeout": 600000,
"viewportWidth": 1980,
"viewportHeight": 1000,
"video": false,
"retries": 0,
"screenshotOnRunFailure": true,
"numTestsKeptInMemory": 0,
"experimentalInteractiveRunEvents": true,
"env": {
"testMode": [
{ "apiType": "rest", "dbType": "xcdb" },
{ "apiType": "rest", "dbType": "mysql" },
{ "apiType": "rest", "dbType": "postgres" }
],
"db": {
"host": "127.0.0.1",
"user": "root",
"password": "password"
},
"screenshot": false,
"airtable": {
"apiKey": "keyn1MR87qgyUsYg4",
"sharedBase": "https://airtable.com/shr4z0qmh6dg5s3eB"
}
},
"fixturesFolder": "scripts/cypress/fixtures",
"integrationFolder": "scripts/cypress/integration",
"pluginsFile": "scripts/cypress/plugins/index.js",
"screenshotsFolder": "scripts/cypress/screenshots",
"videosFolder": "scripts/cypress/videos",
"downloadsFolder": "scripts/cypress/downloads",
"supportFile": "scripts/cypress/support/index.js",
"chromeWebSecurity": false
}

67
scripts/cypress/docker-compose-cypress.yml

@ -1,67 +0,0 @@
version: "3.5"
# https://github.com/docker-library/mysql/issues/149
# disabling default sql-mode set to only_full_group_by
services:
xc-mysql-sakila-db:
network_mode: host
image: mysql:8.0
command: mysqld --sql_mode=""
restart: always
environment:
MYSQL_ROOT_PASSWORD: password
volumes:
- ../../packages/nocodb/tests/mysql-sakila-db:/docker-entrypoint-initdb.d
# xc-cypress-nocodb:
# network_mode: host
# image: node:14-alpine
# environment:
# - EE=true
# volumes:
# - ./packages/nocodb:/home/app
# command:
# - /bin/sh
# - -c
# - |
# echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
# # cp -r /home/app1/ /home/app/
# rm /home/app/package-lock.json
# rm /home/app/noco.db
# cd /home/app/ && npm i && EE=true npm run run
# # cd /home/app/ && npm i && EE=true npm run watch:run
# xc-cypress-nc-gui:
# network_mode: host
# image: node:14-alpine
# environment:
# - HOST=0.0.0.0
# - PORT=3000
# - EE=true
# volumes:
# - ./packages/nc-gui:/home/app
# - ./packages/nc-lib-gui:/home/nc-lib-gui
# command:
# - /bin/sh
# - -c
# - |
# echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
# apk --update --no-cache add git
# # cp -r /home/app1/ /home/app/
# rm /home/app/package-lock.json
# cd /home/app/ && npm i && npm run dev
# # cd /home/app/ && npm i && NODE_ENV=development npm run build && npm start

17
scripts/cypress/docker-compose-pg-cy-quick.yml

@ -1,17 +0,0 @@
version: "2.1"
services:
pg96:
image: postgres:9.6
restart: always
environment:
POSTGRES_PASSWORD: password
ports:
- 5432:5432
volumes:
- ../../packages/nocodb/tests/pg-cy-quick:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save