Browse Source

Merge branch 'develop' into barcode

pull/4641/head
flisowna 2 years ago
parent
commit
9fa675ace7
  1. 4
      .github/ISSUE_TEMPLATE/--bug-report.yaml
  2. 32
      .github/uffizzi/docker-compose.uffizzi.yml
  3. 8
      .github/workflows/release-docker.yml
  4. 3
      .github/workflows/uffizzi-preview.yml
  5. 25
      README.md
  6. 15
      markdown/readme/languages/chinese.md
  7. 13
      markdown/readme/languages/dutch.md
  8. 12
      markdown/readme/languages/french.md
  9. 13
      markdown/readme/languages/german.md
  10. 13
      markdown/readme/languages/indonesian.md
  11. 14
      markdown/readme/languages/italian.md
  12. 13
      markdown/readme/languages/japanese.md
  13. 13
      markdown/readme/languages/korean.md
  14. 13
      markdown/readme/languages/portuguese.md
  15. 13
      markdown/readme/languages/russian.md
  16. 13
      markdown/readme/languages/spanish.md
  17. 410
      packages/nc-cli/package-lock.json
  18. 2
      packages/nc-cli/package.json
  19. 3
      packages/nc-gui/components.d.ts
  20. 13
      packages/nc-gui/components/cell/DateTimePicker.vue
  21. 89
      packages/nc-gui/components/dashboard/TreeView.vue
  22. 4
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  23. 14
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  24. 21
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  25. 46
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  26. 54
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  27. 6
      packages/nc-gui/components/erd/TableNode.vue
  28. 3
      packages/nc-gui/components/general/BaseLogo.vue
  29. 58
      packages/nc-gui/components/general/EmojiIcons.vue
  30. 5
      packages/nc-gui/components/general/ReleaseInfo.vue
  31. 22
      packages/nc-gui/components/general/TableIcon.vue
  32. 4
      packages/nc-gui/components/general/TruncateText.vue
  33. 26
      packages/nc-gui/components/general/ViewIcon.vue
  34. 1
      packages/nc-gui/components/smartsheet/Cell.vue
  35. 80
      packages/nc-gui/components/smartsheet/Grid.vue
  36. 10
      packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue
  37. 38
      packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue
  38. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  39. 57
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  40. 8
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  41. 2
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  42. 21
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  43. 44
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  44. 5
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  45. 5
      packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue
  46. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  47. 9
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  48. 13
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  49. 4
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  50. 21
      packages/nc-gui/composables/useFieldQuery.ts
  51. 28
      packages/nc-gui/composables/useMultiSelect/cellRange.ts
  52. 167
      packages/nc-gui/composables/useMultiSelect/index.ts
  53. 7
      packages/nc-gui/composables/useSharedView.ts
  54. 2
      packages/nc-gui/composables/useSmartsheetStore.ts
  55. 4
      packages/nc-gui/composables/useTabs.ts
  56. 62
      packages/nc-gui/lang/de.json
  57. 242
      packages/nc-gui/lang/ru.json
  58. 3
      packages/nc-gui/layouts/shared-view.vue
  59. 1
      packages/nc-gui/lib/enums.ts
  60. 3
      packages/nc-gui/lib/types.ts
  61. 6
      packages/nc-gui/nuxt.config.ts
  62. 247
      packages/nc-gui/package-lock.json
  63. 5
      packages/nc-gui/package.json
  64. 11
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  65. 3
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/index.vue
  66. 46
      packages/nc-gui/pages/index/index/create-external.vue
  67. 10
      packages/nc-gui/utils/dateTimeUtils.ts
  68. 23
      packages/nc-gui/utils/formulaUtils.ts
  69. 1475
      packages/nc-gui/utils/iconUtils.ts
  70. 2
      packages/nc-gui/utils/parsers/parserHelpers.ts
  71. 31
      packages/nc-gui/utils/projectCreateUtils.ts
  72. 18
      packages/noco-docs/content/en/engineering/builds-and-releases.md
  73. 20
      packages/noco-docs/content/en/getting-started/installation.md
  74. 40
      packages/noco-docs/content/en/getting-started/upgrading.md
  75. 2
      packages/noco-docs/content/en/setup-and-usages/formulas.md
  76. 5
      packages/nocodb-sdk/src/lib/Api.ts
  77. 1
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  78. 1015
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  79. 5
      packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts
  80. 1
      packages/nocodb-sdk/src/lib/sqlUi/index.ts
  81. 9
      packages/nocodb/README.md
  82. 2070
      packages/nocodb/package-lock.json
  83. 5
      packages/nocodb/package.json
  84. 5
      packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts
  85. 2609
      packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts
  86. 5
      packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts
  87. 183
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  88. 7
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts
  89. 86
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  90. 36
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts
  91. 21
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts
  92. 75
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
  93. 87
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts
  94. 137
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts
  95. 29
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts
  96. 3
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts
  97. 975
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts
  98. 2
      packages/nocodb/src/lib/meta/api/apiTokenApis.ts
  99. 57
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  100. 3
      packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -35,7 +35,7 @@ body:
- type: textarea
attributes:
label: Project Details
description: Where to find it ? (See [YouTube video](https://www.youtube.com/watch?v=AUSNN-RCwhE) or [Docs](https://docs.nocodb.com/FAQs#how-to-check-my-project-info-))
description: Click on top left icon and click `Copy Project Info`. (See [YouTube video](https://www.youtube.com/watch?v=AUSNN-RCwhE) or [Docs](https://docs.nocodb.com/FAQs#how-to-check-my-project-info-))
placeholder: |
or provide the following info
```
@ -58,4 +58,4 @@ body:
placeholder: |
> Drag & drop relevant image or videos
validations:
required: false
required: false

32
.github/uffizzi/docker-compose.uffizzi.yml

@ -8,17 +8,45 @@ x-uffizzi:
services:
postgres:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: root_db
deploy:
resources:
limits:
memory: 500M
mssql:
image: "mcr.microsoft.com/mssql/server:2017-latest"
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: Password123.
deploy:
resources:
limits:
memory: 1000M
mysql:
environment:
MYSQL_DATABASE: root_db
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: noco
image: "mysql:5.7"
deploy:
resources:
limits:
memory: 500M
nocodb:
image: "${NOCODB_IMAGE}"
ports:
- "8080:8080"
restart: always
entrypoint: /bin/sh
command: ["-c", "apk add wait4ports && wait4ports tcp://localhost:5432 && /usr/src/appEntry/start.sh"]
environment:
NC_DB: "pg://localhost:5432?u=postgres&p=password&d=root_db"
NC_ADMIN_EMAIL: admin@nocodb.com
NC_ADMIN_PASSWORD: password
deploy:
resources:
limits:
memory: 500M

8
.github/workflows/release-docker.yml

@ -50,6 +50,10 @@ jobs:
run: |
DOCKER_REPOSITORY=nocodb
DOCKER_BUILD_TAG=${{ github.event.inputs.tag || inputs.tag }}
DOCKER_BUILD_LATEST_TAG=latest
if [[ "$DOCKER_BUILD_TAG" =~ "-beta." ]]; then
DOCKER_BUILD_LATEST_TAG=$(echo $DOCKER_BUILD_TAG | awk -F '-beta.' '{print $1}')-beta.latest
fi
if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then
if [[ ${{ github.event.inputs.currentVersion || inputs.currentVersion || 'N/A' }} != 'N/A' ]]; then
DOCKER_BUILD_TAG=${{ github.event.inputs.currentVersion || inputs.currentVersion }}-${{ github.event.inputs.tag || inputs.tag }}
@ -62,8 +66,10 @@ jobs:
fi
echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_OUTPUT
echo "DOCKER_BUILD_TAG=${DOCKER_BUILD_TAG}" >> $GITHUB_OUTPUT
echo "DOCKER_BUILD_LATEST_TAG=${DOCKER_BUILD_LATEST_TAG}" >> $GITHUB_OUTPUT
echo DOCKER_REPOSITORY: ${DOCKER_REPOSITORY}
echo DOCKER_BUILD_TAG: ${DOCKER_BUILD_TAG}
echo DOCKER_BUILD_LATEST_TAG: ${DOCKER_BUILD_LATEST_TAG}
- name: Checkout
uses: actions/checkout@v3
@ -134,7 +140,7 @@ jobs:
push: true
tags: |
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_TAG }}
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:latest
nocodb/${{ steps.get-docker-repository.outputs.DOCKER_REPOSITORY }}:${{ steps.get-docker-repository.outputs.DOCKER_BUILD_LATEST_TAG }}
# Temp fix
# https://github.com/docker/build-push-action/issues/252

3
.github/workflows/uffizzi-preview.yml

@ -11,6 +11,7 @@ jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
@ -85,4 +86,4 @@ jobs:
permissions:
contents: read
pull-requests: write
id-token: write
id-token: write

25
README.md

@ -33,16 +33,6 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
![All Views](https://user-images.githubusercontent.com/35857179/194825053-3aa3373d-3e0f-4b42-b3f1-42928332054a.gif)
<p align="center">
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
</p>
<div align="center">
[<img height="38" src="https://user-images.githubusercontent.com/61551451/135263434-75fe793d-42af-49e4-b964-d70920e41655.png">](markdown/readme/languages/chinese.md)
@ -80,20 +70,6 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
# Quick try
## 1-Click Deploy to Heroku
Before doing so, make sure you have a Heroku account. By default, an add-on Heroku Postgres will be used as meta database. You can see the connection string defined in `DATABASE_URL` by navigating to Heroku App Settings and selecting Config Vars.
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br/>
## NPX
You can run below command if you need an interactive configuration.
@ -226,7 +202,6 @@ Access Dashboard using : [http://localhost:8080/dashboard](http://localhost:8080
# Table of Contents
- [Quick try](#quick-try)
* [1-Click Deploy to Heroku](#1-click-deploy-to-heroku)
* [NPX](#npx)
* [Node Application](#node-application)
* [Docker](#docker)

15
markdown/readme/languages/chinese.md

@ -33,21 +33,6 @@
# 快速尝试
### 一键部署
在部署之前,请确保你有一个 Heroku 账户。默认情况下,将使用一个附加的 Heroku Postgres 作为数据库。你可以通过访问 Heroku 应用程序设置并选择 Config Vars 来查看 DATABASE_URL 中定义的连接方式。
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="一键部署 NocoDB 到 Heroku"
/>
</a>
<br>
## NPX
如果你需要一个交互式的配置,你可以运行下面的命令。

13
markdown/readme/languages/dutch.md

@ -34,19 +34,6 @@ Draait elke MySQL, PostgreSQL, SQL Server, SQLITE & MARIADB in een Smart-Spreads
# Snel proberen
### 1-Click Deploy
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Docker gebruiken
```bash

12
markdown/readme/languages/french.md

@ -34,18 +34,6 @@ Transformez n'importe quel MySQL, PostgreSQL, SQL Server, SQLite & Mariadb en un
# Essayez rapidement
### Déploiement en 1 Clic
#### Heroku
Avant de le faire, assurez-vous que vous avez un compte Heroku. Par défaut, un add-on Heroku Postgres sera utilisé comme meta database. Vous pouvez voir la string pour se connecter définie en tant que `DATABASE_URL` en naviguant dans Heroku App Settings et en sélectionnant Config Vars.
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Utilisez Docker
```bash

13
markdown/readme/languages/german.md

@ -34,19 +34,6 @@ Verwandelt jeden MySQL, PostgreSQL, SQL Server, SQLite & MariaDB in eine Smart-T
# Schneller Versuch
### 1-Klick-Bereitstellung
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Verwenden von Docker
```bash

13
markdown/readme/languages/indonesian.md

@ -34,19 +34,6 @@ Mengubah MySQL, PostgreSQL, SQL Server, SQLite & MariaDB apapun menjadi spreadsh
# Mulai Cepat
### 1-Klik Deploy
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Menggunakan Docker
```bash

14
markdown/readme/languages/italian.md

@ -31,20 +31,8 @@ Trasforma qualsiasi MySQL, PostgreSQL, SQL Server, SQLite & Mariadb in un foglio
<p align="center">
<a href="https://www.producthunt.com/posts/nocodb?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-nocodb" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=297536&theme=dark" alt="NocoDB - The Open Source Airtable alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
# Prova veloce
### 1-Click Deploy
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
# Prova veloce
### Con Docker

13
markdown/readme/languages/japanese.md

@ -34,19 +34,6 @@ MySQL、PostgreSQL、SQL Server、SQLite&Mariadbをスマートスプレッド
# クイック試し
### 1-Click Deploy
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Docker を使う
```bash

13
markdown/readme/languages/korean.md

@ -34,19 +34,6 @@ MySQL, PostgreSQL, SQL Server, SQLite, MariaDB를 똑똑한 스프레드시트
# 바로 써보기
### 원클릭 배포
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="NocoDB를 Heroku에 원클릭 배포하기"
/>
</a>
<br>
### Docker 사용
```bash

13
markdown/readme/languages/portuguese.md

@ -34,19 +34,6 @@ Transforma qualquer MySQL, PostgreSQL, SQL Server, Sqlite e MariaDB em uma plani
# Experimente rápida
### 1-Click Deploy
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Usando o Docker.
```bash

13
markdown/readme/languages/russian.md

@ -34,19 +34,6 @@
# Быстрый старт
### 1-Нажмите на Deploy
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Используя Docker
```bash

13
markdown/readme/languages/spanish.md

@ -34,19 +34,6 @@ Convierte cualquier MySQL, PostgreSQL, SQL Server, SQLite y Mariadb en una hoja
# Prueba rápida
### Implementación en 1-Click
#### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
<br>
### Usando docker
```bash

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

@ -16,7 +16,7 @@
"colors": "1.4.0",
"download": "^8.0.0",
"download-git-repo": "^3.0.2",
"express": "^4.17.1",
"express": "^4.18.2",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"inquirer": "^7.3.3",
@ -1208,12 +1208,12 @@
}
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
@ -2292,23 +2292,26 @@
"dev": true
},
"node_modules/body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
"integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.1",
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.8.1",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.9.6",
"raw-body": "2.4.2",
"type-is": "~1.6.18"
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/debug": {
@ -2322,7 +2325,7 @@
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/boxen": {
"version": "4.2.0",
@ -2488,9 +2491,9 @@
}
},
"node_modules/bytes": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
"integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
@ -2611,7 +2614,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
@ -4321,9 +4323,9 @@
}
},
"node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
@ -4884,17 +4886,21 @@
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.6"
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-file": {
"version": "1.0.0",
@ -5179,7 +5185,7 @@
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.38",
@ -5229,7 +5235,7 @@
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
@ -5381,7 +5387,7 @@
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/escape-string-regexp": {
"version": "2.0.0",
@ -5476,7 +5482,7 @@
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
@ -5736,37 +5742,38 @@
}
},
"node_modules/express": {
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
"integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
"accepts": "~1.3.7",
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.19.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.1",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.9.6",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
@ -6083,16 +6090,16 @@
}
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
@ -6110,7 +6117,7 @@
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/find-cache-dir": {
"version": "2.1.0",
@ -6495,7 +6502,7 @@
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
@ -6585,7 +6592,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@ -7368,7 +7374,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -7520,18 +7525,18 @@
"integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w=="
},
"node_modules/http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "~1.1.2",
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.6"
"node": ">= 0.8"
}
},
"node_modules/http-proxy-agent": {
@ -9270,7 +9275,7 @@
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
@ -9664,9 +9669,9 @@
"integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
@ -10179,7 +10184,6 @@
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -10289,9 +10293,9 @@
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
@ -11306,9 +11310,12 @@
}
},
"node_modules/qs": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
"integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
@ -11376,12 +11383,12 @@
}
},
"node_modules/raw-body": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
"integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.1",
"http-errors": "1.8.1",
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
@ -12009,23 +12016,23 @@
}
},
"node_modules/send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "1.8.1",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
@ -12042,7 +12049,7 @@
"node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/serialize-error": {
"version": "2.1.0",
@ -12054,14 +12061,14 @@
}
},
"node_modules/serve-static": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.2"
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
@ -12188,7 +12195,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
@ -13039,11 +13045,11 @@
}
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.6"
"node": ">= 0.8"
}
},
"node_modules/stream-events": {
@ -13445,14 +13451,6 @@
"readable-stream": "^3.0.1"
}
},
"node_modules/tedious/node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/tedious/node_modules/iconv-lite": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
@ -14776,7 +14774,7 @@
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
@ -16443,12 +16441,12 @@
}
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
}
},
"acorn": {
@ -17297,20 +17295,22 @@
"dev": true
},
"body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
"integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"requires": {
"bytes": "3.1.1",
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.8.1",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.9.6",
"raw-body": "2.4.2",
"type-is": "~1.6.18"
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"dependencies": {
"debug": {
@ -17324,7 +17324,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
@ -17446,9 +17446,9 @@
"dev": true
},
"bytes": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
"integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg=="
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"cache-base": {
"version": "1.0.1",
@ -17549,7 +17549,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
@ -18865,9 +18864,9 @@
"dev": true
},
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"cookie-signature": {
"version": "1.0.6",
@ -19309,14 +19308,14 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"detect-file": {
"version": "1.0.0",
@ -19535,7 +19534,7 @@
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"electron-to-chromium": {
"version": "1.4.38",
@ -19579,7 +19578,7 @@
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"end-of-stream": {
"version": "1.4.4",
@ -19695,7 +19694,7 @@
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"escape-string-regexp": {
"version": "2.0.0",
@ -19767,7 +19766,7 @@
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"event-target-shim": {
"version": "5.0.1",
@ -19957,37 +19956,38 @@
}
},
"express": {
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
"integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"requires": {
"accepts": "~1.3.7",
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.19.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.1",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.9.6",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
@ -20237,16 +20237,16 @@
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"dependencies": {
@ -20261,7 +20261,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
@ -20561,7 +20561,7 @@
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"from2": {
"version": "2.3.0",
@ -20629,7 +20629,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@ -21229,8 +21228,7 @@
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"dev": true
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"has-to-string-tag-x": {
"version": "1.4.1",
@ -21342,14 +21340,14 @@
"integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w=="
},
"http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"requires": {
"depd": "~1.1.2",
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
@ -22626,7 +22624,7 @@
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"mem": {
"version": "5.1.1",
@ -22929,9 +22927,9 @@
"integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"neo-async": {
"version": "2.6.2",
@ -23343,8 +23341,7 @@
"object-inspect": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
"dev": true
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
},
"object-is": {
"version": "1.1.5",
@ -23421,9 +23418,9 @@
}
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
@ -24164,9 +24161,12 @@
"dev": true
},
"qs": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
"integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ=="
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.4"
}
},
"query-string": {
"version": "5.1.1",
@ -24205,12 +24205,12 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
"integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"requires": {
"bytes": "3.1.1",
"http-errors": "1.8.1",
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
@ -24682,23 +24682,23 @@
}
},
"send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "1.8.1",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
"statuses": "2.0.1"
},
"dependencies": {
"debug": {
@ -24712,7 +24712,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
}
@ -24725,14 +24725,14 @@
"dev": true
},
"serve-static": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.2"
"send": "0.18.0"
}
},
"set-blocking": {
@ -24830,7 +24830,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
@ -25517,9 +25516,9 @@
}
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"stream-events": {
"version": "1.0.5",
@ -25827,11 +25826,6 @@
"readable-stream": "^3.0.1"
}
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"iconv-lite": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
@ -26806,7 +26800,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"unset-value": {
"version": "1.0.0",

2
packages/nc-cli/package.json

@ -73,7 +73,7 @@
"colors": "1.4.0",
"download": "^8.0.0",
"download-git-repo": "^3.0.2",
"express": "^4.17.1",
"express": "^4.18.2",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"inquirer": "^7.3.3",

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

@ -170,8 +170,6 @@ declare module '@vue/runtime-core' {
MdiExport: typeof import('~icons/mdi/export')['default']
MdiEyeCircleOutline: typeof import('~icons/mdi/eye-circle-outline')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiEyeSettings: typeof import('~icons/mdi/eye-settings')['default']
MdiEyeSettingsOutline: typeof import('~icons/mdi/eye-settings-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default']
@ -203,7 +201,6 @@ declare module '@vue/runtime-core' {
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMenuIcon: typeof import('~icons/mdi/menu-icon')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']

13
packages/nc-gui/components/cell/DateTimePicker.vue

@ -2,10 +2,13 @@
import dayjs from 'dayjs'
import {
ActiveCellInj,
ColumnInj,
ReadonlyInj,
dateFormats,
inject,
isDrawerOrModalExist,
ref,
timeFormats,
useProject,
useSelectedCellKeyupListener,
watch,
@ -32,7 +35,11 @@ const column = inject(ColumnInj)!
let isDateInvalid = $ref(false)
const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const dateTimeFormat = $computed(() => {
const dateFormat = column?.value?.meta?.date_format ?? dateFormats[0]
const timeFormat = column?.value?.meta?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}`
})
let localState = $computed({
get() {
@ -54,7 +61,7 @@ let localState = $computed({
}
if (val.isValid()) {
emit('update:modelValue', val?.format(dateFormat))
emit('update:modelValue', val?.format(isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'))
}
},
})
@ -165,7 +172,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:show-time="true"
:bordered="false"
class="!w-full !px-0 !border-none"
format="YYYY-MM-DD HH:mm"
:format="dateTimeFormat"
:placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"

89
packages/nc-gui/components/dashboard/TreeView.vue

@ -1,14 +1,17 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import type { Input } from 'ant-design-vue'
import { Dropdown, Tooltip, message } from 'ant-design-vue'
import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button'
import { Icon } from '@iconify/vue'
import type { VNodeRef } from '#imports'
import {
ClientType,
Empty,
TabType,
computed,
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isMac,
reactive,
@ -27,7 +30,7 @@ import {
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
const { addTab } = useTabs()
const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp()
@ -77,7 +80,6 @@ const initSortable = (el: Element) => {
if (!base_id) return
if (sortables[base_id]) sortables[base_id].destroy()
Sortable.create(el as HTMLLIElement, {
handle: '.nc-drag-icon',
onEnd: async (evt) => {
const offset = tables.value.findIndex((table) => table.base_id === base_id)
@ -299,6 +301,26 @@ watch(
},
{ immediate: true },
)
const setIcon = async (icon: string, table: TableType) => {
try {
table.meta = {
...(table.meta || {}),
icon,
}
tables.value.splice(tables.value.indexOf(table), 1, { ...table })
updateTab({ id: table.id }, { meta: table.meta })
$api.dbTable.update(table.id as string, {
meta: table.meta,
})
$e('a:table:icon:navdraw', { icon })
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
@ -392,6 +414,12 @@ watch(
MSSQL
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)">
<div class="color-transition nc-project-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
@ -416,10 +444,10 @@ watch(
<div v-if="bases[0] && bases[0].enabled && !bases.slice(1).filter((el) => el.enabled)?.length" class="flex-1">
<div
v-if="isUIAllowed('table-create')"
class="group flex items-center gap-2 pl-8 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
class="group flex items-center gap-2 pl-2 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog(bases[0].id)"
>
<MdiPlus />
<MdiPlus class="w-5" />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{ $t('tooltip.addTable') }}</span>
@ -502,6 +530,12 @@ watch(
MSSQL
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)">
<div class="color-transition nc-project-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
@ -546,26 +580,47 @@ watch(
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
<GeneralTooltip class="pl-8 pr-3 py-2" modifier-key="Alt">
<GeneralTooltip class="pl-2 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<MdiDragVertical
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
:is="isUIAllowed('tableIconCustomisation') ? Dropdown : 'div'"
trigger="click"
destroy-popup-on-hide
class="flex items-center"
@click.stop
>
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
</span>
<component
:is="icon(table)"
v-else
class="nc-table-icon nc-view-icon w-5"
:class="{ 'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
<template v-if="isUIAllowed('tableIconCustomisation')" #title>Change icon</template>
</component>
</div>
<template v-if="isUIAllowed('tableIconCustomisation')" #overlay>
<GeneralEmojiIcons class="shadow bg-white p-2" @select-icon="setIcon($event, table)" />
</template>
</component>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
<GeneralTruncateText :key="table.title" :length="activeTable === table.id ? 18 : 20">{{
table.title
}}</GeneralTruncateText>
</div>
<a-dropdown

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

@ -200,6 +200,10 @@ watch(
clientType = ClientType.MSSQL
vState.value = DataSourcesSubTab.New
break
case ClientType.SNOWFLAKE:
clientType = ClientType.SNOWFLAKE
vState.value = DataSourcesSubTab.New
break
case DataSourcesSubTab.New:
if (sources.length > 1 && !forceAwakened) {
vState.value = ''

14
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -70,8 +70,6 @@ const columns = [
// Models
title: tableHeaderRenderer(t('labels.models')),
key: 'table_name',
customRender: ({ record }: { record: { table_name: string; title?: string } }) =>
h('div', {}, record.title || record.table_name),
},
{
// Sync state
@ -97,7 +95,6 @@ const columns = [
</div>
</a-button>
</div>
<div class="max-h-600px overflow-y-auto">
<a-table
class="w-full"
@ -116,6 +113,17 @@ const columns = [
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<template #bodyCell="{ record, column }">
<div v-if="column.key === 'table_name'">
<div class="flex items-center gap-1">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="record" class="text-gray-500"></GeneralTableIcon>
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title || record.table_name }}</span>
</div>
</div>
</template>
</a-table>
</div>
</div>

21
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -10,7 +10,6 @@ import {
useI18n,
useNuxtApp,
useProject,
viewIcons,
} from '#imports'
const props = defineProps<{
@ -159,12 +158,24 @@ const columns = [
</template>
<template #bodyCell="{ record, column }">
<div v-if="column.name === 'table_name'">{{ record._ptn }}</div>
<div v-if="column.name === 'table_name'">
<div class="flex items-center gap-1">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon
:meta="{ meta: record.table_meta, type: record.ptype }"
class="text-gray-500"
></GeneralTableIcon>
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record._ptn }}</span>
</div>
</div>
<div v-if="column.name === 'view_name'">
<div class="flex items-center">
<component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" />
{{ record.title }}
<div class="flex items-center gap-1">
<div class="min-w-5 flex items-center justify-center">
<GeneralViewIcon :meta="record" class="text-gray-500"></GeneralViewIcon>
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title }}</span>
</div>
</div>

46
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -83,6 +83,15 @@ const validators = computed(() => {
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: formState.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
@ -383,6 +392,43 @@ watch(
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.username']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

54
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -71,19 +71,22 @@ const customFormState = ref<ProjectCreateForm>({
const validators = computed(() => {
return {
'title': [
{
required: true,
message: 'Base name is required',
},
projectTitleValidator,
],
'title': [projectTitleValidator],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
...(formState.value.dataSource.client === ClientType.SQLITE
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: formState.value.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
@ -376,6 +379,43 @@ onMounted(async () => {
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.username']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

6
packages/nc-gui/components/erd/TableNode.vue

@ -59,10 +59,8 @@ watch(
:class="[showSkeleton ? '' : 'bg-primary bg-opacity-10', hasColumns ? 'border-b-1' : '']"
class="text-slate-600 text-md py-2 border-slate-500 rounded-t-lg w-full h-full px-3 font-semibold flex items-center"
>
<MdiTableLarge v-if="table.type === 'table'" class="text-primary" :class="showSkeleton ? 'text-6xl !px-2' : ''" />
<MdiEyeCircleOutline v-else class="text-primary" :class="showSkeleton ? 'text-6xl !px-2' : ''" />
<div :class="showSkeleton ? 'text-6xl' : ''" class="flex px-2">
<GeneralTableIcon class="text-primary" :class="{ '!text-6xl !w-auto mr-2': showSkeleton }" :meta="table" />
<div :class="showSkeleton ? 'text-6xl' : ''" class="flex pr-2 pl-1">
{{ table.title }}
</div>
</div>

3
packages/nc-gui/components/general/BaseLogo.vue

@ -3,6 +3,7 @@ import LogosMysqlIcon from '~icons/logos/mysql-icon'
import LogosPostgresql from '~icons/logos/postgresql'
import VscodeIconsFileTypeSqlite from '~icons/vscode-icons/file-type-sqlite'
import SimpleIconsMicrosoftsqlserver from '~icons/simple-icons/microsoftsqlserver'
import LogosSnowflakeIcon from '~icons/logos/snowflake-icon'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
const { baseType } = defineProps<{ baseType?: string }>()
@ -17,6 +18,8 @@ const baseIcon = computed(() => {
return VscodeIconsFileTypeSqlite
case ClientType.MSSQL:
return SimpleIconsMicrosoftsqlserver
case ClientType.SNOWFLAKE:
return LogosSnowflakeIcon
default:
return MdiDatabaseOutline
}

58
packages/nc-gui/components/general/EmojiIcons.vue

@ -0,0 +1,58 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import InfiniteLoading from 'v3-infinite-loading'
import { emojiIcons } from '#imports'
const emit = defineEmits(['selectIcon'])
let search = $ref('')
// keep a variable to load icons with infinite scroll
// set initial value to 60 to load first 60 icons (index - `0 - 59`)
// and next value will be 120 and shows first 120 icons ( index - `0 - 129`)
let toIndex = $ref(60)
const filteredIcons = computed(() => {
return emojiIcons.filter((icon) => !search || icon.toLowerCase().includes(search.toLowerCase())).slice(0, toIndex)
})
const load = () => {
// increment `toIndex` to include next set of icons
toIndex += Math.min(filteredIcons.value.length, toIndex + 60)
if (toIndex > filteredIcons.value.length) {
toIndex = filteredIcons.value.length
}
}
const selectIcon = (icon: string) => {
search = ''
emit('selectIcon', `emojione:${icon}`)
}
</script>
<template>
<div class="p-1 w-[280px] h-[280px] flex flex-col gap-1 justify-start nc-emoji" data-testid="nc-emoji-container">
<div @click.stop>
<input
v-model="search"
data-testid="nc-emoji-filter"
class="p-1 text-xs border-1 w-full overflow-y-auto"
placeholder="Search"
@input="toIndex = 60"
/>
</div>
<div class="flex gap-1 flex-wrap w-full flex-shrink overflow-y-auto scrollbar-thin-dull">
<div v-for="icon of filteredIcons" :key="icon" @click="selectIcon(icon)">
<span class="cursor-pointer nc-emoji-item">
<Icon class="text-xl iconify" :icon="`emojione:${icon}`"></Icon>
</span>
</div>
<InfiniteLoading @infinite="load"><span /></InfiniteLoading>
</div>
</div>
</template>
<style scoped>
.nc-emoji-item {
@apply hover:(bg-primary bg-opacity-10) active:(bg-primary !bg-opacity-20) rounded-md w-[38px] h-[38px] block flex items-center justify-center;
}
</style>

5
packages/nc-gui/components/general/ReleaseInfo.vue

@ -7,6 +7,9 @@ const { currentVersion, latestRelease, hiddenRelease } = useGlobal()
const releaseAlert = computed({
get() {
if (currentVersion.value?.includes('-beta.') || latestRelease.value?.includes('-beta.')) {
return false
}
return (
currentVersion.value &&
latestRelease.value &&
@ -22,7 +25,7 @@ const releaseAlert = computed({
async function fetchReleaseInfo() {
try {
const versionInfo = await $api.utils.appVersion()
if (versionInfo && versionInfo.releaseVersion && versionInfo.currentVersion && !/[^0-9.]/.test(versionInfo.currentVersion)) {
if (versionInfo && versionInfo.releaseVersion && versionInfo.currentVersion) {
currentVersion.value = versionInfo.currentVersion
latestRelease.value = versionInfo.releaseVersion
} else {

22
packages/nc-gui/components/general/TableIcon.vue

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { Icon as IcIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk'
const { meta: tableMeta } = defineProps<{
meta: TableType
}>()
</script>
<template>
<IcIcon
v-if="tableMeta.meta?.icon"
:data-testid="`nc-icon-${tableMeta.meta?.icon}`"
class="text-lg"
:icon="tableMeta.meta?.icon"
/>
<MdiEyeCircleOutline v-else-if="tableMeta?.type === 'view'" class="w-5" />
<MdiTableLarge v-else class="w-5" />
</template>
<style scoped></style>

4
packages/nc-gui/components/general/TruncateText.vue

@ -41,7 +41,5 @@ const shortName = computed(() =>
<div v-else class="w-full" data-testid="truncate-label">
<slot />
</div>
<div ref="text" class="hidden">
<slot />
</div>
<div ref="text" class="hidden"><slot /></div>
</template>

26
packages/nc-gui/components/general/ViewIcon.vue

@ -0,0 +1,26 @@
<script lang="ts" setup>
import { Icon as IcIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk'
import { viewIcons } from '#imports'
const { meta: viewMeta } = defineProps<{
meta: TableType
}>()
</script>
<template>
<IcIcon
v-if="viewMeta?.meta?.icon"
:data-testid="`nc-icon-${viewMeta?.meta?.icon}`"
class="text-[16px]"
:icon="viewMeta?.meta?.icon"
/>
<component
:is="viewIcons[viewMeta.type]?.icon"
v-else
class="nc-view-icon group-hover"
:style="{ color: viewIcons[viewMeta.type]?.color }"
/>
</template>
<style scoped></style>

1
packages/nc-gui/components/smartsheet/Cell.vue

@ -163,6 +163,7 @@ const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
v-if="(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column)"
class="nc-locked-overlay"
@click.stop.prevent
@dblclick.stop.prevent
/>
</template>
</div>

80
packages/nc-gui/components/smartsheet/Grid.vue

@ -172,7 +172,7 @@ const getContainerScrollForElement = (
return scroll
}
const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyValue, isCellSelected, selectedCell } =
const { isCellSelected, activeCell, handleMouseDown, handleMouseOver, handleCellClick, clearSelectedRange, copyValue } =
useMultiSelect(
meta,
fields,
@ -201,9 +201,10 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (selectedCell.row !== null && !editEnabled) {
if (activeCell.row != null && !editEnabled) {
e.preventDefault()
const row = data.value[selectedCell.row]
clearSelectedRange()
const row = data.value[activeCell.row]
expandForm(row)
return true
}
@ -227,33 +228,33 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowUp' })
selectedCell.row = 0
selectedCell.col = selectedCell.col ?? 0
clearSelectedRange()
activeCell.row = 0
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowDown':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowDown' })
selectedCell.row = data.value.length - 1
selectedCell.col = selectedCell.col ?? 0
clearSelectedRange()
activeCell.row = data.value.length - 1
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowRight':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowRight' })
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = fields.value?.length - 1
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
case 'ArrowLeft':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowLeft' })
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = 0
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = 0
scrollToCell?.()
editEnabled = false
return true
@ -283,7 +284,7 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
},
async (ctx: { row: number; col?: number; updatedColumnTitle?: string }) => {
const rowObj = data.value[ctx.row]
const columnObj = ctx.col !== null && ctx.col !== undefined ? fields.value[ctx.col] : null
const columnObj = ctx.col !== undefined ? fields.value[ctx.col] : null
if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) {
return
@ -295,10 +296,10 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
)
function scrollToCell(row?: number | null, col?: number | null) {
row = row ?? selectedCell.row
col = col ?? selectedCell.col
row = row ?? activeCell.row
col = col ?? activeCell.col
if (row !== undefined && col !== undefined && row !== null && col !== null) {
if (row !== null && col !== null) {
// get active cell
const rows = tbodyEl.value?.querySelectorAll('tr')
const cols = rows?.[row].querySelectorAll('td')
@ -459,13 +460,14 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
/** On clicking outside of table reset active cell */
const smartTable = ref(null)
onClickOutside(smartTable, (e) => {
// do nothing if context menu was open
if (contextMenu.value) return
clearSelectedRange()
if (selectedCell.col === null) return
const activeCol = fields.value[selectedCell.col]
if (activeCell.row === null || activeCell.col === null) return
const activeCol = fields.value[activeCell.col]
if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return
@ -486,25 +488,29 @@ onClickOutside(smartTable, (e) => {
return
}
selectedCell.row = null
selectedCell.col = null
clearSelectedRange()
activeCell.row = null
activeCell.col = null
})
const onNavigate = (dir: NavigateDir) => {
if (selectedCell.row === null || selectedCell.col === null) return
if (activeCell.row === null || activeCell.col === null) return
editEnabled = false
clearSelectedRange()
switch (dir) {
case NavigateDir.NEXT:
if (selectedCell.row < data.value.length - 1) {
selectedCell.row++
if (activeCell.row < data.value.length - 1) {
activeCell.row++
} else {
addEmptyRow()
selectedCell.row++
activeCell.row++
}
break
case NavigateDir.PREV:
if (selectedCell.row > 0) {
selectedCell.row--
if (activeCell.row > 0) {
activeCell.row--
}
break
}
@ -786,10 +792,10 @@ const closeAddColumnDropdown = () => {
:data-key="rowIndex + columnObj.id"
:data-col="columnObj.id"
:data-title="columnObj.title"
@click="selectCell(rowIndex, colIndex)"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver(rowIndex, colIndex)"
@click="handleCellClick($event, rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@mousedown="startSelectRange($event, rowIndex, colIndex)"
@mouseover="endSelectRange(rowIndex, colIndex)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div v-if="!switchingTab" class="w-full h-full">
@ -797,7 +803,7 @@ const closeAddColumnDropdown = () => {
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="selectedCell.col === colIndex && selectedCell.row === rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
@navigate="onNavigate"
/>
@ -807,10 +813,10 @@ const closeAddColumnDropdown = () => {
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="
!!hasEditPermission && !!editEnabled && selectedCell.col === colIndex && selectedCell.row === rowIndex
!!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex
"
:row-index="rowIndex"
:active="selectedCell.col === colIndex && selectedCell.row === rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
@update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow(row, columnObj.title, state)"
@navigate="onNavigate"
@ -876,7 +882,7 @@ const closeAddColumnDropdown = () => {
</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="copyValue(contextMenuTarget)">
<a-menu-item v-if="contextMenuTarget" data-testid="context-menu-item-copy" @click="copyValue(contextMenuTarget)">
<div v-e="['a:row:copy']" class="nc-project-menu-item">
<!-- Copy -->
{{ $t('general.copy') }}

10
packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue

@ -1,13 +1,5 @@
<script setup lang="ts">
import {
computed,
currencyCodes,
currencyLocales,
useProject,
useVModel,
validateCurrencyCode,
validateCurrencyLocale,
} from '#imports'
import { computed, currencyCodes, currencyLocales, useVModel, validateCurrencyCode, validateCurrencyLocale } from '#imports'
interface Option {
label: string

38
packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue

@ -0,0 +1,38 @@
<script setup lang="ts">
import { dateFormats, timeFormats, useVModel } from '#imports'
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta?.date_format) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.date_format = dateFormats[0]
}
if (!vModel.value.meta?.time_format) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.time_format = timeFormats[0]
}
</script>
<template>
<a-form-item label="Date Format">
<a-select v-model:value="vModel.meta.date_format" class="nc-date-select" dropdown-class-name="nc-dropdown-date-format">
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
{{ format }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Time Format">
<a-select v-model:value="vModel.meta.time_format" class="nc-time-select" dropdown-class-name="nc-dropdown-time-format">
<a-select-option v-for="(format, i) of timeFormats" :key="i" :value="format">
{{ format }}
</a-select-option>
</a-select>
</a-form-item>
</template>

1
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -178,6 +178,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"

57
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -235,6 +235,63 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
},
typeErrors,
)
} else if (parsedTree.callee.name === 'DATETIME_DIFF') {
// parsedTree.arguments[0] = date
validateAgainstType(
parsedTree.arguments[0],
formulaTypes.DATE,
(v: any) => {
if (!validateDateWithUnknownFormat(v)) {
typeErrors.add('The first parameter of DATETIME_DIFF() should have date value')
}
},
typeErrors,
)
// parsedTree.arguments[1] = date
validateAgainstType(
parsedTree.arguments[1],
formulaTypes.DATE,
(v: any) => {
if (!validateDateWithUnknownFormat(v)) {
typeErrors.add('The second parameter of DATETIME_DIFF() should have date value')
}
},
typeErrors,
)
// parsedTree.arguments[2] = ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"]
validateAgainstType(
parsedTree.arguments[2],
formulaTypes.STRING,
(v: any) => {
if (
![
'milliseconds',
'ms',
'seconds',
's',
'minutes',
'm',
'hours',
'h',
'days',
'd',
'weeks',
'w',
'months',
'M',
'quarters',
'Q',
'years',
'y',
].includes(v)
) {
typeErrors.add(
'The third parameter of DATETIME_DIFF() should have value either "milliseconds", "ms", "seconds", "s", "minutes", "m", "hours", "h", "days", "d", "weeks", "w", "months", "M", "quarters", "Q", "years", or "y"',
)
}
},
typeErrors,
)
}
}
}

8
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -73,7 +73,13 @@ const filterOption = (value: string, option: { key: string }) => option.key.toLo
@change="onDataTypeChange"
>
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
{{ table.title }}
<div class="flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500"></GeneralTableIcon>
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ table.title }}</span>
</div>
</a-select-option>
</a-select>
</a-form-item>

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

@ -77,7 +77,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<template>
<div class="flex p-2 items-center gap-2 p-4 nc-expanded-form-header">
<h5 class="text-lg font-weight-medium flex items-center gap-1 mb-0 min-w-0 overflow-x-hidden truncate">
<mdi-table-arrow-right :style="{ color: iconColor }" />
<GeneralTableIcon :style="{ color: iconColor }" :meta="meta" class="mx-2" />
<template v-if="meta">
{{ meta.title }}

21
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -140,7 +140,7 @@ const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy()
sortable = new Sortable(el, {
handle: '.nc-drag-icon',
// handle: '.nc-drag-icon',
ghostClass: 'ghost',
onStart: onSortStart,
onEnd: onSortEnd,
@ -213,6 +213,24 @@ function openDeleteDialog(view: ViewType) {
close(1000)
}
}
const setIcon = async (icon: string, view: ViewType) => {
try {
// modify the icon property in meta
view.meta = {
...(view.meta || {}),
icon,
}
api.dbView.update(view.id as string, {
meta: view.meta,
})
$e('a:view:icon:sidebar', { icon })
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
@ -234,6 +252,7 @@ function openDeleteDialog(view: ViewType) {
@open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog"
@rename="onRename"
@select-icon="setIcon($event, view)"
/>
</a-menu>
</template>

44
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -1,18 +1,8 @@
<script lang="ts" setup>
import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity'
import {
IsLockedInj,
computed,
inject,
message,
onKeyStroke,
useDebounceFn,
useNuxtApp,
useUIPermission,
useVModel,
viewIcons,
} from '#imports'
import { Tooltip } from 'ant-design-vue'
import { IsLockedInj, inject, message, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports'
interface Props {
view: ViewType
@ -21,9 +11,15 @@ interface Props {
interface Emits {
(event: 'update:view', data: Record<string, any>): void
(event: 'selectIcon', icon: string): void
(event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: ViewType): void
(event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
}
@ -48,8 +44,6 @@ let isStopped = $ref(false)
/** Original view title when editing the view name */
let originalTitle = $ref<string | undefined>()
const viewType = computed(() => vModel.value.type as number)
/** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */
const onClick = useDebounceFn(() => {
if (isEditing || isStopped) return
@ -172,17 +166,17 @@ function onStopEdit() {
@click.stop="onClick"
>
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-testid="view-item">
<div class="flex w-auto" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<MdiDrag
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move"
@click.stop.prevent
/>
<component
:is="viewIcons[viewType].icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[viewType].color }"
/>
<div class="flex w-auto min-w-5" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<a-dropdown :trigger="['click']" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
<template v-if="isUIAllowed('tableIconCustomisation')" #title>Change icon</template>
</component>
<template v-if="isUIAllowed('viewIconCustomisation')" #overlay>
<GeneralEmojiIcons class="shadow bg-white p-2" @select-icon="emits('selectIcon', $event)" />
</template>
</a-dropdown>
</div>
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" />

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

@ -17,7 +17,7 @@ const { meta } = useSmartsheetStoreOrThrow()
const activeView = inject(ActiveViewInj, ref())
const { search, loadFieldQuery } = useFieldQuery(activeView)
const { search, loadFieldQuery } = useFieldQuery()
const isDropdownOpen = ref(false)
@ -36,7 +36,7 @@ watch(
() => activeView.value?.id,
(n, o) => {
if (n !== o) {
loadFieldQuery(activeView)
loadFieldQuery(activeView.value?.id)
}
},
{ immediate: true },
@ -76,6 +76,7 @@ function onPressEnter() {
class="max-w-[200px]"
:placeholder="$t('placeholder.filterQuery')"
:bordered="false"
data-testid="search-data-input"
@press-enter="onPressEnter"
>
<template #addonBefore> </template>

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

@ -108,8 +108,9 @@ const deleteLink = async (id: string) => {
<!-- View name -->
<a-table-column key="title" :title="$t('labels.viewName')" data-index="title">
<template #default="{ text }">
<div class="text-xs" :title="text">
<template #default="{ text, record }">
<div class="text-xs flex items-center gap-1" :title="text">
<GeneralViewIcon class="w-5" :meta="record" />
{{ text }}
</div>
</template>

7
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -4,7 +4,6 @@ import {
IsLockedInj,
IsPublicInj,
extractSdkResponseErrorMsg,
getViewIcon,
inject,
message,
ref,
@ -93,11 +92,7 @@ useMenuCloseOnEsc(open)
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<component
:is="getViewIcon(selectedView?.type)?.icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: getViewIcon(selectedView?.type)?.color }"
/>
<GeneralViewIcon :meta="selectedView"></GeneralViewIcon>
<span class="!text-sm font-weight-normal">
<GeneralTruncateText>{{ selectedView?.title }}</GeneralTruncateText>

9
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -1,17 +1,12 @@
<script setup lang="ts">
import { ActiveViewInj, inject, viewIcons } from '#imports'
import { ActiveViewInj, inject } from '#imports'
const selectedView = inject(ActiveViewInj)
</script>
<template>
<div class="flex gap-2 items-center ml-2 mr-2 pr-4 pb-1 py-0.5 border-r-1 border-gray-100">
<component
:is="viewIcons[selectedView?.type].icon"
v-if="selectedView?.type"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
/>
<GeneralViewIcon class="nc-view-icon" :meta="selectedView" />
<span class="!text-sm font-medium max-w-36 overflow-ellipsis overflow-hidden whitespace-nowrap">
{{ selectedView?.title }}

13
packages/nc-gui/components/tabs/auth/UserManagement.vue

@ -44,7 +44,7 @@ let isLoading = $ref(false)
let totalRows = $ref(0)
const currentPage = $ref(1)
let currentPage = $ref(1)
const currentLimit = $ref(10)
@ -58,7 +58,7 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
const response: any = await api.auth.projectUserList(project.value?.id, {
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
offset: (page - 1) * limit,
query: searchText.value,
},
} as RequestParams)
@ -160,7 +160,14 @@ onBeforeMount(async () => {
}
})
watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
watchDebounced(
searchText,
() => {
currentPage = 1
loadUsers()
},
{ debounce: 300, maxWait: 600 },
)
const isSuperAdmin = (user: { main_roles?: string }) => {
return user.main_roles?.split(',').includes(OrgUserRoles.SUPER_ADMIN)

4
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -129,7 +129,9 @@ const onClick = (row: Row) => {
>
<div class="flex items-center gap-1">
<MdiLinkVariant class="text-xs" type="primary" />
Link to '{{ relatedTableMeta.title }}'
Link to '
<GeneralTableIcon :meta="relatedTableMeta" class="-mx-1 w-5" />
{{ relatedTableMeta.title }}'
</div>
</a-button>
</div>

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

@ -1,8 +1,6 @@
import type { Ref } from 'vue'
import type { ViewType } from 'nocodb-sdk'
import { useState } from '#imports'
export function useFieldQuery(view: Ref<ViewType | undefined>) {
export function useFieldQuery() {
// initial search object
const emptyFieldQueryObj = {
field: '',
@ -13,21 +11,16 @@ export function useFieldQuery(view: Ref<ViewType | undefined>) {
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
}
const search = useState<{ field: string; query: string }>('field-query-search', () => ({ ...emptyFieldQueryObj }))
// 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
const loadFieldQuery = (id?: string) => {
if (!id) return
if (!(id in searchMap.value)) {
searchMap.value[id] = { ...emptyFieldQueryObj }
}
search.value = searchMap.value[view!.value!.id!]
search.value = searchMap.value[id]
}
return { search, loadFieldQuery }

28
packages/nc-gui/composables/useMultiSelect/cellRange.ts

@ -1,6 +1,6 @@
export interface Cell {
row: number | null
col: number | null
row: number
col: number
}
export class CellRange {
@ -12,14 +12,22 @@ export class CellRange {
this._end = end ?? this._start
}
get start() {
isEmpty() {
return this._start == null || this._end == null
}
isSingleCell() {
return !this.isEmpty() && this._start?.col === this._end?.col && this._start?.row === this._end?.row
}
get start(): Cell {
return {
row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN),
col: Math.min(this._start?.col ?? NaN, this._end?.col ?? NaN),
}
}
get end() {
get end(): Cell {
return {
row: Math.max(this._start?.row ?? NaN, this._end?.row ?? NaN),
col: Math.max(this._start?.col ?? NaN, this._end?.col ?? NaN),
@ -27,19 +35,11 @@ export class CellRange {
}
startRange(value: Cell) {
if (value == null) {
return
}
this._start = value
this._end = value
}
endRange(value: Cell) {
if (value == null) {
return
}
this._end = value
}
@ -47,8 +47,4 @@ export class CellRange {
this._start = null
this._end = null
}
isEmpty() {
return this._start == null || this._end == null
}
}

167
packages/nc-gui/composables/useMultiSelect/index.ts

@ -4,7 +4,7 @@ import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Cell } from './cellRange'
import { CellRange } from './cellRange'
import convertCellData from './convertCellData'
import type { Row } from '~/lib'
import type { Nullable, Row } from '~/lib'
import {
copyTable,
extractPkFromRow,
@ -22,11 +22,13 @@ import {
useProject,
} from '#imports'
const MAIN_MOUSE_PRESSED = 0
/**
* Utility to help with multi-selecting rows/cells in the smartsheet
*/
export function useMultiSelect(
_meta: MaybeRef<TableType>,
_meta: MaybeRef<TableType | undefined>,
fields: MaybeRef<ColumnType[]>,
data: MaybeRef<Row[]>,
_editEnabled: MaybeRef<boolean>,
@ -51,15 +53,26 @@ export function useMultiSelect(
const editEnabled = ref(_editEnabled)
const selectedCell = reactive<Cell>({ row: null, col: null })
const selectedRange = reactive(new CellRange())
let isMouseDown = $ref(false)
const selectedRange = reactive(new CellRange())
const activeCell = reactive<Nullable<Cell>>({ row: null, col: null })
const columnLength = $computed(() => unref(fields)?.length)
function makeActive(row: number, col: number) {
if (activeCell.row === row && activeCell.col === col) {
return
}
activeCell.row = row
activeCell.col = col
}
async function copyValue(ctx?: Cell) {
try {
if (!selectedRange.isEmpty()) {
if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) {
const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy
const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy
@ -68,8 +81,8 @@ export function useMultiSelect(
} else {
// if copy was called with context (right click position) - copy value from context
// else if there is just one selected cell, copy it's value
const cpRow = ctx?.row ?? selectedCell?.row
const cpCol = ctx?.col ?? selectedCell?.col
const cpRow = ctx?.row ?? activeCell.row
const cpCol = ctx?.col ?? activeCell.col
if (cpRow != null && cpCol != null) {
const rowObj = unref(data)[cpRow]
@ -93,29 +106,19 @@ export function useMultiSelect(
}
}
function selectCell(row: number, col: number) {
selectedRange.clear()
if (selectedCell.row === row && selectedCell.col === col) return
editEnabled.value = false
selectedCell.row = row
selectedCell.col = col
}
function endSelectRange(row: number, col: number) {
function handleMouseOver(row: number, col: number) {
if (!isMouseDown) {
return
}
selectedCell.row = null
selectedCell.col = null
selectedRange.endRange({ row, col })
}
function isCellSelected(row: number, col: number) {
if (selectedCell?.row === row && selectedCell?.col === col) {
if (activeCell.col === col && activeCell.row === row) {
return true
}
if (selectedRange.isEmpty()) {
if (selectedRange.start === null || selectedRange.end === null) {
return false
}
@ -127,46 +130,51 @@ export function useMultiSelect(
)
}
function startSelectRange(event: MouseEvent, row: number, col: number) {
function handleMouseDown(event: MouseEvent, row: number, col: number) {
// if there was a right click on selected range, don't restart the selection
const leftClickButton = 0
if (event?.button !== leftClickButton && isCellSelected(row, col)) {
if (event?.button !== MAIN_MOUSE_PRESSED && isCellSelected(row, col)) {
return
}
if (unref(editEnabled)) {
event.preventDefault()
return
}
editEnabled.value = false
isMouseDown = true
selectedRange.startRange({ row, col })
}
const handleCellClick = (event: MouseEvent, row: number, col: number) => {
isMouseDown = true
selectedRange.clear()
editEnabled.value = false
selectedRange.startRange({ row, col })
selectedRange.endRange({ row, col })
makeActive(row, col)
isMouseDown = false
}
useEventListener(document, 'mouseup', (e) => {
// if the editEnabled is false prevent the mouseup event for not select text
const handleMouseUp = (event: MouseEvent) => {
// timeout is needed, because we want to set cell as active AFTER all the child's click handler's called
// this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown
setTimeout(() => {
makeActive(selectedRange.start.row, selectedRange.start.col)
}, 0)
// if the editEnabled is false, prevent selecting text on mouseUp
if (!unref(editEnabled)) {
e.preventDefault()
event.preventDefault()
}
isMouseDown = false
})
}
const onKeyDown = async (e: KeyboardEvent) => {
const handleKeyDown = async (e: KeyboardEvent) => {
// invoke the keyEventHandler if provided and return if it returns true
if (await keyEventHandler?.(e)) {
return true
}
if (!selectedRange.isEmpty()) {
// In case the user press tabs or arrows keys
selectedCell.row = selectedRange.start.row
selectedCell.col = selectedRange.start.col
if (activeCell.row === null || activeCell.col === null) {
return
}
if (selectedCell.row === null || selectedCell.col === null) return
/** on tab key press navigate through cells */
switch (e.key) {
case 'Tab':
@ -174,21 +182,21 @@ export function useMultiSelect(
selectedRange.clear()
if (e.shiftKey) {
if (selectedCell.col > 0) {
selectedCell.col--
if (activeCell.col > 0) {
activeCell.col--
editEnabled.value = false
} else if (selectedCell.row > 0) {
selectedCell.row--
selectedCell.col = unref(columnLength) - 1
} else if (activeCell.row > 0) {
activeCell.row--
activeCell.col = unref(columnLength) - 1
editEnabled.value = false
}
} else {
if (selectedCell.col < unref(columnLength) - 1) {
selectedCell.col++
if (activeCell.col < unref(columnLength) - 1) {
activeCell.col++
editEnabled.value = false
} else if (selectedCell.row < unref(data).length - 1) {
selectedCell.row++
selectedCell.col = 0
} else if (activeCell.row < unref(data).length - 1) {
activeCell.row++
activeCell.col = 0
editEnabled.value = false
}
}
@ -198,63 +206,68 @@ export function useMultiSelect(
case 'Enter':
e.preventDefault()
selectedRange.clear()
makeEditable(unref(data)[selectedCell.row], unref(fields)[selectedCell.col])
makeEditable(unref(data)[activeCell.row], unref(fields)[activeCell.col])
break
/** on delete key press clear cell */
case 'Delete':
e.preventDefault()
selectedRange.clear()
await clearCell(selectedCell as { row: number; col: number })
await clearCell(activeCell as { row: number; col: number })
break
/** on arrow key press navigate through cells */
case 'ArrowRight':
e.preventDefault()
selectedRange.clear()
if (selectedCell.col < unref(columnLength) - 1) {
selectedCell.col++
if (activeCell.col < unref(columnLength) - 1) {
activeCell.col++
scrollToActiveCell?.()
editEnabled.value = false
}
break
case 'ArrowLeft':
selectedRange.clear()
e.preventDefault()
if (selectedCell.col > 0) {
selectedCell.col--
selectedRange.clear()
if (activeCell.col > 0) {
activeCell.col--
scrollToActiveCell?.()
editEnabled.value = false
}
break
case 'ArrowUp':
selectedRange.clear()
e.preventDefault()
if (selectedCell.row > 0) {
selectedCell.row--
selectedRange.clear()
if (activeCell.row > 0) {
activeCell.row--
scrollToActiveCell?.()
editEnabled.value = false
}
break
case 'ArrowDown':
selectedRange.clear()
e.preventDefault()
if (selectedCell.row < unref(data).length - 1) {
selectedCell.row++
selectedRange.clear()
if (activeCell.row < unref(data).length - 1) {
activeCell.row++
scrollToActiveCell?.()
editEnabled.value = false
}
break
default:
{
const rowObj = unref(data)[selectedCell.row]
const columnObj = unref(fields)[selectedCell.col]
const rowObj = unref(data)[activeCell.row]
const columnObj = unref(fields)[activeCell.col]
if ((!unref(editEnabled) || !isTypableInputColumn(columnObj)) && (isMac() ? e.metaKey : e.ctrlKey)) {
switch (e.keyCode) {
// copy - ctrl/cmd +c
case 67:
// set clipboard context only if single cell selected
if (rowObj.row[columnObj.title!]) {
if (selectedRange.isSingleCell() && rowObj.row[columnObj.title!]) {
clipboardContext = {
value: rowObj.row[columnObj.title!],
uidt: columnObj.uidt as UITypes,
@ -264,6 +277,7 @@ export function useMultiSelect(
}
await copyValue()
break
// paste - ctrl/cmd + v
case 86:
try {
// handle belongs to column
@ -297,7 +311,7 @@ export function useMultiSelect(
(relatedTableMeta as any)!.columns!,
)
return await syncCellData?.({ ...selectedCell, updatedColumnTitle: foreignKeyColumn.title })
return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title })
}
// if it's a virtual column excluding belongs to cell type skip paste
@ -315,9 +329,9 @@ export function useMultiSelect(
isMysql.value,
)
e.preventDefault()
syncCellData?.(selectedCell)
syncCellData?.(activeCell)
} else {
clearCell(selectedCell as { row: number; col: number }, true)
clearCell(activeCell as { row: number; col: number }, true)
makeEditable(rowObj, columnObj)
}
} catch (error: any) {
@ -346,15 +360,18 @@ export function useMultiSelect(
}
}
useEventListener(document, 'keydown', onKeyDown)
const clearSelectedRange = selectedRange.clear.bind(selectedRange)
useEventListener(document, 'keydown', handleKeyDown)
useEventListener(document, 'mouseup', handleMouseUp)
return {
selectCell,
startSelectRange,
endSelectRange,
clearSelectedRange: selectedRange.clear.bind(selectedRange),
handleMouseDown,
handleMouseOver,
clearSelectedRange,
copyValue,
isCellSelected,
selectedCell,
activeCell,
handleCellClick,
}
}

7
packages/nc-gui/composables/useSharedView.ts

@ -59,8 +59,11 @@ export function useSharedView() {
'xc-password': localPassword ?? password.value,
},
})
allowCSVDownload.value = JSON.parse(viewMeta.meta)?.allowCSVDownload
try {
allowCSVDownload.value = (typeof viewMeta.meta === 'string' ? JSON.parse(viewMeta.meta) : viewMeta.meta)?.allowCSVDownload
} catch {
allowCSVDownload.value = false
}
if (localPassword) password.value = localPassword
sharedView.value = { title: '', ...viewMeta }

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

@ -19,7 +19,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const cellRefs = ref<HTMLTableDataCellElement[]>([])
const { search } = useFieldQuery(view)
const { search } = useFieldQuery()
const eventBus = useEventBus<SmartsheetStoreEvents>(Symbol('SmartsheetStore'))

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

@ -41,6 +41,8 @@ const [setup, use] = useInjectionState(() => {
tab.title = currentTable.title
tab.meta = currentTable.meta
// append base alias to tab title if duplicate titles exist on other bases
if (tables.value.find((t) => t.title === currentTable?.title && t.base_id !== currentTable?.base_id))
tab.title = `${tab.title}${currentBase?.alias ? ` (${currentBase.alias})` : ``}`
@ -92,6 +94,8 @@ const [setup, use] = useInjectionState(() => {
const currentTable = tables.value.find((t) => t.id === tabMeta.id || t.title === tabMeta.id)
const currentBase = bases.value.find((b) => b.id === currentTable?.base_id)
tabMeta.meta = currentTable?.meta
// append base alias to tab title if duplicate titles exist on other bases
if (tables.value.find((t) => t.title === currentTable?.title && t.base_id !== currentTable?.base_id))
tabMeta.title = `${tabMeta.title}${currentBase?.alias ? ` (${currentBase.alias})` : ``}`

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

@ -16,7 +16,7 @@
"cancel": "Abbrechen",
"submit": "Übertragen",
"create": "Erstellen",
"duplicate": "Duplicate",
"duplicate": "Duplizieren",
"insert": "Einfügen",
"delete": "Löschen",
"update": "Aktualisieren",
@ -69,12 +69,12 @@
"betaNote": "Diese Funktion befindet sich derzeit in der Beta.",
"moreInfo": "Mehr Informationen können hier gefunden werden",
"logs": "Protokolle",
"groupingField": "Grouping Field",
"insertAfter": "Insert After",
"insertBefore": "Insert Before",
"hideField": "Hide Field",
"sortAsc": "Sort Ascending",
"sortDesc": "Sort Descending"
"groupingField": "Gruppierungsfeld",
"insertAfter": "danach einfügen",
"insertBefore": "davor einfügen",
"hideField": "Feld ausblenden",
"sortAsc": "Aufsteigend sortieren",
"sortDesc": "Absteigend sortieren"
},
"objects": {
"project": "Projekt",
@ -205,10 +205,10 @@
"quickImport": "Schnell Importieren",
"advancedSettings": "Erweiterte Einstellungen",
"codeSnippet": "Code Ausschnitt",
"keyboardShortcut": "Keyboard Shortcuts"
"keyboardShortcut": "Tastenkürzel"
},
"labels": {
"createdBy": "Created By",
"createdBy": "Erstellt von",
"notifyVia": "Benachrichtigen mit",
"projName": "Projektname",
"tableName": "Tabellenname",
@ -247,8 +247,8 @@
"created": "Erstellt",
"sqlOutput": "SQL-Ausgabe",
"addOption": "Option hinzufügen",
"qrCodeValueColumn": "Column with QR code value",
"qrCodeValueTooLong": "Too many characters for a QR code",
"qrCodeValueColumn": "Spalte mit QR-Code",
"qrCodeValueTooLong": "Zu viele Zeichen für einen QR-Code",
"aggregateFunction": "Globale Funktion",
"dbCreateIfNotExists": "Datenbank: Erstellen, falls nicht vorhanden",
"clientKey": "Client-Schlüssel",
@ -291,24 +291,24 @@
"importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns",
"noData": "Keine Daten",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",
"goToDashboard": "Zum Dashboard gehen",
"importing": "Wird importiert",
"flattenNested": "Flatten Nested",
"downloadAllowed": "Download erlaubt",
"weAreHiring": "Wir stellen ein!",
"primaryKey": "Primärschlüssel",
"hasMany": "hat viele",
"belongsTo": "gehört zu",
"manyToMany": "have many to many relation",
"extraConnectionParameters": "Extra connection parameters",
"manyToMany": "haben M:N-Beziehnungen",
"extraConnectionParameters": "Zusätzliche Verbindungsparameter",
"commentsOnly": "Nur Kommentare",
"documentation": "Dokumentation",
"subscribeNewsletter": "Abonnieren Sie unseren wöchentlichen Newsletter",
"signUpWithGoogle": "Sign up with Google",
"signInWithGoogle": "Sign in with Google",
"signUpWithGoogle": "Mit Google anmelden",
"signInWithGoogle": "Mit Google einloggen",
"agreeToTos": "Mit Ihrer Anmeldung stimmen Sie den allgemeinen Nutzungsbedingungen zu",
"welcomeToNc": "Willkommen bei NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url"
"inviteOnlySignup": "Anmeldung nur über Einladungs-URL zulassen"
},
"activity": {
"createProject": "Projekt erstellen",
@ -353,7 +353,7 @@
"invite": "Einladen",
"inviteMore": "Mehr einladen",
"inviteTeam": "Team einladen",
"inviteUser": "Invite User",
"inviteUser": "Benutzer einladen",
"inviteToken": "Token einladen",
"newUser": "Neuer Benutzer",
"editUser": "Benutzer bearbeiten",
@ -376,8 +376,8 @@
"setPrimary": "Als Primärwert festlegen",
"addRow": "Neue Zeile hinzufügen",
"saveRow": "Zeile speichern",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"saveAndExit": "Speichern & Verlassen",
"saveAndStay": "Speichern & Bleiben",
"insertRow": "Neue Zeile einfügen",
"deleteRow": "Zeile löschen",
"deleteSelectedRow": "Ausgewählte Zeilen löschen",
@ -421,9 +421,9 @@
"editConnJson": "Verbindung JSON bearbeiten",
"sponsorUs": "Sponsor uns",
"sendEmail": "E-MAIL SENDEN",
"addUserToProject": "Add user to project",
"getApiSnippet": "Get API Snippet",
"clearCell": "Clear cell",
"addUserToProject": "Benutzer zum Projekt hinzufügen",
"getApiSnippet": "zeige API Snippet",
"clearCell": "Zelle leeren",
"addFilterGroup": "Add Filter Group",
"linkRecord": "Link record",
"addNewRecord": "Add new record",
@ -492,7 +492,7 @@
"defaultValue": "Standardwert",
"filterByEmail": "Filtern nach E-Mail",
"filterQuery": "Filter-Abfrage",
"selectField": "Select field"
"selectField": "Feld wählen"
},
"msg": {
"warning": {
@ -669,13 +669,13 @@
"rowUpdateFailed": "Row update failed",
"deleteRowFailed": "Fehler beim Löschen der Zeile",
"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 _",
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required",
"formViewUpdateFailed": "Fehler beim Aktualisieren der Formularansicht",
"tableNameRequired": "Tabellenname ist erforderlich",
"nameShouldStartWithAnAlphabetOr_": "Name muss mit einem Buchstaben oder _ beginnen",
"followingCharactersAreNotAllowed": "Folgende Zeichen sind nicht erlaubt",
"columnNameRequired": "Spaltenname ist erforderlich",
"projectNameExceeds50Characters": "Projektname überschreitet 50 Zeichen",
"projectNameCannotStartWithSpace": "Project name cannot start with space",
"projectNameCannotStartWithSpace": "Projektname darf nicht mit einem Leerzeichen beginnen",
"requiredField": "Pflichtfeld",
"ipNotAllowed": "IP nicht erlaubt",
"targetFileIsNotAnAcceptedFileType": "Target file is not an accepted file type",

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

@ -16,7 +16,7 @@
"cancel": "Отмена",
"submit": "Отправить",
"create": "Создать",
"duplicate": "Duplicate",
"duplicate": "Копировать",
"insert": "Вставить",
"delete": "Удалить",
"update": "Обновить",
@ -56,25 +56,25 @@
"notification": "Уведомление",
"reference": "Ссылка",
"function": "Функция",
"confirm": "Confirm",
"confirm": "Подтвердить",
"generate": "Generate",
"copy": "Copy",
"misc": "Miscellaneous",
"lock": "Lock",
"unlock": "Unlock",
"credentials": "Credentials",
"help": "Help",
"questions": "Questions",
"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",
"groupingField": "Grouping Field",
"insertAfter": "Insert After",
"insertBefore": "Insert Before",
"hideField": "Hide Field",
"sortAsc": "Sort Ascending",
"sortDesc": "Sort Descending"
"betaNote": "Эта функция еще на стадии бета-версии.",
"moreInfo": "Больше информации можно найти здесь",
"logs": "Журналы",
"groupingField": "Поле группировки",
"insertAfter": "Вставить после",
"insertBefore": "Вставить перед",
"hideField": "Скрыть поле",
"sortAsc": "По Возрастанию",
"sortDesc": "По убыванию"
},
"objects": {
"project": "Проект",
@ -110,10 +110,10 @@
"editor": "Редактор",
"commenter": "Комментатор",
"viewer": "Просмотр",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
"orgLevelCreator": "Уровень Создатель",
"orgLevelViewer": "Уровень Читатель"
},
"sqlVIew": "SQL View"
"sqlVIew": "Просмотр SQL"
},
"datatype": {
"ID": "Идентификатор",
@ -194,21 +194,21 @@
"headCreateProject": "Создать проект |. NOCODB",
"headLogin": "Войти |. NOCODB",
"resetPassword": "Сбросить пароль",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable",
"generateToken": "Generate Token",
"teamAndSettings": "Команда и настройки",
"apiDocs": "Документация API",
"importFromAirtable": "Импортировать из Airtable",
"generateToken": "Создать Токен",
"APIsAndSupport": "APIs & Support",
"helpCenter": "Help center",
"swaggerDocumentation": "Swagger Documentation",
"quickImportFrom": "Quick Import From",
"quickImport": "Quick Import",
"advancedSettings": "Advanced Settings",
"helpCenter": "Центр поддержки",
"swaggerDocumentation": "Документация Swagger",
"quickImportFrom": "Быстрый импорт из",
"quickImport": "Быстрый импорт",
"advancedSettings": "Расширенные настройки",
"codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts"
"keyboardShortcut": "Горячие клавиши"
},
"labels": {
"createdBy": "Created By",
"createdBy": "Автор",
"notifyVia": "Уведомлять через",
"projName": "Название проекта",
"tableName": "Название таблицы",
@ -226,7 +226,7 @@
"port": "Номер порта",
"username": "Имя пользователя",
"password": "Пароль",
"schemaName": "Schema name",
"schemaName": "Имя схемы",
"database": "База данных",
"action": "Действие",
"actions": "Действия",
@ -247,8 +247,8 @@
"created": "Созданный",
"sqlOutput": "Вывод SQL",
"addOption": "Добавить настройку",
"qrCodeValueColumn": "Column with QR code value",
"qrCodeValueTooLong": "Too many characters for a QR code",
"qrCodeValueColumn": "Столбец с QR-кодом",
"qrCodeValueTooLong": "Слишком много символов для QR-кода",
"aggregateFunction": "Агрегатная функция",
"dbCreateIfNotExists": "База данных: создать, если не существует",
"clientKey": "Ключ клиента",
@ -266,7 +266,7 @@
"bookDemo": "Забронировать бесплатное демо",
"getAnswered": "Получите ответы на ваши вопросы",
"joinDiscord": "Присоединиться к",
"joinCommunity": "Join NocoDB Community",
"joinCommunity": "Присоединяйтесь к сообществу NocoDB",
"joinReddit": "Присоединиться /r/NocoDB",
"followNocodb": "Следите за NocoDB"
},
@ -276,25 +276,25 @@
"childColumn": "Дочерний столбец",
"onUpdate": "При обновлении",
"onDelete": "При удалении",
"account": "Account",
"language": "Language",
"primaryColor": "Primary Color",
"account": "Учётная запись",
"language": "Язык",
"primaryColor": "Основной цвет",
"accentColor": "Accent Color",
"customTheme": "Custom Theme",
"customTheme": "Пользовательская тема",
"requestDataSource": "Request a data source you need?",
"apiKey": "API Key",
"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",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",
"noData": "Нет данных",
"goToDashboard": "Перейти к панели управления",
"importing": "Импорт",
"flattenNested": "Flatten Nested",
"downloadAllowed": "Download allowed",
"downloadAllowed": "Скачивание разрешено",
"weAreHiring": "We are Hiring!",
"primaryKey": "Primary key",
"hasMany": "has many",
@ -302,13 +302,13 @@
"manyToMany": "have many to many relation",
"extraConnectionParameters": "Extra connection parameters",
"commentsOnly": "Comments only",
"documentation": "Documentation",
"subscribeNewsletter": "Subscribe to our weekly newsletter",
"signUpWithGoogle": "Sign up with Google",
"signInWithGoogle": "Sign in with Google",
"documentation": "Документация",
"subscribeNewsletter": "Подпишитесь на нашу еженедельную рассылку",
"signUpWithGoogle": "Зарегистрируйтесь с помощью Google",
"signInWithGoogle": "Войти при помощи Google",
"agreeToTos": "By signing up, you agree to the Terms of Service",
"welcomeToNc": "Welcome to NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url"
"welcomeToNc": "Добро пожаловать в NocoDB!",
"inviteOnlySignup": "Разрешить регистрацию только по ссылке"
},
"activity": {
"createProject": "Создать проект",
@ -353,14 +353,14 @@
"invite": "Пригласить",
"inviteMore": "Пригласить еще",
"inviteTeam": "Пригласить команду",
"inviteUser": "Invite User",
"inviteUser": "Пригласить пользователя",
"inviteToken": "Токен приглашения",
"newUser": "Новый пользователь",
"editUser": "Редактировать пользователя",
"deleteUser": "Удалить пользователя из проекта",
"resendInvite": "Переотправить приглашение e-mail",
"copyInviteURL": "Скопировать URL-адрес приглашения",
"copyPasswordResetURL": "Copy password reset URL",
"copyPasswordResetURL": "Скопировать URL для сброса пароля",
"newRole": "Новая роль",
"reloadRoles": "Перезагрузить роли",
"nextPage": "Следущая страница",
@ -376,13 +376,13 @@
"setPrimary": "Установить в качестве основного значения",
"addRow": "Добавить новую строку",
"saveRow": "Сохранить строку",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"saveAndExit": "Сохранить и выйти",
"saveAndStay": "Сохранить и остаться",
"insertRow": "Вставить новый строк",
"deleteRow": "Удалить строку",
"deleteSelectedRow": "Удалить выбранные строки",
"importExcel": "Импорт из Excel",
"importCSV": "Import CSV",
"importCSV": "Импорт CSV",
"downloadCSV": "Скачать как CSV.",
"downloadExcel": "Скачать как XLSX",
"uploadCSV": "Загрузить CSV.",
@ -421,20 +421,20 @@
"editConnJson": "Редактировать соединение JSON",
"sponsorUs": "Спонсируйте нас",
"sendEmail": "Отправить письмо",
"addUserToProject": "Add user to project",
"addUserToProject": "Добавить пользователя в проект",
"getApiSnippet": "Get API Snippet",
"clearCell": "Clear cell",
"addFilterGroup": "Add Filter Group",
"clearCell": "Очистить ячейку",
"addFilterGroup": "Добавить группу фильтров",
"linkRecord": "Link record",
"addNewRecord": "Add new record",
"addNewRecord": "Добавить новую запись",
"useConnectionUrl": "Use Connection URL",
"toggleCommentsDraw": "Toggle comments draw",
"expandRecord": "Expand Record",
"deleteRecord": "Delete Record",
"expandRecord": "Развернуть запись",
"deleteRecord": "Удалить запись",
"erd": {
"showColumns": "Show Columns",
"showColumns": "Показать колонки",
"showPkAndFk": "Show Primary and Foreign Keys",
"showSqlViews": "Show SQL Views",
"showSqlViews": "Показать SQL представления",
"showMMTables": "Show Many to Many tables",
"showJunctionTableNames": "Show Junction Table Names"
},
@ -442,7 +442,7 @@
"collapseStack": "Collapse Stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"chooseGroupingField": "Choose a Grouping Field",
"chooseGroupingField": "Выберите поле группировки",
"addOrEditStack": "Add / Edit Stack"
}
},
@ -492,20 +492,20 @@
"defaultValue": "Значение по умолчанию",
"filterByEmail": "Фильтр по электронной почте",
"filterQuery": "Filter query",
"selectField": "Select field"
"selectField": "Выбрать поле"
},
"msg": {
"warning": {
"nonEditableFields": {
"computedFieldUnableToClear": "Warning: Computed field - unable to clear text",
"qrFieldsCannotBeDirectlyChanged": "Warning: QR fields cannot be directly changed."
"computedFieldUnableToClear": "Предупреждение: Вычисляемое поле - невозможно очистить текст",
"qrFieldsCannotBeDirectlyChanged": "Внимание: QR-поля не могут быть изменены напрямую."
}
},
"info": {
"pasteNotSupported": "Paste operation is not supported on the active cell",
"pasteNotSupported": "Операция Вставки не поддерживается в выделенной ячейке",
"roles": {
"orgCreator": "Creator can create new projects and access any invited project.",
"orgViewer": "Viewer is not allowed to create new projects but they can access any invited project."
"orgViewer": "Читателю не может создавать новые проекты, но может получить доступ к любому проекту по приглашению."
},
"footerInfo": "Строк на страницу",
"upload": "Выберите файл для загрузки",
@ -597,28 +597,28 @@
"addDefaultColumns": "Добавьте столбцы по умолчанию",
"tableNameInDb": "Название таблицы как сохранено в базе данных",
"airtable": {
"credentials": "Where to find this?"
"credentials": "Где найти это?"
},
"import": {
"clickOrDrag": "Click or drag file to this area to upload"
"clickOrDrag": "Нажмите или перетащите файл в эту область для загрузки"
},
"metaDataRecreated": "Table metadata recreated successfully",
"invalidCredentials": "Invalid credentials",
"metaDataRecreated": "Метаданные таблицы успешно воссозданы",
"invalidCredentials": "Неверные учетные данные",
"downloadingMoreFiles": "Downloading more files",
"copiedToClipboard": "Copied to clipboard",
"requriedFieldsCantBeMoved": "Required field can't be moved",
"updateNotAllowedWithoutPK": "Update not allowed for table which doesn't have primary key",
"copiedToClipboard": "Скопировано в буфер",
"requriedFieldsCantBeMoved": "Обязательное поле не может быть перемещено",
"updateNotAllowedWithoutPK": "Обновление не разрешено для таблицы, которая не имеет первичного ключа",
"autoIncFieldNotEditable": "Auto increment field is not editable",
"editingPKnotSupported": "Editing primary key not supported",
"deletedCache": "Deleted cache successfully",
"cacheEmpty": "Cache is empty",
"editingPKnotSupported": "Редактирование первичного ключа не поддерживается",
"deletedCache": "Очистка кэша завершена",
"cacheEmpty": "Кэш пуст",
"exportedCache": "Exported Cache Successfully",
"valueAlreadyInList": "This value is already in the list",
"noColumnsToUpdate": "No columns to update",
"tableDeleted": "Deleted table successfully",
"valueAlreadyInList": "Это значение уже есть в списке",
"noColumnsToUpdate": "Нет столбцов для обновления",
"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",
"deleteViewConfirmation": "Вы действительно хотите удалить это представление?",
"deleteTableConfirmation": "Вы действительно хотите удалить эту таблицу",
"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.",
"computedFieldEditWarning": "Computed field: contents are read-only. Use column edit menu to reconfigure",
@ -639,52 +639,52 @@
"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"
"atLeast8Char": "Минимум 8 символов",
"atLeastOneUppercase": "Одна прописная буква",
"atLeastOneNumber": "Одна цифра",
"atLeastOneSpecialChar": "Один специальный символ",
"allowedSpecialCharList": "Список разрешенных специальных символов"
},
"invalidURL": "Invalid URL",
"invalidURL": "Неверный URL",
"internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file",
"templateGeneratorNotFound": "Генератор шаблонов не найден!",
"fileUploadFailed": "Не удалось загрузить файл",
"primaryColumnUpdateFailed": "Failed to update primary column",
"formDescriptionTooLong": "Data too long for Form Description",
"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",
"sourceHasInvalidNumbers": "Исходные данные содержат недопустимые числа",
"sourceHasInvalidBoolean": "Source data contains some invalid boolean values",
"invalidForm": "Invalid Form",
"formValidationFailed": "Form validation failed",
"youHaveBeenSignedOut": "You have been signed out",
"failedToLoadList": "Failed to load list",
"formValidationFailed": "Ошибка проверки формы",
"youHaveBeenSignedOut": "Вы вышли из системы",
"failedToLoadList": "Не удалось загрузить список",
"failedToLoadChildrenList": "Failed to load children list",
"deleteFailed": "Delete failed",
"unlinkFailed": "Unlink failed",
"deleteFailed": "Не удалось удалить",
"unlinkFailed": "Не удалось отменить связь",
"rowUpdateFailed": "Row update failed",
"deleteRowFailed": "Failed to delete row",
"setFormDataFailed": "Failed to set form data",
"deleteRowFailed": "Не удалось удалить строку",
"setFormDataFailed": "Не удалось задать данные формы",
"formViewUpdateFailed": "Failed to update form view",
"tableNameRequired": "Table name is required",
"tableNameRequired": "Требуется имя таблицы",
"nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _",
"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",
"followingCharactersAreNotAllowed": "Нельзя использовать следующие символы",
"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",
"theAcceptedFileTypeIsCsv": "Допустимый тип файла: .csv",
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Допустимые типы файлов: .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard"
"fieldRequired": "{value} не может быть пустым.",
"projectNotAccessible": "Проект недоступен",
"copyToClipboardError": "Не удалось скопировать в буфер обмена"
},
"toast": {
"exportMetadata": "Метаданные проекта успешно экспортированы",
@ -704,18 +704,18 @@
"futureRelease": "Скоро!"
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"columnDuplicated": "Столбец успешно скопирован",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",
"pluginUninstalled": "Плагин успешно удален",
"pluginSettingsSaved": "Настройки плагина сохранены",
"pluginTested": "Successfully tested plugin settings",
"tableRenamed": "Table renamed successfully",
"viewDeleted": "View deleted successfully",
"tableRenamed": "Таблица успешно переименована",
"viewDeleted": "Представление успешно удалено",
"primaryColumnUpdated": "Successfully updated as primary column",
"tableDataExported": "Successfully exported all table data",
"updated": "Successfully updated",
"tableDataExported": "Все данные таблицы успешно экспортированы",
"updated": "Успешно обновлено",
"sharedViewDeleted": "Deleted shared view successfully",
"userDeleted": "User deleted successfully",
"userDeleted": "Пользователь успешно удален",
"viewRenamed": "View renamed successfully",
"tokenGenerated": "Token generated successfully",
"tokenDeleted": "Token deleted successfully",

3
packages/nc-gui/layouts/shared-view.vue

@ -58,7 +58,8 @@ export default {
<MdiReload :class="{ 'animate-infinite animate-spin ': isLoading }" />
</template>
<div v-else class="text-xl font-semibold truncate text-white nc-shared-view-title">
<div v-else class="text-xl font-semibold truncate text-white nc-shared-view-title flex gap-2 items-center">
<GeneralViewIcon class="!text-xl" :meta="sharedView" />
{{ sharedView?.title }}
</div>
</div>

1
packages/nc-gui/lib/enums.ts

@ -20,6 +20,7 @@ export enum ClientType {
PG = 'pg',
SQLITE = 'sqlite3',
VITESS = 'vitess',
SNOWFLAKE = 'snowflake',
}
export enum Language {

3
packages/nc-gui/lib/types.ts

@ -78,6 +78,7 @@ export interface TabItem {
viewId?: string
sortsState?: Map<string, any>
filterState?: Map<string, any>
meta?: Record<string, any>
}
export interface SharedViewMeta extends Record<string, any> {
@ -100,3 +101,5 @@ export interface SharedView {
export type importFileList = (UploadFile & { data: string | ArrayBuffer })[]
export type streamImportFileList = UploadFile[]
export type Nullable<T> = { [K in keyof T]: T[K] | null }

6
packages/nc-gui/nuxt.config.ts

@ -7,6 +7,8 @@ import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill'
import PurgeIcons from 'vite-plugin-purge-icons'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: ['@vueuse/nuxt', 'nuxt-windicss', '@nuxt/image-edge'],
@ -138,6 +140,10 @@ export default defineNuxtConfig({
monacoEditorPlugin({
languageWorkers: ['json'],
}),
PurgeIcons({
/* PurgeIcons Options */
includedCollections: ['emojione'],
}),
],
define: {
'process.env.DEBUG': 'false',

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

@ -9,6 +9,7 @@
"license": "AGPL-3.0-or-later",
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@iconify/vue": "^4.0.1",
"@types/file-saver": "^2.0.5",
"@vue-flow/additional-components": "^1.2.0",
"@vue-flow/core": "^1.3.0",
@ -35,6 +36,7 @@
"sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
"v3-infinite-loading": "^1.2.2",
"validator": "^13.7.0",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
@ -87,6 +89,7 @@
"unplugin-icons": "^0.14.7",
"unplugin-vue-components": "^0.22.4",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-purge-icons": "^0.9.0",
"vitest": "^0.18.0",
"windicss": "^3.5.6"
}
@ -1205,6 +1208,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify/iconify": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.2.1.tgz",
"integrity": "sha512-WJzw+3iicrF/tbjbxxRinSgy5FHdJoz/egTqwi3xCDkNRJPq482RX1iyaWrjNuY2vMNSPkQMuqHvZDXgA+WnwQ==",
"dev": true,
"funding": {
"url": "http://github.com/sponsors/cyberalien"
}
},
"node_modules/@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
@ -1225,6 +1237,25 @@
"local-pkg": "^0.4.1"
}
},
"node_modules/@iconify/vue": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.0.1.tgz",
"integrity": "sha512-k4VwcSQpGqJpoyqENRRviFuXlVcquLvQ6BKLNJ6o2amZo7u+3HyALSO79Xyz7Sg68szQGstOk6weaKUF0DJbog==",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"vue": ">=3"
}
},
"node_modules/@iconify/vue/node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
},
"node_modules/@intlify/bundle-utils": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-2.2.2.tgz",
@ -2569,6 +2600,49 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"node_modules/@purge-icons/core": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@purge-icons/core/-/core-0.9.1.tgz",
"integrity": "sha512-sx8/a30MbbqQVEqhuMPE1wJpdVRRbEmwEPZpFzVkcDixzX4p+R2A0WVxqkb0xfHUBAVQwrSE2SeAyniIQLqbLw==",
"dev": true,
"dependencies": {
"@iconify/iconify": "2.1.2",
"axios": "^0.26.0",
"debug": "^4.3.3",
"fast-glob": "^3.2.11",
"fs-extra": "^10.0.1"
}
},
"node_modules/@purge-icons/core/node_modules/@iconify/iconify": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.1.2.tgz",
"integrity": "sha512-QcUzFeEWkE/mW+BVtEGmcWATClcCOIJFiYUD/PiCWuTcdEA297o8D4oN6Ra44WrNOHu1wqNW4J0ioaDIiqaFOQ==",
"dev": true,
"dependencies": {
"cross-fetch": "^3.1.5"
},
"funding": {
"url": "http://github.com/sponsors/cyberalien"
}
},
"node_modules/@purge-icons/core/node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/@purge-icons/generated": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@purge-icons/generated/-/generated-0.9.0.tgz",
"integrity": "sha512-s2t+1oVtGDV6KtqfCXtUOhxfeYvOdDF90IVm+nMs/6bUP0HeGZLslguuL/AibpwtfL4FA/oCsIu/RhwapgAdJw==",
"dev": true,
"dependencies": {
"@iconify/iconify": ">=2.0.0-rc.6"
}
},
"node_modules/@rollup/plugin-alias": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-4.0.2.tgz",
@ -5756,6 +5830,35 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dev": true,
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/cross-fetch/node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -14228,6 +14331,19 @@
"rollup-plugin-inject": "^3.0.0"
}
},
"node_modules/rollup-plugin-purge-icons": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-purge-icons/-/rollup-plugin-purge-icons-0.9.1.tgz",
"integrity": "sha512-hRDKBsPUz47UMdBufki2feTmBF2ClEJlYqL7N6vpVAHSLd7V2BJUaNKOF7YYbLMofVVF+9hm44YSkYO6k9hUgg==",
"dev": true,
"dependencies": {
"@purge-icons/core": "^0.9.1",
"@purge-icons/generated": "^0.9.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
@ -16136,6 +16252,11 @@
"node": ">= 0.4.0"
}
},
"node_modules/v3-infinite-loading": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/v3-infinite-loading/-/v3-infinite-loading-1.2.2.tgz",
"integrity": "sha512-MWJc6yChnqeUasBFJ3Enu8IGPcQgRMSTrAEtT1MsHBEx+QjwvNTaY8o+8V9DgVt1MVhQSl3MC55hsaWLJmpRMw=="
},
"node_modules/v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
@ -16734,6 +16855,23 @@
"monaco-editor": ">=0.33.0"
}
},
"node_modules/vite-plugin-purge-icons": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/vite-plugin-purge-icons/-/vite-plugin-purge-icons-0.9.1.tgz",
"integrity": "sha512-oS0Y9Iq6vGnTDVRzB8xJNhA/gGlyR0lfCICU6+9FRKdrO5PnT34fRjvd8YWEsegCrk91+w3GVZc0HJDj/dPp5Q==",
"dev": true,
"dependencies": {
"@purge-icons/core": "^0.9.1",
"@purge-icons/generated": "^0.9.0",
"rollup-plugin-purge-icons": "^0.9.1"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"vite": "^2.0.0-beta.3 || ^3.0.0"
}
},
"node_modules/vite-plugin-windicss": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz",
@ -18522,6 +18660,12 @@
"@iconify/types": "*"
}
},
"@iconify/iconify": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.2.1.tgz",
"integrity": "sha512-WJzw+3iicrF/tbjbxxRinSgy5FHdJoz/egTqwi3xCDkNRJPq482RX1iyaWrjNuY2vMNSPkQMuqHvZDXgA+WnwQ==",
"dev": true
},
"@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
@ -18542,6 +18686,21 @@
"local-pkg": "^0.4.1"
}
},
"@iconify/vue": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.0.1.tgz",
"integrity": "sha512-k4VwcSQpGqJpoyqENRRviFuXlVcquLvQ6BKLNJ6o2amZo7u+3HyALSO79Xyz7Sg68szQGstOk6weaKUF0DJbog==",
"requires": {
"@iconify/types": "^2.0.0"
},
"dependencies": {
"@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
}
}
},
"@intlify/bundle-utils": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-2.2.2.tgz",
@ -19485,6 +19644,48 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"@purge-icons/core": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@purge-icons/core/-/core-0.9.1.tgz",
"integrity": "sha512-sx8/a30MbbqQVEqhuMPE1wJpdVRRbEmwEPZpFzVkcDixzX4p+R2A0WVxqkb0xfHUBAVQwrSE2SeAyniIQLqbLw==",
"dev": true,
"requires": {
"@iconify/iconify": "2.1.2",
"axios": "^0.26.0",
"debug": "^4.3.3",
"fast-glob": "^3.2.11",
"fs-extra": "^10.0.1"
},
"dependencies": {
"@iconify/iconify": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.1.2.tgz",
"integrity": "sha512-QcUzFeEWkE/mW+BVtEGmcWATClcCOIJFiYUD/PiCWuTcdEA297o8D4oN6Ra44WrNOHu1wqNW4J0ioaDIiqaFOQ==",
"dev": true,
"requires": {
"cross-fetch": "^3.1.5"
}
},
"axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.8"
}
}
}
},
"@purge-icons/generated": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@purge-icons/generated/-/generated-0.9.0.tgz",
"integrity": "sha512-s2t+1oVtGDV6KtqfCXtUOhxfeYvOdDF90IVm+nMs/6bUP0HeGZLslguuL/AibpwtfL4FA/oCsIu/RhwapgAdJw==",
"dev": true,
"requires": {
"@iconify/iconify": ">=2.0.0-rc.6"
}
},
"@rollup/plugin-alias": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-4.0.2.tgz",
@ -21794,6 +21995,26 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dev": true,
"requires": {
"node-fetch": "2.6.7"
},
"dependencies": {
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
}
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -27944,6 +28165,16 @@
"rollup-plugin-inject": "^3.0.0"
}
},
"rollup-plugin-purge-icons": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-purge-icons/-/rollup-plugin-purge-icons-0.9.1.tgz",
"integrity": "sha512-hRDKBsPUz47UMdBufki2feTmBF2ClEJlYqL7N6vpVAHSLd7V2BJUaNKOF7YYbLMofVVF+9hm44YSkYO6k9hUgg==",
"dev": true,
"requires": {
"@purge-icons/core": "^0.9.1",
"@purge-icons/generated": "^0.9.0"
}
},
"rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
@ -29379,6 +29610,11 @@
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true
},
"v3-infinite-loading": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/v3-infinite-loading/-/v3-infinite-loading-1.2.2.tgz",
"integrity": "sha512-MWJc6yChnqeUasBFJ3Enu8IGPcQgRMSTrAEtT1MsHBEx+QjwvNTaY8o+8V9DgVt1MVhQSl3MC55hsaWLJmpRMw=="
},
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
@ -29690,6 +29926,17 @@
"dev": true,
"requires": {}
},
"vite-plugin-purge-icons": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/vite-plugin-purge-icons/-/vite-plugin-purge-icons-0.9.1.tgz",
"integrity": "sha512-oS0Y9Iq6vGnTDVRzB8xJNhA/gGlyR0lfCICU6+9FRKdrO5PnT34fRjvd8YWEsegCrk91+w3GVZc0HJDj/dPp5Q==",
"dev": true,
"requires": {
"@purge-icons/core": "^0.9.1",
"@purge-icons/generated": "^0.9.0",
"rollup-plugin-purge-icons": "^0.9.1"
}
},
"vite-plugin-windicss": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-1.8.7.tgz",

5
packages/nc-gui/package.json

@ -32,6 +32,8 @@
},
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@iconify/vue": "^4.0.1",
"@types/file-saver": "^2.0.5",
"@vue-flow/additional-components": "^1.2.0",
"@vue-flow/core": "^1.3.0",
"@vuelidate/core": "^2.0.0-alpha.44",
@ -41,7 +43,6 @@
"ant-design-vue": "^3.2.11",
"d3-scale": "^4.0.2",
"dagre": "^0.8.5",
"@types/file-saver": "^2.0.5",
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"httpsnippet": "^2.0.0",
@ -58,6 +59,7 @@
"sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
"v3-infinite-loading": "^1.2.2",
"validator": "^13.7.0",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
@ -110,6 +112,7 @@
"unplugin-icons": "^0.14.7",
"unplugin-vue-components": "^0.22.4",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-purge-icons": "^0.9.0",
"vitest": "^0.18.0",
"windicss": "^3.5.6"
}

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

@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import type { TabItem } from '~/lib'
import { TabType } from '~/lib'
import { TabMetaInj, iconMap, provide, useGlobal, useSidebar, useTabs } from '#imports'
@ -46,9 +47,15 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="onEdit">
<a-tab-pane v-for="(tab, i) of tabs" :key="i">
<template #tab>
<div class="flex items-center gap-2 max-w-[110px]">
<div class="flex items-center gap-2 max-w-[110px]" data-testid="nc-tab-title">
<div class="flex items-center">
<component :is="icon(tab)" class="text-sm" />
<Icon
v-if="tab.meta?.icon"
:icon="tab.meta?.icon"
class="text-xl"
:data-testid="`nc-tab-icon-${tab.meta?.icon}`"
/>
<component :is="icon(tab)" v-else class="text-sm" />
</div>
<a-tooltip v-if="tab.title?.length > 12" placement="bottom">

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

@ -19,7 +19,7 @@ const { isOverDropZone } = useDropZone(dropZone, onDrop)
const { files, open, reset } = useFileDialog()
const { isSharedBase } = useProject()
const { bases, isSharedBase } = useProject()
const { isUIAllowed } = useUIPermission()
@ -128,6 +128,7 @@ function openCreateTable() {
const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
'baseId': bases.value[0].id,
})
function closeDialog() {

46
packages/nc-gui/pages/index/index/create-external.vue

@ -79,6 +79,15 @@ const validators = computed(() => {
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: formState.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
@ -385,6 +394,43 @@ onMounted(async () => {
<a-input v-model:value="formState.dataSource.connection.connection.filename" />
</a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.username']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

10
packages/nc-gui/utils/dateTimeUtils.ts

@ -5,19 +5,21 @@ export const timeAgo = (date: any) => {
}
export const dateFormats = [
'YYYY-MM-DD',
'YYYY/MM/DD',
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
]
export const timeFormats = ['HH:mm', 'HH:mm:ss']
export const handleTZ = (val: any) => {
if (!val) {
if (val === undefined || val === null) {
return
}
if (typeof val !== 'string') {
@ -60,7 +62,7 @@ export function getDateFormat(v: string) {
export function getDateTimeFormat(v: string) {
for (const format of dateFormats) {
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
for (const timeFormat of timeFormats) {
const dateTimeFormat = `${format} ${timeFormat}`
if (dayjs(v, dateTimeFormat, true).isValid() as any) {
return dateTimeFormat

23
packages/nc-gui/utils/formulaUtils.ts

@ -51,6 +51,29 @@ const formulas: Record<string, any> = {
'DATEADD({column1}, -2, "year")',
],
},
DATETIME_DIFF: {
type: formulaTypes.DATE,
validation: {
args: {
min: 2,
max: 3,
},
},
description: 'Calculate the difference of two given date / datetime in specified units.',
syntax:
'DATETIME_DIFF(date | datetime, date | datetime, ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"])',
examples: [
'DATEDIFF({column1}, {column2})',
'DATEDIFF({column1}, {column2}, "seconds")',
'DATEDIFF({column1}, {column2}, "s")',
'DATEDIFF({column1}, {column2}, "years")',
'DATEDIFF({column1}, {column2}, "y")',
'DATEDIFF({column1}, {column2}, "minutes")',
'DATEDIFF({column1}, {column2}, "m")',
'DATEDIFF({column1}, {column2}, "days")',
'DATEDIFF({column1}, {column2}, "d")',
],
},
AND: {
type: formulaTypes.COND_EXP,
validation: {

1475
packages/nc-gui/utils/iconUtils.ts

File diff suppressed because it is too large Load Diff

2
packages/nc-gui/utils/parsers/parserHelpers.ts

@ -21,7 +21,7 @@ const booleanOptions = [
const aggBooleanOptions: any = booleanOptions.reduce((obj, o) => ({ ...obj, ...o }), {})
const getColVal = (row: any, col?: number) => {
return row && col ? row[col] : row
return row && col !== undefined ? row[col] : row
}
export const isCheckboxType: any = (values: [], col?: number) => {

31
packages/nc-gui/utils/projectCreateUtils.ts

@ -4,7 +4,7 @@ interface ProjectCreateForm {
title: string
dataSource: {
client: ClientType
connection: DefaultConnection | SQLiteConnection
connection: DefaultConnection | SQLiteConnection | SnowflakeConnection
searchPath?: string[]
}
inflection: {
@ -33,6 +33,15 @@ interface SQLiteConnection {
useNullAsDefault?: boolean
}
export interface SnowflakeConnection {
account: string
username: string
password: string
warehouse: string
database: string
schema: string
}
const defaultHost = 'localhost'
const testDataBaseNames = {
@ -45,7 +54,7 @@ const testDataBaseNames = {
}
export const getTestDatabaseName = (db: { client: ClientType; connection?: { database?: string } }) => {
if (db.client === ClientType.PG) return db.connection?.database
if (db.client === ClientType.PG || db.client === ClientType.SNOWFLAKE) return db.connection?.database
return testDataBaseNames[db.client as keyof typeof testDataBaseNames]
}
@ -66,12 +75,16 @@ export const clientTypes = [
text: 'SQLite',
value: ClientType.SQLITE,
},
{
text: 'SnowFlake',
value: ClientType.SNOWFLAKE,
},
]
const homeDir = ''
type ConnectionClientType =
| Exclude<ClientType, ClientType.SQLITE>
| Exclude<ClientType, ClientType.SQLITE | ClientType.SNOWFLAKE>
| 'tidb'
| 'yugabyte'
| 'citusdb'
@ -79,7 +92,9 @@ type ConnectionClientType =
| 'oracledb'
| 'greenplum'
const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection } & { [ClientType.SQLITE]: SQLiteConnection } = {
const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection } & { [ClientType.SQLITE]: SQLiteConnection } & {
[ClientType.SNOWFLAKE]: SnowflakeConnection
} = {
[ClientType.PG]: {
host: defaultHost,
port: '5432',
@ -116,6 +131,14 @@ const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection }
},
useNullAsDefault: true,
},
[ClientType.SNOWFLAKE]: {
account: 'account',
username: 'username',
password: 'password',
warehouse: 'warehouse',
database: 'database',
schema: 'schema',
},
tidb: {
host: defaultHost,
port: '4000',

18
packages/noco-docs/content/en/engineering/builds-and-releases.md

@ -6,7 +6,9 @@ category: "Engineering"
menuTitle: "Releases & Builds"
---
## Builds of NocoDB
There are 3 kinds of docker builds in NocoDB
- Release builds [nocodb/nocodb](https://hub.docker.com/r/nocodb/nocodb) : built during NocoDB release.
- Daily builds [nocodb/nocodb-daily](https://hub.docker.com/r/nocodb/nocodb-daily) : built every 6 hours from Develop branch.
- Daily builds [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely): built for every PR.
@ -26,12 +28,24 @@ Below is an overview of how to make these builds and what happens behind the sce
![image](https://user-images.githubusercontent.com/35857179/167240383-dda05f76-8323-4f4a-b3e7-9db886dbd68d.png)
- Then there would be two cases - you can either leave target tag and pervious tag blank or manually input some values
> Target Tag means the target deployment version, while Previous Tag means the latest version as of now. Previous Tag is used for Release Note only - showing the file / commit differences between two tags.
- Target Tag means the target deployment version, while Previous Tag means the latest version as of now. Previous Tag is used for Release Note only - showing the file / commit differences between two tags.
### Tagging
The naming convention would be following given the actual release tag is `0.100.0`
- `0.100.0-beta.1` (first version of pre-release)
- `0.100.0-beta.2` (include bug fix changes on top of the previous version)
- `0.100.0-beta.3`(include bug fix changes on top of the previous version)
- and so on ...
- `0.100.0` (actual release)
- `0.100.1` (minor bug fix release)
- `0.100.2` (minor bug fix release)
### Case 1: Leaving inputs blank
- If Previous Tag is blank, then the value will be fetched from [latest](https://github.com/nocodb/nocodb/releases/latest)
- If Target Tag is blank, then the value will be Previous Tag plus one. Example: 0.90.11 (Previous Tag) + 1 = 0.90.12 (Target Tag)
- If Target Tag is blank, then the value will be Previous Tag plus one. Example: 0.90.11 (Previous Tag) + 0.0.1 = 0.90.12 (Target Tag)
### Case 2: Manually Input

20
packages/noco-docs/content/en/getting-started/installation.md

@ -14,18 +14,6 @@ Simple installation - takes about three minutes!
## Quick try
### Heroku
Before doing so, make sure you have a Heroku account. By default, an add-on Heroku Postgres will be used as meta database. You can see the connection string defined in `DATABASE_URL` by navigating to Heroku App Settings and selecting Config Vars.
<a href="https://heroku.com/deploy?template=https://github.com/nocodb/nocodb-seed-heroku">
<img
src="https://www.herokucdn.com/deploy/button.svg"
width="300px"
alt="Deploy NocoDB to Heroku with 1-Click"
/>
</a>
### Docker
If you are a Docker user, you may try this way!
@ -466,7 +454,7 @@ It is mandatory to configure `NC_DB` environment variables for production usecas
| NC_DB | Yes | See our database URLs | A local SQLite will be created in root folder | |
| NC_DB_JSON | Yes | Can be used instead of `NC_DB` and value should be valid knex connection JSON | | |
| NC_DB_JSON_FILE | Yes | Can be used instead of `NC_DB` and value should be a valid path to knex connection JSON | | |
| DATABASE_URL | No | JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku deployment | | |
| DATABASE_URL | No | JDBC URL Format. Can be used instead of NC_DB. | | |
| DATABASE_URL_FILE | No | Can be used instead of DATABASE_URL: path to file containing JDBC URL Format. | | |
| NC_AUTH_JWT_SECRET | Yes | JWT secret used for auth and storing other secrets | A Random secret will be generated | |
| PORT | No | For setting app running port | `8080` | |
@ -486,7 +474,6 @@ It is mandatory to configure `NC_DB` environment variables for production usecas
| NC_GOOGLE_CLIENT_ID | No | Google client id to enable google authentication | | |
| NC_GOOGLE_CLIENT_SECRET | No | Google client secret to enable google authentication | | |
| NC_MIGRATIONS_DISABLED | No | Disable NocoDB migration | | |
| NC_ONE_CLICK | No | Used for Heroku one-click deployment | | |
| NC_MIN | No | If set to any non-empty string the default splash screen(initial welcome animation) and matrix screensaver will disable | | |
| NC_SENTRY_DSN | No | For Sentry monitoring | | |
| NC_REDIS_URL | No | Custom Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data will be stored in memory | |
@ -526,7 +513,4 @@ It is mandatory to configure `NC_DB` environment variables for production usecas
### Using NPX
<youtube id="v6Nn75P1p7I"></youtube>
### Heroku Deployment
<youtube id="WB7yYXfhocY"></youtube>
<youtube id="v6Nn75P1p7I"></youtube>

40
packages/noco-docs/content/en/getting-started/upgrading.md

@ -1,6 +1,6 @@
---
title: 'Upgrading'
description: 'Upgrading NocoDB : Docker, Node, Heroku and Homebrew!'
description: 'Upgrading NocoDB : Docker, Node and Homebrew!'
position: 20
category: 'Getting started'
menuTitle: 'Upgrading'
@ -60,44 +60,6 @@ npm uninstall nocodb
npm install --save nocodb
```
## Heroku
### Using the Heroku CLI login
```bash
heroku container:login
docker pull nocodb/nocodb:latest
docker tag nocodb/nocodb:latest registry.heroku.com/<HEROKU_APP_NAME>/web
docker push registry.heroku.com/<HEROKU_APP_NAME>/web
heroku container:release -a <HEROKU_APP_NAME> web
```
#### On Apple M1 Chipset
> Please make sure you change Docker's default architecture to `linux/amd64` by running the following command _before_ executing the aforementioned steps
>
> ```export DOCKER_DEFAULT_PLATFORM=linux/amd64```
>
> More details can be found [here](https://medium.com/geekculture/from-apple-silicon-to-heroku-docker-registry-without-swearing-36a2f59b30a3).
### Using GitHub
Fork the [nocodb-seed-heroku repository](https://github.com/nocodb/nocodb-seed-heroku) to your GitHub account.
Login to Heroku, go to your NocoDB app, and head to the "Deploy" tab.
Select "GitHub" in the "Deployment method" section.
In the "Connect to GitHub" section, search for your forked nocodb-seed-heroku repo. Connect to it:
![image](https://user-images.githubusercontent.com/55474996/143479577-e8bdc1f0-99d1-4072-8d95-4879cc54ddb2.png)
In the "Automatic deploys" section, select "Enable Automatic Deploys":
![image](https://user-images.githubusercontent.com/55474996/143479705-b5280199-aa31-40db-a5aa-7586eb918c01.png)
Head back to your forked nocodb-seed-heroku repo on your GitHub account. Edit one of your files and make a simple modification (example, add some random characters to the readme.md) and commit the change directly to the main branch.
This will trigger the Heroku deployment. Your app should now be updated to the latest release of NocoDB.
## Homebrew
Run following commands to upgrade Homebrew Nocodb version.

2
packages/noco-docs/content/en/setup-and-usages/formulas.md

@ -97,6 +97,8 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing {DATE_COL_1} is 2017-08-25 and {DATE_COL_2} is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |

5
packages/nocodb-sdk/src/lib/Api.ts

@ -124,6 +124,7 @@ export interface TableType {
columnsById?: object;
slug?: string;
mm?: boolean | number;
meta?: any;
}
export interface ViewType {
@ -134,6 +135,7 @@ export interface ViewType {
fk_model_id?: string;
slug?: string;
uuid?: string;
meta?: any;
show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal';
type?: number;
@ -175,6 +177,7 @@ export interface TableReqType {
order?: number;
mm?: boolean;
columns: ColumnType[];
meta?: any;
}
export interface TableListType {
@ -2159,6 +2162,7 @@ export class Api<
table_name?: string;
title?: string;
project_id?: string;
meta?: any;
},
params: RequestParams = {}
) =>
@ -2313,6 +2317,7 @@ export class Api<
viewId: string,
data: {
order?: number;
meta?: any;
title?: string;
show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal';

1
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -132,6 +132,7 @@ export function jsepTreeToFormula(node) {
'AVG',
'ADD',
'DATEADD',
'DATETIME_DIFF',
'WEEKDAY',
'AND',
'OR',

1015
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

File diff suppressed because it is too large Load Diff

5
packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts

@ -5,6 +5,7 @@ import { MysqlUi } from './MysqlUi';
import { OracleUi } from './OracleUi';
import { PgUi } from './PgUi';
import { SqliteUi } from './SqliteUi';
import { SnowflakeUi } from './SnowflakeUi';
// import {YugabyteUi} from "./YugabyteUi";
// import {TidbUi} from "./TidbUi";
@ -42,6 +43,10 @@ export class SqlUiFactory {
return PgUi;
}
if (connectionConfig.client === 'snowflake') {
return SnowflakeUi;
}
throw new Error('Database not supported');
}
}

1
packages/nocodb-sdk/src/lib/sqlUi/index.ts

@ -5,4 +5,5 @@ export * from './PgUi';
export * from './MssqlUi';
export * from './OracleUi';
export * from './SqliteUi';
export * from './SnowflakeUi';
export * from './SqlUiFactory';

9
packages/nocodb/README.md

@ -4,19 +4,18 @@
Even though this package is a backend project, you can still visit the dashboard as it includes ``nc-lib-gui``.
```
```sh
npm install
npm run watch:run
# open localhost:8080/dashboard in browser
```
As ``nc-lib-gui`` is hosted in npm registry, for local development, you should run ``nc-gui`` separately.
As ``nc-lib-gui`` is hosted in the npm registry, for local development, you should run ``nc-gui`` separately.
If you wish to combine the frontend and backend together in your local devlopment environment, you may use ``packages/nc-lib-gui`` as a local depenedency by updating the ``packages/nocodb/package.json`` to
If you wish to combine the frontend and backend together in your local development environment, you may use ``packages/nc-lib-gui`` as a local dependency by updating the ``packages/nocodb/package.json`` to
```json
"nc-lib-gui": "file:../nc-lib-gui"
```
In this case, whenever there is any changes made in frontend, you need to run ``npm run build:copy`` under ``packages/nc-gui/``.
In this case, whenever there are any changes made in the frontend, you need to run ``npm run build:copy`` under ``packages/nc-gui/``.

2070
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

5
packages/nocodb/package.json

@ -63,6 +63,7 @@
"bullmq": "^1.81.1",
"clear": "^0.1.0",
"colors": "1.4.0",
"compare-versions": "^5.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cron": "^1.8.2",
@ -92,7 +93,7 @@
"jsep": "^1.3.6",
"jsonfile": "^6.1.0",
"jsonwebtoken": "^8.5.1",
"knex": "^2.2.0",
"knex": "2.2.0",
"lodash": "^4.17.19",
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
@ -103,7 +104,7 @@
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nanoid": "^3.1.20",
"nc-help": "0.2.79",
"nc-help": "0.2.85",
"nc-lib-gui": "0.100.2",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",

5
packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts

@ -6,6 +6,8 @@ import PgClient from './pg/PgClient';
import YugabyteClient from './pg/YugabyteClient';
import TidbClient from './mysql/TidbClient';
import VitessClient from './mysql/VitessClient';
import SfClient from './snowflake/SnowflakeClient';
import { SnowflakeClient } from 'nc-help';
class SqlClientFactory {
static create(connectionConfig) {
@ -31,6 +33,9 @@ class SqlClientFactory {
if (connectionConfig.meta.dbtype === 'yugabyte')
return new YugabyteClient(connectionConfig);
return new PgClient(connectionConfig);
} else if (connectionConfig.client === 'snowflake') {
connectionConfig.client = SnowflakeClient;
return new SfClient(connectionConfig);
}
throw new Error('Database not supported');

2609
packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts

File diff suppressed because it is too large Load Diff

5
packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts

@ -0,0 +1,5 @@
// Snowflake queries
const sfQueries = {};
export default sfQueries;

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

@ -101,10 +101,9 @@ class BaseModelSqlv2 {
qb.where(_wherePk(this.model.primaryKeys, id));
let data = (await this.extractRawQueryAndExec(qb))?.[0];
let data = (await this.execAndParse(qb))?.[0];
if (data) {
data = this.convertAttachmentType(data);
const proto = await this.getProto();
data.__proto__ = proto;
}
@ -163,7 +162,6 @@ class BaseModelSqlv2 {
let data = await qb.first();
if (data) {
data = this.convertAttachmentType(data);
const proto = await this.getProto();
data.__proto__ = proto;
}
@ -255,8 +253,7 @@ class BaseModelSqlv2 {
if (!ignoreViewFilterAndSort) applyPaginate(qb, rest);
const proto = await this.getProto();
let data = await this.extractRawQueryAndExec(qb);
data = this.convertAttachmentType(data);
let data = await this.execAndParse(qb);
return data?.map((d) => {
d.__proto__ = proto;
@ -324,7 +321,7 @@ class BaseModelSqlv2 {
as: 'count',
}).first();
const res = (await this.dbDriver.raw(unsanitize(qb.toQuery()))) as any;
return (this.isPg ? res.rows[0] : res[0][0] ?? res[0]).count;
return ((this.isPg || this.isSnowflake) ? res.rows[0] : res[0][0] ?? res[0]).count;
}
// todo: add support for sortArrJson and filterArrJson
@ -368,7 +365,7 @@ class BaseModelSqlv2 {
qb.groupBy(args.column_name);
if (sorts) await sortV2(sorts, qb, this.dbDriver);
applyPaginate(qb, rest);
return this.convertAttachmentType(await qb);
return await qb;
}
async multipleHmList({ colId, ids }, args: { limit?; offset? } = {}) {
@ -427,8 +424,7 @@ class BaseModelSqlv2 {
.as('list')
);
let children = await this.extractRawQueryAndExec(childQb);
children = this.convertAttachmentType(children);
let children = await this.execAndParse(childQb, childTable);
const proto = await (
await Model.getBaseModelSQL({
id: childTable.id,
@ -555,8 +551,7 @@ class BaseModelSqlv2 {
await childModel.selectObject({ qb });
let children = await this.extractRawQueryAndExec(qb);
children = this.convertAttachmentType(children);
let children = await this.execAndParse(qb, childTable);
const proto = await (
await Model.getBaseModelSQL({
@ -673,11 +668,10 @@ class BaseModelSqlv2 {
!this.isSqlite
);
let children = await this.extractRawQueryAndExec(finalQb);
let children = await this.execAndParse(finalQb, childTable);
if (this.isMySQL) {
children = children[0];
}
children = this.convertAttachmentType(children);
const proto = await (
await Model.getBaseModelSQL({
id: rtnId,
@ -742,8 +736,7 @@ class BaseModelSqlv2 {
qb.limit(+rest?.limit || 25);
qb.offset(+rest?.offset || 0);
let children = await this.extractRawQueryAndExec(qb);
children = this.convertAttachmentType(children);
let children = await this.execAndParse(qb, childTable);
const proto = await (
await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver })
).getProto();
@ -970,7 +963,6 @@ class BaseModelSqlv2 {
const proto = await childModel.getProto();
let data = await qb;
data = this.convertAttachmentType(data);
return data.map((c) => {
c.__proto__ = proto;
return c;
@ -1084,8 +1076,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await childModel.getProto();
let data = await this.extractRawQueryAndExec(qb);
data = this.convertAttachmentType(data);
let data = await this.execAndParse(qb, childTable);
return data.map((c) => {
c.__proto__ = proto;
@ -1203,8 +1194,7 @@ class BaseModelSqlv2 {
applyPaginate(qb, rest);
const proto = await parentModel.getProto();
let data = await this.extractRawQueryAndExec(qb);
data = this.convertAttachmentType(data);
let data = await this.execAndParse(qb, childTable);
return data.map((c) => {
c.__proto__ = proto;
@ -1568,7 +1558,7 @@ class BaseModelSqlv2 {
query.returning(
`${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`
);
response = await this.extractRawQueryAndExec(query);
response = await this.execAndParse(query);
}
const ai = this.model.columns.find((c) => c.ai);
@ -1578,7 +1568,7 @@ class BaseModelSqlv2 {
// handle if autogenerated primary key is used
if (ag) {
if (!response) await this.extractRawQueryAndExec(query);
if (!response) await this.execAndParse(query);
response = await this.readByPk(data[ag.title]);
} else if (
!response ||
@ -1588,7 +1578,7 @@ class BaseModelSqlv2 {
if (response?.length) {
id = response[0];
} else {
const res = await this.extractRawQueryAndExec(query);
const res = await this.execAndParse(query);
id = res?.id ?? res[0]?.insertId;
}
@ -1600,6 +1590,11 @@ class BaseModelSqlv2 {
.select(ai.column_name)
.max(ai.column_name, { as: 'id' })
)[0].id;
} else if (this.isSnowflake) {
id = ((
await this.dbDriver(this.tnPath)
.max(ai.column_name, { as: 'id' })
) as any)[0].id;
}
response = await this.readByPk(id);
} else {
@ -1693,7 +1688,7 @@ class BaseModelSqlv2 {
.update(updateObj)
.where(await this._wherePk(id));
await this.extractRawQueryAndExec(query);
await this.execAndParse(query);
const response = await this.readByPk(id);
await this.afterUpdate(response, trx, cookie);
@ -1712,11 +1707,13 @@ class BaseModelSqlv2 {
private getTnPath(tb: Model) {
const schema = (this.dbDriver as any).searchPath?.();
const table =
this.isMssql && schema
? this.dbDriver.raw('??.??', [schema, tb.table_name])
: tb.table_name;
return table;
if (this.isMssql && schema) {
return this.dbDriver.raw('??.??', [schema, tb.table_name]);
} else if (this.isSnowflake) {
return [this.dbDriver.client.config.connection.database, this.dbDriver.client.config.connection.schema, tb.table_name].join('.');
} else {
return tb.table_name;
}
}
public get tnPath() {
@ -1739,6 +1736,10 @@ class BaseModelSqlv2 {
return this.clientType === 'mysql2' || this.clientType === 'mysql';
}
get isSnowflake() {
return this.clientType === 'snowflake';
}
get clientType() {
return this.dbDriver.clientType();
}
@ -1841,7 +1842,19 @@ class BaseModelSqlv2 {
}
if (ai) {
// response = await this.readByPk(id)
if (this.isSqlite) {
// sqlite doesnt return id after insert
id = (
await this.dbDriver(this.tnPath)
.select(ai.column_name)
.max(ai.column_name, { as: 'id' })
)[0].id;
} else if (this.isSnowflake) {
id = ((
await this.dbDriver(this.tnPath)
.max(ai.column_name, { as: 'id' })
) as any).rows[0].id;
}
response = await this.readByPk(id);
} else {
response = data;
@ -1914,10 +1927,10 @@ class BaseModelSqlv2 {
const response =
this.isPg || this.isMssql
? await this.dbDriver
.batchInsert(this.model.table_name, insertDatas, chunkSize)
.batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name)
: await this.dbDriver.batchInsert(
this.model.table_name,
this.tnPath,
insertDatas,
chunkSize
);
@ -1950,7 +1963,7 @@ class BaseModelSqlv2 {
continue;
}
const wherePk = await this._wherePk(pkValues);
const response = await transaction(this.model.table_name)
const response = await transaction(this.tnPath)
.update(d)
.where(wherePk);
res.push(response);
@ -2020,13 +2033,17 @@ class BaseModelSqlv2 {
async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) {
let transaction;
try {
const deleteIds = await Promise.all(
ids.map((d) => this.model.mapAliasToColumn(d))
);
transaction = await this.dbDriver.transaction();
// await this.beforeDeleteb(ids, transaction);
const res = [];
for (const d of ids) {
for (const d of deleteIds) {
if (Object.keys(d).length) {
const response = await transaction(this.model.table_name)
const response = await transaction(this.tnPath)
.del()
.where(d);
res.push(response);
@ -2276,7 +2293,7 @@ class BaseModelSqlv2 {
subject: 'NocoDB Form',
html: ejs.render(formSubmissionEmailTemplate, {
data: transformedData,
tn: this.model.table_name,
tn: this.tnPath,
_tn: this.model.title,
}),
});
@ -2398,16 +2415,34 @@ class BaseModelSqlv2 {
const vTn = this.getTnPath(vTable);
await this.dbDriver(vTn).insert({
[vParentCol.column_name]: this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId))
.first(),
[vChildCol.column_name]: this.dbDriver(childTn)
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first(),
});
if (this.isSnowflake) {
const parentPK = this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId))
.first();
const childPK = this.dbDriver(childTn)
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first();
await this.dbDriver.raw(`INSERT INTO ?? (??, ??) SELECT (${parentPK.toQuery()}), (${childPK.toQuery()})`, [
vTn,
vParentCol.column_name,
vChildCol.column_name,
])
} else {
await this.dbDriver(vTn).insert({
[vParentCol.column_name]: this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId))
.first(),
[vChildCol.column_name]: this.dbDriver(childTn)
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first(),
});
}
}
break;
case RelationTypes.HAS_MANY:
@ -2595,7 +2630,7 @@ class BaseModelSqlv2 {
} else {
groupingValues = new Set(
(
await this.dbDriver(this.model.table_name)
await this.dbDriver(this.tnPath)
.select(column.column_name)
.distinct()
).map((row) => row[column.column_name])
@ -2603,7 +2638,7 @@ class BaseModelSqlv2 {
groupingValues.add(null);
}
const qb = this.dbDriver(this.model.table_name);
const qb = this.dbDriver(this.tnPath);
qb.limit(+rest?.limit || 25);
qb.offset(+rest?.offset || 0);
@ -2697,7 +2732,6 @@ class BaseModelSqlv2 {
const proto = await this.getProto();
let data = await groupedQb;
data = this.convertAttachmentType(data);
const result = data?.map((d) => {
d.__proto__ = proto;
return d;
@ -2742,7 +2776,7 @@ class BaseModelSqlv2 {
if (isVirtualCol(column))
NcError.notImplemented('Grouping for virtual columns not implemented');
const qb = this.dbDriver(this.model.table_name)
const qb = this.dbDriver(this.tnPath)
.count('*', { as: 'count' })
.groupBy(column.column_name);
@ -2800,38 +2834,51 @@ class BaseModelSqlv2 {
return await qb;
}
private async extractRawQueryAndExec(qb: Knex.QueryBuilder) {
private async execAndParse(
qb: Knex.QueryBuilder,
childTable?: Model
) {
let query = qb.toQuery();
if (!this.isPg && !this.isMssql) {
if (!this.isPg && !this.isMssql && !this.isSnowflake) {
query = unsanitize(qb.toQuery());
} else {
query = sanitize(query);
}
return this.isPg
? (await this.dbDriver.raw(query))?.rows
: query.slice(0, 6) === 'select' && !this.isMssql
? await this.dbDriver.from(
this.dbDriver.raw(query).wrap('(', ') __nc_alias')
)
: await this.dbDriver.raw(query);
return this.convertAttachmentType(
this.isPg || this.isSnowflake
? (await this.dbDriver.raw(query))?.rows
: query.slice(0, 6) === 'select' && !this.isMssql
? await this.dbDriver.from(
this.dbDriver.raw(query).wrap('(', ') __nc_alias')
)
: await this.dbDriver.raw(query),
childTable
);
}
private _convertAttachmentType(attachmentColumns, d) {
attachmentColumns.forEach((col) => {
if (d[col.title] && typeof d[col.title] === 'string') {
d[col.title] = JSON.parse(d[col.title]);
private _convertAttachmentType(
attachmentColumns: Record<string, any>[],
d: Record<string, any>
) {
try {
if (d) {
attachmentColumns.forEach((col) => {
if (d[col.title] && typeof d[col.title] === 'string') {
d[col.title] = JSON.parse(d[col.title]);
}
});
}
});
} catch {}
return d;
}
private convertAttachmentType(data) {
private convertAttachmentType(data: Record<string, any>, childTable?: Model) {
// 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
);
const attachmentColumns = (
childTable ? childTable.columns : this.model.columns
).filter((c) => c.uidt === UITypes.Attachment);
if (attachmentColumns.length) {
if (Array.isArray(data)) {
data = data.map((d) =>

7
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts

@ -1,4 +1,5 @@
import { Knex, knex } from 'knex';
import { SnowflakeClient } from 'nc-help';
const types = require('pg').types;
// override parsing date column to Date()
@ -993,6 +994,8 @@ function CustomKnex(arg: string | Knex.Config<any> | any): CustomKnex {
arg.useNullAsDefault = true;
}
if (arg?.client === 'snowflake') arg.client = SnowflakeClient;
const kn: any = knex(arg);
const knexRaw = kn.raw;
@ -1019,7 +1022,9 @@ function CustomKnex(arg: string | Knex.Config<any> | any): CustomKnex {
value: () => {
return typeof arg === 'string'
? arg.match(/^(\w+):/) ?? [1]
: arg.client;
: (typeof arg.client === 'string')
? arg.client
: arg.client?.prototype?.dialect || arg.client?.prototype?.driverName;
},
},
searchPath: {

86
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts

@ -8,6 +8,7 @@ import { XKnex } from '../../../index';
import LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn';
import LookupColumn from '../../../../../models/LookupColumn';
import { jsepCurlyHook, UITypes } from 'nocodb-sdk';
import { validateDateWithUnknownFormat } from '../helpers/formulaFnHelper';
// todo: switch function based on database
@ -55,8 +56,11 @@ export default async function formulaQueryBuilderv2(
jsep.plugins.register(jsepCurlyHook);
const tree = jsep(_tree);
const columnIdToUidt = {};
// todo: improve - implement a common solution for filter, sort, formula, etc
for (const col of await model.getColumns()) {
columnIdToUidt[col.id] = col.uidt;
if (col.id in aliasToColumn) continue;
switch (col.uidt) {
case UITypes.Formula:
@ -659,11 +663,93 @@ export default async function formulaQueryBuilderv2(
const right = fn(pt.right, null, pt.operator).toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw
// `ERROR: zero-length delimited identifier` in Postgres
if (
knex.clientType() === 'pg' &&
columnIdToUidt[pt.left.name] === UITypes.Date
) {
// The correct way to compare with Date should be using
// `IS_AFTER`, `IS_BEFORE`, or `IS_SAME`
// This is to prevent empty data returned to UI due to incorrect SQL
if (pt.right.value === '') {
if (pt.operator === '=') {
sql = `${left} IS NULL ${colAlias}`;
} else {
sql = `${left} IS NOT NULL ${colAlias}`;
}
} else if (!validateDateWithUnknownFormat(pt.right.value)) {
// left tree value is date but right tree value is not date
// return true if left tree value is not null, else false
sql = `${left} IS NOT NULL ${colAlias}`;
}
}
if (
knex.clientType() === 'pg' &&
columnIdToUidt[pt.right.name] === UITypes.Date
) {
// The correct way to compare with Date should be using
// `IS_AFTER`, `IS_BEFORE`, or `IS_SAME`
// This is to prevent empty data returned to UI due to incorrect SQL
if (pt.left.value === '') {
if (pt.operator === '=') {
sql = `${right} IS NULL ${colAlias}`;
} else {
sql = `${right} IS NOT NULL ${colAlias}`;
}
} else if (!validateDateWithUnknownFormat(pt.left.value)) {
// right tree value is date but left tree value is not date
// return true if right tree value is not null, else false
sql = `${right} IS NOT NULL ${colAlias}`;
}
}
// handle NULL values when calling CONCAT for sqlite3
if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') {
sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`;
}
if (knex.clientType() === 'mysql2') {
sql = `IFNULL(${left} ${pt.operator} ${right}, ${
pt.operator === '='
? pt.left.type === 'Literal'
? pt.left.value === ''
: pt.right.value === ''
: pt.operator === '!='
? pt.left.type !== 'Literal'
? pt.left.value === ''
: pt.right.value === ''
: 0
}) ${colAlias}`;
} else if (
knex.clientType() === 'sqlite3' ||
knex.clientType() === 'pg' ||
knex.clientType() === 'mssql'
) {
if (pt.operator === '=') {
if (pt.left.type === 'Literal' && pt.left.value === '') {
sql = `${right} IS NULL OR CAST(${right} AS TEXT) = ''`;
} else if (pt.right.type === 'Literal' && pt.right.value === '') {
sql = `${left} IS NULL OR CAST(${left} AS TEXT) = ''`;
}
} else if (pt.operator === '!=') {
if (pt.left.type === 'Literal' && pt.left.value === '') {
sql = `${right} IS NOT NULL AND CAST(${right} AS TEXT) != ''`;
} else if (pt.right.type === 'Literal' && pt.right.value === '') {
sql = `${left} IS NOT NULL AND CAST(${left} AS TEXT) != ''`;
}
}
if (
(pt.operator === '=' || pt.operator === '!=') &&
prevBinaryOp !== 'AND' &&
prevBinaryOp !== 'OR'
) {
sql = `(CASE WHEN ${sql} THEN true ELSE false END ${colAlias})`;
} else {
sql = `${sql} ${colAlias}`;
}
}
const query = knex.raw(sql);
if (prevBinaryOp && pt.operator !== prevBinaryOp) {
query.wrap('(', ')');

36
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts

@ -1,6 +1,7 @@
import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
const mssql = {
@ -110,6 +111,17 @@ const mssql = {
END${colAlias}`
);
},
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
const datetime_expr2 = fn(pt.arguments[1]);
const rawUnit = pt.arguments[2]
? fn(pt.arguments[2]).bindings[0]
: 'seconds';
const unit = convertUnits(rawUnit, 'mssql');
return knex.raw(
`DATEDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// DATEPART(WEEKDAY, DATE): sunday = 1, monday = 2, ..., saturday = 7
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
@ -123,6 +135,30 @@ const mssql = {
)} % 7 + 7) % 7 ${colAlias}`
);
},
AND: (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map((ar) => args.fn(ar, '', 'AND').toQuery())
.join(' AND ')}`
)
.wrap('(', ')')
.toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
);
},
OR: (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map((ar) => args.fn(ar, '', 'OR').toQuery())
.join(' OR ')}`
)
.wrap('(', ')')
.toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
);
},
};
export default mssql;

21
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts

@ -1,6 +1,7 @@
import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
const mysql2 = {
@ -61,6 +62,26 @@ const mysql2 = {
END${colAlias}`
);
},
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
const datetime_expr2 = fn(pt.arguments[1]);
const unit = convertUnits(
pt.arguments[2] ? fn(pt.arguments[2]).bindings[0] : 'seconds',
'mysql'
);
if (unit === 'MICROSECOND') {
// MySQL doesn't support millisecond
// hence change from MICROSECOND to millisecond manually
return knex.raw(
`TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) div 1000 ${colAlias}`
);
}
return knex.raw(
`TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
return knex.raw(

75
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts

@ -1,6 +1,7 @@
import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
const pg = {
@ -50,6 +51,56 @@ const pg = {
)}')::interval${colAlias}`
);
},
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
const datetime_expr2 = fn(pt.arguments[1]);
const rawUnit = pt.arguments[2]
? fn(pt.arguments[2]).bindings[0]
: 'seconds';
let sql;
const unit = convertUnits(rawUnit, 'pg');
switch (unit) {
case 'second':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER`;
break;
case 'minute':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER / 60`;
break;
case 'milliseconds':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER * 1000`;
break;
case 'hour':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER / 3600`;
break;
case 'week':
sql = `TRUNC(DATE_PART('day', ${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP) / 7)`;
break;
case 'month':
sql = `(
DATE_PART('year', ${datetime_expr1}::TIMESTAMP) -
DATE_PART('year', ${datetime_expr2}::TIMESTAMP)
) * 12 + (
DATE_PART('month', ${datetime_expr1}::TIMESTAMP) -
DATE_PART('month', ${datetime_expr2}::TIMESTAMP)
)`;
break;
case 'quarter':
sql = `((EXTRACT(QUARTER FROM ${datetime_expr1}::TIMESTAMP) +
DATE_PART('year', AGE(${datetime_expr1}, '1900/01/01')) * 4) - 1) -
((EXTRACT(QUARTER FROM ${datetime_expr2}::TIMESTAMP) +
DATE_PART('year', AGE(${datetime_expr2}, '1900/01/01')) * 4) - 1)`;
break;
case 'year':
sql = `DATE_PART('year', ${datetime_expr1}::TIMESTAMP) - DATE_PART('year', ${datetime_expr2}::TIMESTAMP)`;
break;
case 'day':
sql = `DATE_PART('day', ${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP)`;
break;
default:
sql = '';
}
return knex.raw(`${sql} ${colAlias}`);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// isodow: the day of the week as Monday (1) to Sunday (7)
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
@ -63,6 +114,30 @@ const pg = {
)} % 7 + 7) ::INTEGER % 7 ${colAlias}`
);
},
AND: (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map((ar) => args.fn(ar, '', 'AND').toQuery())
.join(' AND ')}`
)
.wrap('(', ')')
.toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
);
},
OR: (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map((ar) => args.fn(ar, '', 'OR').toQuery())
.join(' OR ')}`
)
.wrap('(', ')')
.toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
);
},
};
export default pg;

87
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts

@ -1,7 +1,12 @@
import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper';
import {
convertToTargetFormat,
getDateFormat,
} from '../../../../../utils/dateTimeUtils';
const sqlite3 = {
...commonFns,
@ -77,7 +82,63 @@ const sqlite3 = {
END${colAlias}`
);
},
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
let datetime_expr1 = fn(pt.arguments[0]).bindings[0];
let datetime_expr2 = fn(pt.arguments[1]).bindings[0];
// JULIANDAY takes YYYY-MM-DD
datetime_expr1 = convertToTargetFormat(
datetime_expr1,
getDateFormat(datetime_expr1),
'YYYY-MM-DD'
);
datetime_expr2 = convertToTargetFormat(
datetime_expr2,
getDateFormat(datetime_expr2),
'YYYY-MM-DD'
);
const rawUnit = pt.arguments[2]
? fn(pt.arguments[2]).bindings[0]
: 'seconds';
let sql;
const unit = convertUnits(rawUnit, 'sqlite');
switch (unit) {
case 'seconds':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 86400)`;
break;
case 'minutes':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 1440)`;
break;
case 'hours':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 24)`;
break;
case 'milliseconds':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 86400000)`;
break;
case 'weeks':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 7)`;
break;
case 'months':
sql = `(ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365))
* 12 + (ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365 / 12))`;
break;
case 'quarters':
sql = `
ROUND((JULIANDAY('${datetime_expr1}')) / 365 / 4) -
ROUND((JULIANDAY('${datetime_expr2}')) / 365 / 4) +
(ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365)) * 4
`;
break;
case 'years':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365)`;
break;
case 'days':
sql = `JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')`;
break;
default:
sql = '';
}
return knex.raw(`${sql} ${colAlias}`);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// strftime('%w', date) - day of week 0 - 6 with Sunday == 0
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
@ -91,6 +152,30 @@ const sqlite3 = {
)} % 7 + 7) % 7 ${colAlias}`
);
},
AND: (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map((ar) => args.fn(ar, '', 'AND').toQuery())
.join(' AND ')}`
)
.wrap('(', ')')
.toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
);
},
OR: (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map((ar) => args.fn(ar, '', 'OR').toQuery())
.join(' OR ')}`
)
.wrap('(', ')')
.toQuery()} THEN 1 ELSE 0 END ${args.colAlias}`
);
},
};
export default sqlite3;

137
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts

@ -0,0 +1,137 @@
export function convertUnits(
unit: string,
type: 'mysql' | 'mssql' | 'pg' | 'sqlite'
) {
switch (unit) {
case 'milliseconds':
case 'ms': {
switch (type) {
case 'mssql':
return 'millisecond';
case 'mysql':
// MySQL doesn't support millisecond
// hence change from MICROSECOND to millisecond manually
return 'MICROSECOND';
case 'pg':
case 'sqlite':
return 'milliseconds';
default:
return unit;
}
}
case 'seconds':
case 's': {
switch (type) {
case 'mssql':
case 'pg':
return 'second';
case 'mysql':
return 'SECOND';
case 'sqlite':
return 'seconds';
default:
return unit;
}
}
case 'minutes':
case 'm': {
switch (type) {
case 'mssql':
case 'pg':
return 'minute';
case 'mysql':
return 'MINUTE';
case 'sqlite':
return 'minutes';
default:
return unit;
}
}
case 'hours':
case 'h': {
switch (type) {
case 'mssql':
case 'pg':
return 'hour';
case 'mysql':
return 'HOUR';
case 'sqlite':
return 'hours';
default:
return unit;
}
}
case 'days':
case 'd': {
switch (type) {
case 'mssql':
case 'pg':
return 'day';
case 'mysql':
return 'DAY';
case 'sqlite':
return 'days';
default:
return unit;
}
}
case 'weeks':
case 'w': {
switch (type) {
case 'mssql':
case 'pg':
return 'week';
case 'mysql':
return 'WEEK';
case 'sqlite':
return 'weeks';
default:
return unit;
}
}
case 'months':
case 'M': {
switch (type) {
case 'mssql':
case 'pg':
return 'month';
case 'mysql':
return 'MONTH';
case 'sqlite':
return 'months';
default:
return unit;
}
}
case 'quarters':
case 'Q': {
switch (type) {
case 'mssql':
case 'pg':
return 'quarter';
case 'mysql':
return 'QUARTER';
case 'sqlite':
return 'quarters';
default:
return unit;
}
}
case 'years':
case 'y': {
switch (type) {
case 'mssql':
case 'pg':
return 'year';
case 'mysql':
return 'YEAR';
case 'sqlite':
return 'years';
default:
return unit;
}
}
default:
return unit;
}
}

29
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts

@ -1,3 +1,7 @@
import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
extend(customParseFormat);
export function getWeekdayByText(v: string) {
return {
monday: 0,
@ -21,3 +25,28 @@ export function getWeekdayByIndex(idx: number): string {
6: 'sunday',
}[idx || 0];
}
export function validateDateWithUnknownFormat(v: string) {
const dateFormats = [
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}

3
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts

@ -4,6 +4,7 @@ import ModelXcMetaMysql from './ModelXcMetaMysql';
import ModelXcMetaOracle from './ModelXcMetaOracle';
import ModelXcMetaPg from './ModelXcMetaPg';
import ModelXcMetaSqlite from './ModelXcMetaSqlite';
import ModelXcMetaSnowflake from './ModelXcMetaSnowflake';
class ModelXcMetaFactory {
public static create(connectionConfig, args): BaseModelXcMeta {
@ -20,6 +21,8 @@ class ModelXcMetaFactory {
return new ModelXcMetaPg(args);
} else if (connectionConfig.client === 'oracledb') {
return new ModelXcMetaOracle(args);
} else if (connectionConfig.client === 'snowflake') {
return new ModelXcMetaSnowflake(args);
}
throw new Error('Database not supported');

975
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts

@ -0,0 +1,975 @@
import BaseModelXcMeta from './BaseModelXcMeta';
class ModelXcMetaSnowflake extends BaseModelXcMeta {
/**
* @param dir
* @param filename
* @param ctx
* @param ctx.tn
* @param ctx.columns
* @param ctx.relations
*/
constructor({ dir, filename, ctx }) {
super({ dir, filename, ctx });
}
/**
* Prepare variables used in code template
*/
prepare() {
const data: any = {};
/* run of simple variable */
data.tn = this.ctx.tn;
data.dbType = this.ctx.dbType;
/* for complex code provide a func and args - do derivation within the func cbk */
data.columns = {
func: this._renderXcColumns.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
relations: this.ctx.relations,
},
};
/* for complex code provide a func and args - do derivation within the func cbk */
data.hasMany = {
func: this._renderXcHasMany.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
hasMany: this.ctx.hasMany,
},
};
/* for complex code provide a func and args - do derivation within the func cbk */
data.belongsTo = {
func: this._renderXcBelongsTo.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
belongsTo: this.ctx.belongsTo,
},
};
return data;
}
_renderXcHasMany(args) {
return JSON.stringify(args.hasMany);
}
_renderXcBelongsTo(args) {
return JSON.stringify(args.belongsTo);
}
/**
*
* @param args
* @param args.columns
* @param args.relations
* @returns {string}
* @private
*/
_renderXcColumns(args) {
let str = '[\r\n';
for (let i = 0; i < args.columns.length; ++i) {
str += `{\r\n`;
str += `cn: '${args.columns[i].cn}',\r\n`;
str += `type: '${this._getAbstractType(args.columns[i])}',\r\n`;
str += `dt: '${args.columns[i].dt}',\r\n`;
if (args.columns[i].rqd) str += `rqd: ${args.columns[i].rqd},\r\n`;
if (args.columns[i].cdf) {
str += `default: "${args.columns[i].cdf}",\r\n`;
str += `columnDefault: "${args.columns[i].cdf}",\r\n`;
}
if (args.columns[i].un) str += `un: ${args.columns[i].un},\r\n`;
if (args.columns[i].pk) str += `pk: ${args.columns[i].pk},\r\n`;
if (args.columns[i].ai) str += `ai: ${args.columns[i].ai},\r\n`;
if (args.columns[i].dtxp) str += `dtxp: "${args.columns[i].dtxp}",\r\n`;
if (args.columns[i].dtxs) str += `dtxs: ${args.columns[i].dtxs},\r\n`;
str += `validate: {
func: [],
args: [],
msg: []
},`;
str += `},\r\n`;
}
str += ']\r\n';
return str;
}
_getAbstractType(column) {
let str = '';
switch (column.dt) {
case 'int':
str = 'integer';
break;
case 'integer':
str = 'integer';
break;
case 'bigint':
str = 'bigInteger';
break;
case 'bigserial':
str = 'bigserial';
break;
case 'char':
str = 'string';
break;
case 'int2':
str = 'integer';
break;
case 'int4':
str = 'integer';
break;
case 'int8':
str = 'integer';
break;
case 'int4range':
str = 'int4range';
break;
case 'int8range':
str = 'int8range';
break;
case 'serial':
str = 'serial';
break;
case 'serial2':
str = 'serial2';
break;
case 'serial8':
str = 'serial8';
break;
case 'character':
str = 'string';
break;
case 'bit':
str = 'bit';
break;
case 'bool':
str = 'boolean';
break;
case 'boolean':
str = 'boolean';
break;
case 'date':
str = 'date';
break;
case 'double precision':
str = 'double';
break;
case 'event_trigger':
str = 'event_trigger';
break;
case 'fdw_handler':
str = 'fdw_handler';
break;
case 'float4':
str = 'float';
break;
case 'float8':
str = 'float';
break;
case 'uuid':
str = 'uuid';
break;
case 'smallint':
str = 'integer';
break;
case 'smallserial':
str = 'smallserial';
break;
case 'character varying':
str = 'string';
break;
case 'text':
str = 'text';
break;
case 'real':
str = 'float';
break;
case 'time':
str = 'time';
break;
case 'time without time zone':
str = 'time';
break;
case 'timestamp':
str = 'timestamp';
break;
case 'timestamp without time zone':
str = 'timestamp';
break;
case 'timestamptz':
str = 'timestampt';
break;
case 'timestamp with time zone':
str = 'timestamp';
break;
case 'timetz':
str = 'time';
break;
case 'time with time zone':
str = 'time';
break;
case 'daterange':
str = 'daterange';
break;
case 'json':
str = 'json';
break;
case 'jsonb':
str = 'jsonb';
break;
case 'gtsvector':
str = 'gtsvector';
break;
case 'index_am_handler':
str = 'index_am_handler';
break;
case 'anyenum':
str = 'enum';
break;
case 'anynonarray':
str = 'anynonarray';
break;
case 'anyrange':
str = 'anyrange';
break;
case 'box':
str = 'box';
break;
case 'bpchar':
str = 'bpchar';
break;
case 'bytea':
str = 'bytea';
break;
case 'cid':
str = 'cid';
break;
case 'cidr':
str = 'cidr';
break;
case 'circle':
str = 'circle';
break;
case 'cstring':
str = 'cstring';
break;
case 'inet':
str = 'inet';
break;
case 'internal':
str = 'internal';
break;
case 'interval':
str = 'interval';
break;
case 'language_handler':
str = 'language_handler';
break;
case 'line':
str = 'line';
break;
case 'lsec':
str = 'lsec';
break;
case 'macaddr':
str = 'macaddr';
break;
case 'money':
str = 'float';
break;
case 'name':
str = 'name';
break;
case 'numeric':
str = 'numeric';
break;
case 'numrange':
str = 'numrange';
break;
case 'oid':
str = 'oid';
break;
case 'opaque':
str = 'opaque';
break;
case 'path':
str = 'path';
break;
case 'pg_ddl_command':
str = 'pg_ddl_command';
break;
case 'pg_lsn':
str = 'pg_lsn';
break;
case 'pg_node_tree':
str = 'pg_node_tree';
break;
case 'point':
str = 'point';
break;
case 'polygon':
str = 'polygon';
break;
case 'record':
str = 'record';
break;
case 'refcursor':
str = 'refcursor';
break;
case 'regclass':
str = 'regclass';
break;
case 'regconfig':
str = 'regconfig';
break;
case 'regdictionary':
str = 'regdictionary';
break;
case 'regnamespace':
str = 'regnamespace';
break;
case 'regoper':
str = 'regoper';
break;
case 'regoperator':
str = 'regoperator';
break;
case 'regproc':
str = 'regproc';
break;
case 'regpreocedure':
str = 'regpreocedure';
break;
case 'regrole':
str = 'regrole';
break;
case 'regtype':
str = 'regtype';
break;
case 'reltime':
str = 'reltime';
break;
case 'smgr':
str = 'smgr';
break;
case 'tid':
str = 'tid';
break;
case 'tinterval':
str = 'tinterval';
break;
case 'trigger':
str = 'trigger';
break;
case 'tsm_handler':
str = 'tsm_handler';
break;
case 'tsquery':
str = 'tsquery';
break;
case 'tsrange':
str = 'tsrange';
break;
case 'tstzrange':
str = 'tstzrange';
break;
case 'tsvector':
str = 'tsvector';
break;
case 'txid_snapshot':
str = 'txid_snapshot';
break;
case 'unknown':
str = 'unknown';
break;
case 'void':
str = 'void';
break;
case 'xid':
str = 'xid';
break;
case 'xml':
str = 'xml';
break;
default:
str += `"${column.dt}"`;
break;
}
return str;
}
getUIDataType(col): any {
switch (this.getAbstractType(col)) {
case 'integer':
return 'Number';
case 'boolean':
return 'Checkbox';
case 'float':
return 'Decimal';
case 'date':
return 'Date';
case 'datetime':
return 'DateTime';
case 'time':
return 'Time';
case 'year':
return 'Year';
case 'string':
return 'SingleLineText';
case 'text':
return 'LongText';
case 'enum':
return 'SingleSelect';
case 'set':
return 'MultiSelect';
case 'json':
return 'JSON';
case 'blob':
return 'LongText';
case 'geometry':
return 'Geometry';
default:
return 'SpecificDBType';
}
}
getAbstractType(col): any {
const dt = col.dt.toLowerCase();
switch (dt) {
case 'anyenum':
return 'enum';
case 'anynonarray':
case 'anyrange':
return dt;
case 'bit':
return 'integer';
case 'bigint':
case 'bigserial':
return 'integer';
case 'bool':
return 'boolean';
case 'bpchar':
case 'bytea':
return dt;
case 'char':
case 'character':
case 'character varying':
return 'string';
case 'cid':
case 'cidr':
case 'cstring':
return dt;
case 'date':
return 'date';
case 'daterange':
return 'string';
case 'double precision':
return 'string';
case 'event_trigger':
case 'fdw_handler':
return dt;
case 'float4':
case 'float8':
return 'float';
case 'gtsvector':
case 'index_am_handler':
case 'inet':
return dt;
case 'int':
case 'int2':
case 'int4':
case 'int8':
case 'integer':
return 'integer';
case 'int4range':
case 'int8range':
case 'internal':
case 'interval':
return 'string';
case 'json':
case 'jsonb':
return 'json';
case 'language_handler':
case 'lsec':
case 'macaddr':
case 'money':
case 'name':
case 'numeric':
case 'numrange':
case 'oid':
case 'opaque':
case 'path':
case 'pg_ddl_command':
case 'pg_lsn':
case 'pg_node_tree':
return dt;
case 'real':
return 'float';
case 'record':
case 'refcursor':
case 'regclass':
case 'regconfig':
case 'regdictionary':
case 'regnamespace':
case 'regoper':
case 'regoperator':
case 'regproc':
case 'regpreocedure':
case 'regrole':
case 'regtype':
case 'reltime':
return dt;
case 'serial':
case 'serial2':
case 'serial8':
case 'smallint':
case 'smallserial':
return 'integer';
case 'smgr':
return dt;
case 'text':
return 'text';
case 'tid':
return dt;
case 'time':
case 'time without time zone':
return 'time';
case 'timestamp':
case 'timestamp without time zone':
case 'timestamptz':
case 'timestamp with time zone':
return 'datetime';
case 'timetz':
case 'time with time zone':
return 'time';
case 'tinterval':
case 'trigger':
case 'tsm_handler':
case 'tsquery':
case 'tsrange':
case 'tstzrange':
case 'tsvector':
case 'txid_snapshot':
case 'unknown':
case 'void':
case 'xid':
case 'xml':
return dt;
case 'tinyint':
case 'mediumint':
return 'integer';
case 'float':
case 'decimal':
case 'double':
return 'float';
case 'boolean':
return 'boolean';
case 'datetime':
return 'datetime';
case 'uuid':
case 'year':
case 'varchar':
case 'nchar':
return 'string';
case 'tinytext':
case 'mediumtext':
case 'longtext':
return 'text';
case 'binary':
case 'varbinary':
return 'text';
case 'blob':
case 'tinyblob':
case 'mediumblob':
case 'longblob':
return 'blob';
case 'enum':
return 'enum';
case 'set':
return 'set';
case 'line':
case 'point':
case 'polygon':
case 'circle':
case 'box':
case 'geometry':
case 'linestring':
case 'multipoint':
case 'multilinestring':
case 'multipolygon':
return 'geometry';
}
}
_sequelizeGetType(column) {
let str = '';
switch (column.dt) {
case 'int':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'tinyint':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'smallint':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'mediumint':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'bigint':
str += `DataTypes.BIGINT`;
if (column.un) str += `.UNSIGNED`;
break;
case 'float':
str += `DataTypes.FLOAT`;
break;
case 'decimal':
str += `DataTypes.DECIMAL`;
break;
case 'double':
str += `"DOUBLE(${column.dtxp},${column.ns})"`;
break;
case 'real':
str += `DataTypes.FLOAT`;
break;
case 'bit':
str += `DataTypes.BOOLEAN`;
break;
case 'boolean':
str += `DataTypes.STRING(45)`;
break;
case 'serial':
str += `DataTypes.BIGINT`;
break;
case 'date':
str += `DataTypes.DATEONLY`;
break;
case 'datetime':
str += `DataTypes.DATE`;
break;
case 'timestamp':
str += `DataTypes.DATE`;
break;
case 'time':
str += `DataTypes.TIME`;
break;
case 'year':
str += `"YEAR"`;
break;
case 'char':
str += `DataTypes.CHAR(${column.dtxp})`;
break;
case 'varchar':
str += `DataTypes.STRING(${column.dtxp})`;
break;
case 'nchar':
str += `DataTypes.CHAR(${column.dtxp})`;
break;
case 'text':
str += `DataTypes.TEXT`;
break;
case 'tinytext':
str += `DataTypes.TEXT`;
break;
case 'mediumtext':
str += `DataTypes.TEXT`;
break;
case 'longtext':
str += `DataTypes.TEXT`;
break;
case 'binary':
str += `"BINARY(${column.dtxp})"`;
break;
case 'varbinary':
str += `"VARBINARY(${column.dtxp})"`;
break;
case 'blob':
str += `"BLOB"`;
break;
case 'tinyblob':
str += `"TINYBLOB"`;
break;
case 'mediumblob':
str += `"MEDIUMBLOB"`;
break;
case 'longblob':
str += `"LONGBLOB"`;
break;
case 'enum':
str += `DataTypes.ENUM(${column.dtxp})`;
break;
case 'set':
str += `"SET(${column.dtxp})"`;
break;
case 'geometry':
str += `DataTypes.GEOMETRY`;
break;
case 'point':
str += `"POINT"`;
break;
case 'linestring':
str += `"LINESTRING"`;
break;
case 'polygon':
str += `"POLYGON"`;
break;
case 'multipoint':
str += `"MULTIPOINT"`;
break;
case 'multilinestring':
str += `"MULTILINESTRING"`;
break;
case 'multipolygon':
str += `"MULTIPOLYGON"`;
break;
case 'json':
str += `DataTypes.JSON`;
break;
default:
str += `"${column.dt}"`;
break;
}
return str;
}
_sequelizeGetDefault(column) {
let str = '';
switch (column.dt) {
case 'int':
str += `'${column.cdf}'`;
break;
case 'tinyint':
str += `'${column.cdf}'`;
break;
case 'smallint':
str += `'${column.cdf}'`;
break;
case 'mediumint':
str += `'${column.cdf}'`;
break;
case 'bigint':
str += `'${column.cdf}'`;
break;
case 'float':
str += `'${column.cdf}'`;
break;
case 'decimal':
str += `'${column.cdf}'`;
break;
case 'double':
str += `'${column.cdf}'`;
break;
case 'real':
str += `'${column.cdf}'`;
break;
case 'bit':
str += column.cdf ? column.cdf.split('b')[1] : column.cdf;
break;
case 'boolean':
str += column.cdf;
break;
case 'serial':
str += column.cdf;
break;
case 'date':
str += `sequelize.literal('${column.cdf_sequelize}')`;
break;
case 'datetime':
str += `sequelize.literal('${column.cdf_sequelize}')`;
break;
case 'timestamp':
str += `sequelize.literal('${column.cdf_sequelize}')`;
break;
case 'time':
str += `'${column.cdf}'`;
break;
case 'year':
str += `'${column.cdf}'`;
break;
case 'char':
str += `'${column.cdf}'`;
break;
case 'varchar':
str += `'${column.cdf}'`;
break;
case 'nchar':
str += `'${column.cdf}'`;
break;
case 'text':
str += column.cdf;
break;
case 'tinytext':
str += column.cdf;
break;
case 'mediumtext':
str += column.cdf;
break;
case 'longtext':
str += column.cdf;
break;
case 'binary':
str += column.cdf;
break;
case 'varbinary':
str += column.cdf;
break;
case 'blob':
str += column.cdf;
break;
case 'tinyblob':
str += column.cdf;
break;
case 'mediumblob':
str += column.cdf;
break;
case 'longblob':
str += column.cdf;
break;
case 'enum':
str += `'${column.cdf}'`;
break;
case 'set':
str += `'${column.cdf}'`;
break;
case 'geometry':
str += `'${column.cdf}'`;
break;
case 'point':
str += `'${column.cdf}'`;
break;
case 'linestring':
str += `'${column.cdf}'`;
break;
case 'polygon':
str += `'${column.cdf}'`;
break;
case 'multipoint':
str += `'${column.cdf}'`;
break;
case 'multilinestring':
str += `'${column.cdf}'`;
break;
case 'multipolygon':
str += `'${column.cdf}'`;
break;
case 'json':
str += column.cdf;
break;
}
return str;
}
/* getXcColumnsObject(args) {
const columnsArr = [];
for (const column of args.columns) {
const columnObj = {
validate: {
func: [],
args: [],
msg: []
},
cn: column.cn,
_cn: column._cn || column.cn,
type: this._getAbstractType(column),
dt: column.dt,
uidt: column.uidt || this._getUIDataType(column),
uip: column.uip,
uicn: column.uicn,
...column
};
if (column.rqd) {
columnObj.rqd = column.rqd;
}
if (column.cdf) {
columnObj.default = column.cdf;
columnObj.columnDefault = column.cdf;
}
if (column.un) {
columnObj.un = column.un;
}
if (column.pk) {
columnObj.pk = column.pk;
}
if (column.ai) {
columnObj.ai = column.ai;
}
if (column.dtxp) {
columnObj.dtxp = column.dtxp;
}
if (column.dtxs) {
columnObj.dtxs = column.dtxs;
}
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
return columnsArr;
}*/
/* getObject() {
return {
tn: this.ctx.tn,
_tn: this.ctx._tn,
columns: this.getXcColumnsObject(this.ctx),
pks: [],
hasMany: this.ctx.hasMany,
belongsTo: this.ctx.belongsTo,
dbType: this.ctx.dbType,
type: this.ctx.type,
}
}*/
}
export default ModelXcMetaSnowflake;

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

@ -14,7 +14,7 @@ export async function apiTokenCreate(req: Request, res: Response) {
res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id }));
}
export async function apiTokenDelete(req: Request, res: Response) {
const apiToken = await ApiToken.getByToken(req.params.apiTokenId);
const apiToken = await ApiToken.getByToken(req.params.token);
if (
!req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN) &&
apiToken.fk_user_id !== req['user'].id

57
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -44,6 +44,7 @@ type MetaDiff = {
table_name: string;
base_id: string;
type: ModelTypes;
meta?: any;
detectedChanges: Array<MetaDiffChange>;
};
@ -176,6 +177,7 @@ async function getMetaDiff(
const tableProp: MetaDiff = {
title: oldMeta.title,
meta: oldMeta.meta,
table_name: table.tn,
base_id: base.id,
type: ModelTypes.TABLE,
@ -248,6 +250,7 @@ async function getMetaDiff(
for (const model of oldTableMetas) {
changes.push({
table_name: model.table_name,
meta: model.meta,
base_id: base.id,
type: ModelTypes.TABLE,
detectedChanges: [
@ -452,6 +455,7 @@ async function getMetaDiff(
const tableProp: MetaDiff = {
title: oldMeta.title,
meta: oldMeta.meta,
table_name: view.tn,
base_id: base.id,
type: ModelTypes.VIEW,
@ -520,6 +524,7 @@ async function getMetaDiff(
for (const model of oldViewMetas) {
changes.push({
table_name: model.table_name,
meta: model.meta,
base_id: base.id,
type: ModelTypes.TABLE,
detectedChanges: [
@ -539,7 +544,7 @@ async function getMetaDiff(
export async function metaDiff(req, res) {
const project = await Project.getWithInfo(req.params.projectId);
let changes = []
let changes = [];
for (const base of project.bases) {
try {
// @ts-ignore
@ -556,7 +561,7 @@ export async function metaDiff(req, res) {
export async function baseMetaDiff(req, res) {
const project = await Project.getWithInfo(req.params.projectId);
const base = await Base.get(req.params.baseId);
let changes = []
let changes = [];
const sqlClient = NcConnectionMgrv2.getSqlClient(base);
changes = await getMetaDiff(sqlClient, project, base);
@ -572,10 +577,10 @@ export async function metaDiffSync(req, res) {
// @ts-ignore
const sqlClient = NcConnectionMgrv2.getSqlClient(base);
const changes = await getMetaDiff(sqlClient, project, base);
/* Get all relations */
// const relations = (await sqlClient.relationListAll())?.data?.list;
for (const { table_name, detectedChanges } of changes) {
// reorder changes to apply relation remove changes
// before column remove to avoid foreign key constraint error
@ -585,7 +590,7 @@ export async function metaDiffSync(req, res) {
applyChangesPriorityOrder.indexOf(a.type)
);
});
for (const change of detectedChanges) {
switch (change.type) {
case MetaDiffType.TABLE_NEW:
@ -593,15 +598,19 @@ export async function metaDiffSync(req, res) {
const columns = (
await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns);
const model = await Model.insert(project.id, base.id, {
table_name: table_name,
title: getTableNameAlias(table_name, base.is_meta ? project.prefix : '', base),
title: getTableNameAlias(
table_name,
base.is_meta ? project.prefix : '',
base
),
type: ModelTypes.TABLE,
});
for (const column of columns) {
await Column.insert({
uidt: getColumnUiType(base, column),
@ -617,15 +626,15 @@ export async function metaDiffSync(req, res) {
const columns = (
await sqlClient.columnList({ tn: table_name })
)?.data?.list?.map((c) => ({ ...c, column_name: c.cn }));
mapDefaultPrimaryValue(columns);
const model = await Model.insert(project.id, base.id, {
table_name: table_name,
title: getTableNameAlias(table_name, project.prefix, base),
type: ModelTypes.VIEW,
});
for (const column of columns) {
await Column.insert({
uidt: getColumnUiType(base, column),
@ -657,7 +666,7 @@ export async function metaDiffSync(req, res) {
// update old
// populateParams.tableNames.push({ tn });
// populateParams.oldMetas[tn] = oldMetas.find(m => m.tn === tn);
break;
case MetaDiffType.TABLE_COLUMN_TYPE_CHANGE:
case MetaDiffType.VIEW_COLUMN_TYPE_CHANGE:
@ -698,17 +707,21 @@ export async function metaDiffSync(req, res) {
});
const parentCol = await parentModel
.getColumns()
.then((cols) => cols.find((c) => c.column_name === change.rcn));
.then((cols) =>
cols.find((c) => c.column_name === change.rcn)
);
const childCol = await childModel
.getColumns()
.then((cols) => cols.find((c) => c.column_name === change.cn));
.then((cols) =>
cols.find((c) => c.column_name === change.cn)
);
await Column.update(childCol.id, {
...childCol,
uidt: UITypes.ForeignKey,
system: true,
});
if (change.relationType === RelationTypes.BELONGS_TO) {
const title = getUniqueColumnAliasName(
childModel.columns,
@ -746,9 +759,9 @@ export async function metaDiffSync(req, res) {
}
}
}
await NcHelp.executeOperations(virtualColumnInsert, base.type);
// populate m2m relations
await extractAndGenerateManyToManyRelations(await base.getModels());
}
@ -784,7 +797,11 @@ export async function baseMetaDiffSync(req, res) {
const model = await Model.insert(project.id, base.id, {
table_name: table_name,
title: getTableNameAlias(table_name, base.is_meta ? project.prefix : '', base),
title: getTableNameAlias(
table_name,
base.is_meta ? project.prefix : '',
base
),
type: ModelTypes.TABLE,
});

3
packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts

@ -48,7 +48,7 @@ export async function xcVisibilityMetaGet(
const roles = ['owner', 'creator', 'viewer', 'editor', 'commenter', 'guest'];
const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {});
let models =
_models ||
(await Model.list({
@ -78,6 +78,7 @@ export async function xcVisibilityMetaGet(
ptype: model.type,
tn: view.title,
_tn: view.title,
table_meta: model.meta,
...view,
disabled: { ...defaultDisabled },
};

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

Loading…
Cancel
Save