Browse Source

Merge branch 'develop' into fix/if-formula-with-date-empty-string

pull/4586/head
Wing-Kam Wong 2 years ago
parent
commit
fa037a6814
  1. 24
      .github/uffizzi/docker-compose.uffizzi.yml
  2. 71
      .github/workflows/release-pr.yml
  3. 88
      .github/workflows/uffizzi-preview.yml
  4. 25
      README.md
  5. 15
      markdown/readme/languages/chinese.md
  6. 13
      markdown/readme/languages/dutch.md
  7. 12
      markdown/readme/languages/french.md
  8. 13
      markdown/readme/languages/german.md
  9. 13
      markdown/readme/languages/indonesian.md
  10. 14
      markdown/readme/languages/italian.md
  11. 13
      markdown/readme/languages/japanese.md
  12. 13
      markdown/readme/languages/korean.md
  13. 13
      markdown/readme/languages/portuguese.md
  14. 13
      markdown/readme/languages/russian.md
  15. 13
      markdown/readme/languages/spanish.md
  16. 410
      packages/nc-cli/package-lock.json
  17. 2
      packages/nc-cli/package.json
  18. 14
      packages/nc-gui/components.d.ts
  19. 4
      packages/nc-gui/components/cell/DateTimePicker.vue
  20. 4
      packages/nc-gui/components/cell/Json.vue
  21. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  22. 2
      packages/nc-gui/components/cell/TextArea.vue
  23. 4
      packages/nc-gui/components/cell/TimePicker.vue
  24. 4
      packages/nc-gui/components/cell/attachment/Carousel.vue
  25. 640
      packages/nc-gui/components/dashboard/TreeView.vue
  26. 169
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  27. 426
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  28. 8
      packages/nc-gui/components/dashboard/settings/Erd.vue
  29. 11
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  30. 180
      packages/nc-gui/components/dashboard/settings/Modal.vue
  31. 9
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  32. 615
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  33. 612
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  34. 7
      packages/nc-gui/components/dlg/AirtableImport.vue
  35. 2
      packages/nc-gui/components/dlg/QuickImport.vue
  36. 9
      packages/nc-gui/components/dlg/TableCreate.vue
  37. 9
      packages/nc-gui/components/dlg/TableRename.vue
  38. 19
      packages/nc-gui/components/erd/View.vue
  39. 22
      packages/nc-gui/components/general/AddBaseButton.vue
  40. 28
      packages/nc-gui/components/general/BaseLogo.vue
  41. 15
      packages/nc-gui/components/general/ShareBaseButton.vue
  42. 11
      packages/nc-gui/components/general/Tooltip.vue
  43. 9
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  44. 4
      packages/nc-gui/components/smartsheet/Cell.vue
  45. 15
      packages/nc-gui/components/smartsheet/Grid.vue
  46. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  47. 6
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  48. 6
      packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue
  49. 2
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  50. 15
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  51. 16
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  52. 39
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  53. 5
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  54. 38
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  55. 4
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  56. 9
      packages/nc-gui/components/smartsheet/column/utils.ts
  57. 7
      packages/nc-gui/components/smartsheet/header/Cell.vue
  58. 5
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  59. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  60. 12
      packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue
  61. 4
      packages/nc-gui/components/smartsheet/toolbar/MoreActions.vue
  62. 7
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  63. 9
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  64. 22
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  65. 270
      packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue
  66. 2
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  67. 2
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  68. 45
      packages/nc-gui/components/template/Editor.vue
  69. 2
      packages/nc-gui/components/virtual-cell/Formula.vue
  70. 27
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  71. 2
      packages/nc-gui/components/webhook/List.vue
  72. 27
      packages/nc-gui/composables/useColumnCreateStore.ts
  73. 2
      packages/nc-gui/composables/useExpandedFormDetached/index.ts
  74. 4
      packages/nc-gui/composables/useGlobal/state.ts
  75. 2
      packages/nc-gui/composables/useLTARStore.ts
  76. 52
      packages/nc-gui/composables/useProject.ts
  77. 4
      packages/nc-gui/composables/useSharedFormViewStore.ts
  78. 9
      packages/nc-gui/composables/useSharedView.ts
  79. 5
      packages/nc-gui/composables/useSmartsheetStore.ts
  80. 9
      packages/nc-gui/composables/useTable.ts
  81. 31
      packages/nc-gui/composables/useTabs.ts
  82. 1
      packages/nc-gui/context/index.ts
  83. 62
      packages/nc-gui/lang/de.json
  84. 10
      packages/nc-gui/lang/fr.json
  85. 292
      packages/nc-gui/lang/ja.json
  86. 126
      packages/nc-gui/lang/ru.json
  87. 9
      packages/nc-gui/lib/enums.ts
  88. 2
      packages/nc-gui/nuxt.config.ts
  89. 49
      packages/nc-gui/package-lock.json
  90. 3
      packages/nc-gui/package.json
  91. 21
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  92. 1
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  93. 5
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  94. 2
      packages/nc-gui/pages/[projectType]/gallery/[viewId]/index.vue
  95. 2
      packages/nc-gui/pages/[projectType]/kanban/[viewId]/index.vue
  96. 2
      packages/nc-gui/pages/[projectType]/view/[viewId].vue
  97. 5
      packages/nc-gui/pages/forgot-password.vue
  98. 2
      packages/nc-gui/pages/index/apps.vue
  99. 3
      packages/nc-gui/pages/index/index/[projectId].vue
  100. 4
      packages/nc-gui/pages/index/index/create-external.vue
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -0,0 +1,24 @@
version: '3'
x-uffizzi:
ingress:
service: nocodb
port: 8080
services:
postgres:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: root_db
nocodb:
image: "${NOCODB_IMAGE}"
ports:
- "8080:8080"
restart: always
environment:
NC_DB: "pg://localhost:5432?u=postgres&p=password&d=root_db"
NC_ADMIN_EMAIL: admin@nocodb.com
NC_ADMIN_PASSWORD: password

71
.github/workflows/release-pr.yml

@ -6,7 +6,8 @@ on:
# reopened: closed pull request is reopened
# synchronize: commit(s) pushed to the pull request
# ready_for_review: non PR release
types: [opened, reopened, synchronize, ready_for_review]
# closed: pull request is closed, used to delete uffizzi previews
types: [opened, reopened, synchronize, ready_for_review, closed]
paths:
- "packages/nocodb-sdk/**"
- "packages/nc-gui/**"
@ -20,7 +21,7 @@ concurrency:
jobs:
# enrich tag for pr release
set-tag:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
runs-on: 'ubuntu-latest'
steps:
- name: set-tag
@ -47,7 +48,7 @@ jobs:
# Build, install, publish frontend and backend to npm
release-npm:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
needs: [set-tag]
uses: ./.github/workflows/release-npm.yml
with:
@ -58,7 +59,7 @@ jobs:
# Build docker image and push to docker hub
release-docker:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
needs: [release-npm, set-tag]
uses: ./.github/workflows/release-docker.yml
with:
@ -72,7 +73,7 @@ jobs:
# Build executables and publish to GitHub
release-executables:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml
with:
@ -82,7 +83,7 @@ jobs:
# Add a comment for PR docker build
leave-comment:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
runs-on: 'ubuntu-latest'
needs: [release-docker, set-tag]
steps:
@ -94,9 +95,45 @@ jobs:
docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
```
# Create a preview for the pull request
preview-pull-request:
name: "Trigger Uffizzi Preview"
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
runs-on: 'ubuntu-latest'
needs: [release-docker, set-tag]
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
NOCODB_IMAGE=nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
export NOCODB_IMAGE
# Render simple template from environment variables.
envsubst < .github/uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
# Add a comment for PR executable build
leave-executable-comment:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action != 'closed' }}
runs-on: 'ubuntu-latest'
needs: [release-executables, set-tag]
steps:
@ -129,3 +166,23 @@ jobs:
```
For executables visit [here](https://github.com/nocodb/nocodb-timely/releases/tag/${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }})
# delete the uffizzi preview created off of this PR
delete-uffizzi-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && github.event.pull_request.draft == false && github.base_ref == 'develop' && github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

88
.github/workflows/uffizzi-preview.yml

@ -0,0 +1,88 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "PR Release"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: 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",

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

@ -10,6 +10,8 @@ declare module '@vue/runtime-core' {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
@ -85,6 +87,8 @@ declare module '@vue/runtime-core' {
IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default']
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosMysqlIcon: typeof import('~icons/logos/mysql-icon')['default']
LogosPostgresql: typeof import('~icons/logos/postgresql')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['default']
MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default']
@ -144,7 +148,10 @@ declare module '@vue/runtime-core' {
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseAlert: typeof import('~icons/mdi/database-alert')['default']
MdiDatabaseLockOutline: typeof import('~icons/mdi/database-lock-outline')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDatabasePlusOutline: typeof import('~icons/mdi/database-plus-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
@ -163,6 +170,8 @@ 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']
@ -176,6 +185,7 @@ declare module '@vue/runtime-core' {
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGraphOutline: typeof import('~icons/mdi/graph-outline')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
@ -193,6 +203,7 @@ 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']
@ -235,7 +246,10 @@ declare module '@vue/runtime-core' {
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
PhCloudLightningDuotone: typeof import('~icons/ph/cloud-lightning-duotone')['default']
PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RiTeamFill: typeof import('~icons/ri/team-fill')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SimpleIconsMicrosoftsqlserver: typeof import('~icons/simple-icons/microsoftsqlserver')['default']
VscodeIconsFileTypeSqlite: typeof import('~icons/vscode-icons/file-type-sqlite')['default']
}
}

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

@ -28,9 +28,11 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
let isDateInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
let localState = $computed({
get() {

4
packages/nc-gui/components/cell/Json.vue

@ -135,13 +135,13 @@ useSelectedCellKeyupListener(active, (e) => {
<a-button type="text" size="small" :onclick="clear"><div class="text-xs">Cancel</div></a-button>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel">
<div class="text-xs" :onclick="onSave">Save</div>
<div class="text-xs" @click="onSave">Save</div>
</a-button>
</div>
</div>
<LazyMonacoEditor
:model-value="localValue"
:model-value="localValue || ''"
class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
:hide-minimap="true"

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

@ -105,7 +105,7 @@ const vModel = computed({
const selectedTitles = computed(() =>
modelValue
? typeof modelValue === 'string'
? isMysql
? isMysql(column.value.base_id)
? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b)

2
packages/nc-gui/components/cell/TextArea.vue

@ -3,7 +3,7 @@ import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, inject, useVModel } from '#imports'
const props = defineProps<{
modelValue?: string | null
modelValue?: string | number
}>()
const emits = defineEmits(['update:modelValue'])

4
packages/nc-gui/components/cell/TimePicker.vue

@ -19,9 +19,11 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
let isTimeInvalid = $ref(false)
const dateFormat = isMysql.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({
get() {

4
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -47,7 +47,7 @@ onClickOutside(carouselRef, () => {
</script>
<template>
<general-overlay v-model="selectedImage" :z-index="1001">
<GeneralOverlay v-model="selectedImage" :z-index="1001">
<template v-if="selectedImage">
<div class="overflow-hidden p-12 text-center relative">
<div class="text-white group absolute top-5 right-5">
@ -101,7 +101,7 @@ onClickOutside(carouselRef, () => {
</a-carousel>
</div>
</template>
</general-overlay>
</GeneralOverlay>
</template>
<style scoped>

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

@ -5,6 +5,7 @@ import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button'
import type { VNodeRef } from '#imports'
import {
ClientType,
Empty,
TabType,
computed,
@ -30,7 +31,7 @@ const { addTab } = useTabs()
const { $api, $e } = useNuxtApp()
const { tables, loadTables, isSharedBase } = useProject()
const { bases, tables, loadTables, isSharedBase } = useProject()
const { activeTab } = useTabs()
@ -42,13 +43,17 @@ const route = useRoute()
const [searchActive, toggleSearchActive] = useToggle()
let key = $ref(0)
const toggleDialog = inject(ToggleDialogInj, () => {})
const menuRef = $ref<HTMLLIElement>()
const keys = $ref<Record<string, number>>({})
const activeKey = ref<string[]>([])
const menuRefs = $ref<HTMLElement[] | HTMLElement>()
let filterQuery = $ref('')
const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null))
const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.id : null))
const tablesById = $computed(() =>
tables.value?.reduce<Record<string, TableType>>((acc, table) => {
@ -59,17 +64,23 @@ const tablesById = $computed(() =>
)
const filteredTables = $computed(() =>
tables.value?.filter((table) => !filterQuery || table.title.toLowerCase().includes(filterQuery.toLowerCase())),
tables.value?.filter(
(table) => !searchActive.value || !filterQuery || table.title.toLowerCase().includes(filterQuery.toLowerCase()),
),
)
let sortable: Sortable
const sortables: Record<string, Sortable> = {}
// todo: replace with vuedraggable
const initSortable = (el: Element) => {
if (sortable) sortable.destroy()
sortable = Sortable.create(el as HTMLLIElement, {
const base_id = el.getAttribute('nc-base')
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)
const { newIndex = 0, oldIndex = 0 } = evt
const itemEl = evt.item as HTMLLIElement
@ -99,10 +110,14 @@ const initSortable = (el: Element) => {
}
// update the order of the moved item
tables.value?.splice(newIndex, 0, ...tables.value?.splice(oldIndex, 1))
tables.value?.splice(newIndex + offset, 0, ...tables.value?.splice(oldIndex + offset, 1))
// force re-render the list
key++
if (keys[base_id]) {
keys[base_id] = keys[base_id] + 1
} else {
keys[base_id] = 1
}
// update the item order
await $api.dbTable.reorder(item.id as string, {
@ -114,8 +129,12 @@ const initSortable = (el: Element) => {
}
watchEffect(() => {
if (menuRef) {
initSortable(menuRef)
if (menuRefs) {
if (menuRefs instanceof HTMLElement) {
initSortable(menuRefs)
} else {
menuRefs.forEach((el) => initSortable(el))
}
}
})
@ -145,7 +164,7 @@ const addTableTab = (table: TableType) => {
addTab({ title: table.title, id: table.id, type: table.type as TabType })
}
function openRenameTableDialog(table: TableType, rightClick = false) {
function openRenameTableDialog(table: TableType, baseId?: string, rightClick = false) {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
const isOpen = ref(true)
@ -153,6 +172,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen,
'tableMeta': table,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog,
})
@ -163,7 +183,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
}
}
function openQuickImportDialog(type: string) {
function openQuickImportDialog(type: string, baseId?: string) {
$e(`a:actions:import-${type}`)
const isOpen = ref(true)
@ -171,6 +191,7 @@ function openQuickImportDialog(type: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen,
'importType': type,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog,
})
@ -181,13 +202,14 @@ function openQuickImportDialog(type: string) {
}
}
function openAirtableImportDialog() {
function openAirtableImportDialog(baseId?: string) {
$e('a:actions:import-airtable')
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog,
})
@ -198,13 +220,14 @@ function openAirtableImportDialog() {
}
}
function openTableCreateDialog() {
function openTableCreateDialog(baseId?: string) {
$e('c:table:create:navdraw')
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog,
})
@ -217,9 +240,18 @@ function openTableCreateDialog() {
const searchInputRef: VNodeRef = (vnode: typeof Input) => vnode?.$el?.focus()
const beforeSearch = ref<string[]>([])
const onSearchCloseIconClick = () => {
filterQuery = ''
toggleSearchActive(false)
activeKey.value = beforeSearch.value
}
const onSearchIconClick = () => {
beforeSearch.value = activeKey.value
toggleSearchActive(true)
activeKey.value = bases.value.filter((el) => el.enabled).map((el) => `collapse-${el.id}`)
}
const isCreateTableAllowed = computed(
@ -249,13 +281,35 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}
}
})
watch(
activeTable,
(value, oldValue) => {
if (value) {
if (value !== oldValue) {
const fndTable = tables.value.find((el) => el.id === value)
if (fndTable) {
activeKey.value = [`collapse-${fndTable.base_id}`]
}
}
} else {
if (bases.value.filter((el) => el.enabled)[0]?.id)
activeKey.value = [`collapse-${bases.value.filter((el) => el.enabled)[0].id}`]
}
},
{ immediate: true },
)
</script>
<template>
<div class="nc-treeview-container flex flex-col">
<a-dropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu">
<div class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-col scrollbar-thin-dull" :class="{ 'mb-[20px]': isSharedBase }">
<div class="min-h-[36px] py-1 px-3 flex w-full items-center gap-1 cursor-pointer" @contextmenu="setMenuContext('main')">
<div
v-if="bases[0] && bases[0].enabled && !bases.slice(1).filter((el) => el.enabled)?.length"
class="min-h-[36px] py-1 px-3 flex w-full items-center gap-1 cursor-pointer"
@contextmenu="setMenuContext('main')"
>
<Transition name="slide-left" mode="out-in">
<a-input
v-if="searchActive"
@ -268,7 +322,9 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<span v-else class="flex-1 text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
{{ $t('objects.tables') }}
<template v-if="tables?.length"> ({{ tables.length }}) </template>
<template v-if="tables.filter((table) => table.base_id === bases[0].id)?.length">
({{ tables.filter((table) => table.base_id === bases[0].id).length }})
</template>
</span>
</Transition>
@ -277,19 +333,98 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<IcRoundSearch v-else class="text-lg text-primary mx-1 mt-0.5" @click="toggleSearchActive(true)" />
</Transition>
</div>
<div
v-else
class="min-h-[36px] py-1 px-3 flex w-full items-center gap-1 cursor-pointer"
@contextmenu="setMenuContext('main')"
>
<Transition name="slide-left" mode="out-in">
<a-input
v-if="searchActive"
:ref="searchInputRef"
v-model:value="filterQuery"
class="flex-1 rounded"
:placeholder="$t('placeholder.searchProjectTree')"
/>
<span v-else class="flex-1 text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
BASES
<template v-if="tables.filter((table) => table.base_id === bases[0].id)?.length">
({{ bases.filter((el) => el.enabled).length }})
</template>
</span>
</Transition>
<Transition name="slide-right" mode="out-in">
<MdiClose v-if="searchActive" class="text-lg mx-1 mt-0.5" @click="onSearchCloseIconClick" />
<IcRoundSearch v-else class="text-lg text-primary mx-1 mt-0.5" @click="onSearchIconClick" />
</Transition>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<Transition name="slide-right" mode="out-in">
<MdiDotsVertical v-if="!searchActive" class="hover:text-accent outline-0" />
</Transition>
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item-group title="Connect to new datasource" class="!px-0 !mx-0">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL)">
<div class="color-transition nc-project-menu-item group">
<LogosMysqlIcon class="group-hover:text-accent" />
MySQL
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG)">
<div class="color-transition nc-project-menu-item group">
<LogosPostgresql class="group-hover:text-accent" />
Postgres
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE)">
<div class="color-transition nc-project-menu-item group">
<VscodeIconsFileTypeSqlite class="group-hover:text-accent" />
SQLite
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL)">
<div class="color-transition nc-project-menu-item group">
<SimpleIconsMicrosoftsqlserver class="group-hover:text-accent" />
MSSQL
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-accent" />
<!-- Request a data source you need? -->
{{ $t('labels.requestDataSource') }}
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="flex-1">
<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-5 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog"
class="group flex items-center gap-2 pl-8 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog(bases[0].id)"
>
<MdiPlus />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{ $t('tooltip.addTable') }}</span>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu" />
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu outline-0" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
@ -298,7 +433,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
@click="openAirtableImportDialog"
@click="openAirtableImportDialog(bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-accent" />
@ -306,14 +441,22 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('csvImport')" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<a-menu-item
v-if="isUIAllowed('csvImport')"
key="quick-import-csv"
@click="openQuickImportDialog('csv', bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-accent" />
CSV file
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('jsonImport')" key="quick-import-json" @click="openQuickImportDialog('json')">
<a-menu-item
v-if="isUIAllowed('jsonImport')"
key="quick-import-json"
@click="openQuickImportDialog('json', bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-accent" />
JSON file
@ -323,7 +466,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
@click="openQuickImportDialog('excel')"
@click="openQuickImportDialog('excel', bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-accent" />
@ -334,6 +477,35 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-divider class="my-0" />
<a-menu-item-group title="Connect to new datasource" class="!px-0 !mx-0">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL)">
<div class="color-transition nc-project-menu-item group">
<LogosMysqlIcon class="group-hover:text-accent" />
MySQL
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG)">
<div class="color-transition nc-project-menu-item group">
<LogosPostgresql class="group-hover:text-accent" />
Postgres
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE)">
<div class="color-transition nc-project-menu-item group">
<VscodeIconsFileTypeSqlite class="group-hover:text-accent" />
SQLite
</div>
</a-menu-item>
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL)">
<div class="color-transition nc-project-menu-item group">
<SimpleIconsMicrosoftsqlserver class="group-hover:text-accent" />
MSSQL
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-e="['e:datasource:import-request']"
@ -351,88 +523,380 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</a-dropdown>
</div>
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
v-for="table of tables"
:key="table.id"
v-e="['a:table:open']"
:class="[
{ hidden: !filteredTables?.includes(table), active: activeTable === table.title },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
]"
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
<div class="transition-height duration-200">
<div class="border-none sortable-list">
<div v-if="bases[0]" :key="`base-${bases[0].id}`">
<div
v-if="bases[0] && bases[0].enabled"
ref="menuRefs"
:key="`sortable-${bases[0].id}-${bases[0].id && bases[0].id in keys ? keys[bases[0].id] : '0'}`"
:nc-base="bases[0].id"
>
<div
v-for="table of tables.filter((table) => table.base_id === bases[0].id)"
:key="table.id"
v-e="['a:table:open']"
:class="[
{ hidden: !filteredTables?.includes(table), active: activeTable === table.id },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
]"
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
<GeneralTooltip class="pl-8 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') }"
/>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
</div>
<a-dropdown
v-if="!isSharedBase && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']"
@click.stop
>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 outline-0" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table, bases[0].id)">
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`">
{{ $t('general.rename') }}
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-testid="`sidebar-table-delete-${table.title}`"
@click="deleteTable(table)"
>
<div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</GeneralTooltip>
</div>
</div>
</div>
</div>
</div>
<div
v-if="!tables.filter((table) => table.base_id === bases[0].id)?.length"
class="mt-0.5 pt-16 mx-3 flex flex-col items-center border-t-1 border-gray-50"
>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</div>
<div v-else class="transition-height duration-200">
<div class="border-none sortable-list">
<div v-for="[index, base] of Object.entries(bases)" :key="`base-${base.id}`">
<a-collapse
v-if="base && base.enabled"
v-model:activeKey="activeKey"
:class="[{ hidden: searchActive && !!filterQuery && !filteredTables?.find((el) => el.base_id === base.id) }]"
expand-icon-position="right"
:bordered="false"
:accordion="!searchActive"
ghost
>
<GeneralTooltip class="pl-5 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') }"
/>
<a-collapse-panel :key="`collapse-${base.id}`">
<template #header>
<div v-if="index === '0'" class="flex items-center gap-2 text-gray-500 font-bold">
<GeneralBaseLogo :base-type="base.type" />
Default ({{ tables.filter((table) => table.base_id === base.id).length || '0' }})
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
<div v-else class="flex items-center gap-2 text-gray-500 font-bold">
<GeneralBaseLogo :base-type="base.type" />
{{ base.alias || '' }}
({{ tables.filter((table) => table.base_id === base.id).length || '0' }})
</div>
</template>
<div
v-if="index === '0' && 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"
@click="openTableCreateDialog(bases[0].id)"
>
<MdiPlus />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{
$t('tooltip.addTable')
}}</span>
<a-dropdown
v-if="!isSharedBase && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
v-if="!isSharedBase"
:trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop
>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu outline-0" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)">
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`">
{{ $t('general.rename') }}
</div>
<!-- Quick Import From -->
<a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
@click="openAirtableImportDialog(bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-accent" />
Airtable
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('csvImport')"
key="quick-import-csv"
@click="openQuickImportDialog('csv', bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-accent" />
CSV file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('jsonImport')"
key="quick-import-json"
@click="openQuickImportDialog('json', bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-accent" />
JSON file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
@click="openQuickImportDialog('excel', bases[0].id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-accent" />
Microsoft Excel
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-accent" />
<!-- Request a data source you need? -->
{{ $t('labels.requestDataSource') }}
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div
v-else-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"
@click="openTableCreateDialog(base.id)"
>
<MdiPlus />
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-testid="`sidebar-table-delete-${table.title}`"
@click="deleteTable(table)"
>
<div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{
$t('tooltip.addTable')
}}</span>
<a-dropdown
v-if="!isSharedBase"
:trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop
>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu outline-0" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<!-- Quick Import From -->
<a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
@click="openAirtableImportDialog(base.id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-accent" />
Airtable
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('csvImport')"
key="quick-import-csv"
@click="openQuickImportDialog('csv', base.id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-accent" />
CSV file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('jsonImport')"
key="quick-import-json"
@click="openQuickImportDialog('json', base.id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-accent" />
JSON file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
@click="openQuickImportDialog('excel', base.id)"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-accent" />
Microsoft Excel
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-accent" />
<!-- Request a data source you need? -->
{{ $t('labels.requestDataSource') }}
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</GeneralTooltip>
</div>
<div
ref="menuRefs"
:key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`"
:nc-base="base.id"
>
<div
v-for="table of tables.filter((table) => table.base_id === base.id)"
:key="table.id"
v-e="['a:table:open']"
:class="[
{ hidden: !filteredTables?.includes(table), active: activeTable === table.id },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
]"
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
<GeneralTooltip class="pl-8 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') }"
/>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
</div>
<a-dropdown
v-if="!isSharedBase && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']"
@click.stop
>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 outline-0" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item
v-if="isUIAllowed('table-rename')"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, base.id)"
>
<div class="nc-project-menu-item">
{{ $t('general.rename') }}
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-testid="`sidebar-table-delete-${table.title}`"
@click="deleteTable(table)"
>
<div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</GeneralTooltip>
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
</div>
<div v-else class="mt-0.5 pt-16 mx-3 flex flex-col items-center border-t-1 border-gray-50">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</div>
</div>
<template v-if="!isSharedBase" #overlay>
<a-menu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'table'">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<a-menu-item
v-if="isUIAllowed('table-rename')"
@click="openRenameTableDialog(contextMenuTarget.value, bases[0].id, true)"
>
<div class="nc-project-menu-item">
{{ $t('general.rename') }}
</div>
@ -459,7 +923,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2">
<LazyGeneralShareBaseButton
<LazyGeneralAddBaseButton
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/>
@ -527,7 +991,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}
.nc-tree-item {
@apply relative cursor-pointer after:(pointer-events-none content-[''] absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0);
@apply relative cursor-pointer after:(pointer-events-none content-[''] absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0);
}
.nc-tree-item svg {
@ -576,4 +1040,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
:deep(.ant-dropdown-menu-title-content) {
@apply !p-0;
}
:deep(.ant-collapse-content-box) {
@apply !p-0;
}
:deep(.ant-collapse-header) {
@apply !border-0;
}
</style>

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

@ -70,96 +70,97 @@ onMounted(async () => {
</script>
<template>
<a-modal
v-model:visible="showPluginInstallModal"
:class="{ active: showPluginInstallModal }"
:closable="false"
centered
min-height="300"
:footer="null"
wrap-class-name="nc-modal-plugin-install"
v-bind="$attrs"
>
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@saved="saved()"
/>
</a-modal>
<a-modal
v-model:visible="showPluginUninstallModal"
:class="{ active: showPluginUninstallModal }"
:closable="false"
width="24rem"
centered
:footer="null"
wrap-class-name="nc-modal-plugin-uninstall"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
{{ `Click on confirm to reset ${pluginApp && pluginApp.title}` }}
</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showPluginUninstallModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="resetPlugin"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
</a-modal>
<div class="grid grid-cols-2 gap-x-2 gap-y-4 mt-4">
<a-card
v-for="(app, i) in apps"
:key="i"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
:body-style="{ width: '100%' }"
<div>
<a-modal
v-model:visible="showPluginInstallModal"
:class="{ active: showPluginInstallModal }"
:closable="false"
centered
min-height="300"
:footer="null"
wrap-class-name="nc-modal-plugin-install"
v-bind="$attrs"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
Edit
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlus />
Install
</div>
</a-button>
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@saved="saved()"
/>
</a-modal>
<a-modal
v-model:visible="showPluginUninstallModal"
:closable="false"
width="24rem"
centered
:footer="null"
wrap-class-name="nc-modal-plugin-uninstall"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
{{ `Click on confirm to reset ${pluginApp && pluginApp.title}` }}
</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showPluginUninstallModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="resetPlugin"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-20 pl-3">
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</a-modal>
<div class="grid grid-cols-2 gap-x-2 gap-y-4 mt-4">
<a-card
v-for="(app, i) in apps"
:key="i"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
:body-style="{ width: '100%' }"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
Edit
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlus />
Install
</div>
</a-button>
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-20 pl-3">
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
{{ app.description }}
</div>
</div>
</div>
</a-card>
</a-card>
</div>
</div>
</template>

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

@ -0,0 +1,426 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import type { BaseType } from 'nocodb-sdk'
import CreateBase from './data-sources/CreateBase.vue'
import EditBase from './data-sources/EditBase.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Erd from './Erd.vue'
import { ClientType, DataSourcesSubTab } from '~/lib'
import { useNuxtApp, useProject } from '#imports'
interface Props {
state: string
reload: boolean
}
const props = defineProps<Props>()
const emits = defineEmits(['update:state', 'update:reload', 'awaken'])
const vState = useVModel(props, 'state', emits)
const vReload = useVModel(props, 'reload', emits)
const { $api, $e } = useNuxtApp()
const { project, loadProject } = useProject()
let sources = $ref<BaseType[]>([])
let activeBaseId = $ref('')
let metadiffbases = $ref<string[]>([])
let clientType = $ref<ClientType>(ClientType.MYSQL)
let isReloading = $ref(false)
let forceAwakened = $ref(false)
async function loadBases() {
try {
if (!project.value?.id) return
isReloading = true
vReload.value = true
const baseList = await $api.base.list(project.value?.id)
if (baseList.bases.list && baseList.bases.list.length) {
sources = baseList.bases.list
}
} catch (e) {
console.error(e)
} finally {
vReload.value = false
isReloading = false
}
}
async function loadMetaDiff() {
try {
if (!project.value?.id) return
metadiffbases = []
const metadiff = await $api.project.metaDiffGet(project.value?.id)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
metadiffbases.push(model.base_id)
}
}
} catch (e) {
console.error(e)
}
}
const baseAction = (baseId?: string, action?: string) => {
if (!baseId) return
activeBaseId = baseId
vState.value = action || ''
}
const deleteBase = (base: BaseType) => {
$e('c:base:delete')
Modal.confirm({
title: `Do you want to delete '${base.alias}' project?`,
wrapClassName: 'nc-modal-base-delete',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
try {
await $api.base.delete(base.project_id as string, base.id as string)
$e('a:base:delete')
sources.splice(sources.indexOf(base), 1)
await loadProject()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
style: 'top: 30%!important',
})
}
const toggleBase = async (base: BaseType, state: boolean) => {
try {
if (!state && sources.filter((src) => src.enabled).length < 2) {
message.info('There should be at least one enabled base!')
return
}
base.enabled = state
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
enabled: base.enabled,
})
await loadProject()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const moveBase = async (e: any) => {
try {
if (e.oldIndex === e.newIndex) return
// sources list is mutated so we have to get the new index and mirror it to backend
const base = sources[e.newIndex]
if (base) {
if (!base.order) {
// empty update call to reorder bases (migration)
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
})
message.info('Bases are migrated. Please try again.')
} else {
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
order: e.newIndex + 1,
})
}
}
await loadProject()
await loadBases()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const forceAwaken = () => {
forceAwakened = !forceAwakened
emits('awaken', forceAwakened)
}
onMounted(async () => {
if (sources.length === 0) {
await loadBases()
await loadMetaDiff()
}
})
watch(
() => props.reload,
async (reload) => {
if (reload && !isReloading) {
await loadBases()
await loadMetaDiff()
}
},
)
watch(
() => sources.length,
(l) => {
if (l > 1 && !forceAwakened) {
emits('awaken', false)
} else {
emits('awaken', true)
}
},
{ immediate: true },
)
watch(
vState,
async (newState) => {
if (!sources.length) {
await loadBases()
}
switch (newState) {
case ClientType.MYSQL:
clientType = ClientType.MYSQL
vState.value = DataSourcesSubTab.New
break
case ClientType.PG:
clientType = ClientType.PG
vState.value = DataSourcesSubTab.New
break
case ClientType.SQLITE:
clientType = ClientType.SQLITE
vState.value = DataSourcesSubTab.New
break
case ClientType.MSSQL:
clientType = ClientType.MSSQL
vState.value = DataSourcesSubTab.New
break
case DataSourcesSubTab.New:
if (sources.length > 1 && !forceAwakened) {
vState.value = ''
}
break
}
},
{ immediate: true },
)
</script>
<template>
<div class="flex flex-row w-full h-full">
<div class="flex flex-col w-full overflow-auto">
<div v-if="vState === ''" class="max-h-600px min-w-1200px overflow-y-auto">
<div class="ds-table-head">
<div class="ds-table-row">
<div class="ds-table-col ds-table-name">Name</div>
<div class="ds-table-col ds-table-actions">Actions</div>
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">Show / Hide</div>
</div>
</div>
<div class="ds-table-body">
<Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase">
<template #header>
<div v-if="sources[0]" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-name">
<div class="flex items-center gap-1">
<GeneralBaseLogo :base-type="sources[0].type" />
BASE
<span class="text-gray-400 text-xs">({{ sources[0].type }})</span>
</div>
</div>
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<a-button
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<a-tooltip v-if="metadiffbases.includes(sources[0].id as string)">
<template #title>Out of sync</template>
<MdiDatabaseAlert class="text-lg group-hover:text-accent text-primary" />
</a-tooltip>
<MdiDatabaseSync v-else class="text-lg group-hover:text-accent" />
Sync Metadata
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiDatabaseLockOutline class="text-lg group-hover:text-accent" />
UI ACL
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiGraphOutline class="text-lg group-hover:text-accent" />
ERD
</div>
</a-button>
<a-button
v-if="!sources[0].is_meta"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiEditOutline class="text-lg group-hover:text-accent" />
Edit
</div>
</a-button>
</div>
</div>
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer">
<a-tooltip>
<template #title>
<template v-if="sources[0].enabled">Hide in UI</template>
<template v-else>Show in UI</template>
</template>
<a-switch :checked="sources[0].enabled ? true : false" @change="toggleBase(sources[0], $event)" />
</a-tooltip>
</div>
</div>
</div>
</template>
<template #item="{ element: base, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-name">
<MdiDragVertical v-if="sources.length > 2" small class="ds-table-handle" />
<div class="flex items-center gap-1">
<GeneralBaseLogo :base-type="base.type" />
{{ base.is_meta ? 'BASE' : base.alias }} <span class="text-gray-400 text-xs">({{ base.type }})</span>
</div>
</div>
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<a-button
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<a-tooltip v-if="metadiffbases.includes(base.id as string)">
<template #title>Out of sync</template>
<MdiDatabaseAlert class="text-lg group-hover:text-accent text-primary" />
</a-tooltip>
<MdiDatabaseSync v-else class="text-lg group-hover:text-accent" />
Sync Metadata
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiDatabaseLockOutline class="text-lg group-hover:text-accent" />
UI ACL
</div>
</a-button>
<a-button class="nc-action-btn cursor-pointer outline-0" @click="baseAction(base.id, DataSourcesSubTab.ERD)">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiGraphOutline class="text-lg group-hover:text-accent" />
ERD
</div>
</a-button>
<a-button
v-if="!base.is_meta"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.Edit)"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiEditOutline class="text-lg group-hover:text-accent" />
Edit
</div>
</a-button>
<a-button v-if="!base.is_meta" class="nc-action-btn cursor-pointer outline-0" @click="deleteBase(base)">
<div class="flex items-center gap-2 text-red-500 font-light">
<MdiDeleteOutline class="text-lg group-hover:text-accent" />
Delete
</div>
</a-button>
</div>
</div>
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer">
<a-tooltip>
<template #title>
<template v-if="base.enabled">Hide in UI</template>
<template v-else>Show in UI</template>
</template>
<a-switch :checked="base.enabled ? true : false" @change="toggleBase(base, $event)" />
</a-tooltip>
</div>
</div>
</div>
</template>
</Draggable>
</div>
</div>
<div v-else-if="vState === DataSourcesSubTab.New" class="max-h-600px overflow-y-auto">
<CreateBase :connection-type="clientType" @base-created="loadBases" />
</div>
<div v-else-if="vState === DataSourcesSubTab.Metadata" class="max-h-600px overflow-y-auto">
<Metadata :base-id="activeBaseId" @base-synced="loadBases" />
</div>
<div v-else-if="vState === DataSourcesSubTab.UIAcl" class="max-h-600px overflow-y-auto">
<UIAcl :base-id="activeBaseId" />
</div>
<div v-else-if="vState === DataSourcesSubTab.ERD" class="max-h-600px overflow-y-auto">
<Erd :base-id="activeBaseId" />
</div>
<div v-else-if="vState === DataSourcesSubTab.Edit" class="max-h-600px overflow-y-auto">
<EditBase :base-id="activeBaseId" @base-updated="loadBases" />
</div>
</div>
</div>
</template>
<style>
.ds-table-head {
@apply flex items-center border-t bg-gray-100 font-bold text-gray-500;
}
.ds-table-body {
@apply flex flex-col;
}
.ds-table-row {
@apply grid grid-cols-20 border-b w-full h-full border-l border-r;
}
.ds-table-col {
@apply flex items-center p-3 border-r-1 mr-2 h-50px;
}
.ds-table-enabled {
@apply col-span-2 flex justify-center;
}
.ds-table-name {
@apply col-span-8;
}
.ds-table-actions {
@apply col-span-10;
}
.ds-table-col:last-child {
@apply border-r-0;
}
.ds-table-handle {
@apply cursor-pointer justify-self-start mr-2;
}
</style>

8
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -1,5 +1,11 @@
<script setup lang="ts">
const props = defineProps<{
baseId: string
}>()
</script>
<template>
<div class="w-full h-full !p-0 h-70vh">
<ErdView />
<ErdView :base-id="props.baseId" />
</div>
</template>

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

@ -1,6 +1,12 @@
<script setup lang="ts">
import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports'
const props = defineProps<{
baseId: string
}>()
const emit = defineEmits(['baseSynced'])
const { $api } = useNuxtApp()
const { project, loadTables } = useProject()
@ -19,7 +25,7 @@ async function loadMetaDiff() {
isLoading = true
isDifferent = false
metadiff = await $api.project.metaDiffGet(project.value?.id)
metadiff = await $api.base.metaDiffGet(project.value?.id, props.baseId)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
@ -38,11 +44,12 @@ async function syncMetaDiff() {
if (!project.value?.id || !isDifferent) return
isLoading = true
await $api.project.metaDiffSync(project.value.id)
await $api.base.metaDiffSync(project.value.id, props.baseId)
// Table metadata recreated successfully
message.info(t('msg.info.metaDataRecreated'))
await loadTables()
await loadMetaDiff()
emit('baseSynced')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {

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

@ -1,14 +1,19 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import { resolveComponent, useI18n, useNuxtApp, useUIPermission, useVModel, watch } from '#imports'
import AppStore from './AppStore.vue'
import DataSources from './DataSources.vue'
import Misc from './Misc.vue'
import { DataSourcesSubTab, useI18n, useNuxtApp, useUIPermission, useVModel, watch } from '#imports'
import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NotebookOutline from '~icons/mdi/notebook-outline'
import FolderCog from '~icons/mdi/folder-cog'
interface Props {
modelValue: boolean
openKey?: string
openKey: string
dataSourcesState: string
}
interface SubTabGroup {
@ -30,16 +35,24 @@ interface TabGroup {
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const emits = defineEmits(['update:modelValue', 'update:openKey', 'update:dataSourcesState'])
const vModel = useVModel(props, 'modelValue', emits)
const vOpenKey = useVModel(props, 'openKey', emits)
const vDataState = useVModel(props, 'dataSourcesState', emits)
const { isUIAllowed } = useUIPermission()
const { t } = useI18n()
const { $e } = useNuxtApp()
const dataSourcesReload = ref(false)
const dataSourcesAwakened = ref(false)
const tabsInfo: TabGroup = {
teamAndAuth: {
title: t('title.teamAndAuth'),
@ -68,56 +81,33 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth')
},
},
...(isUIAllowed('appStore')
? {
appStore: {
// App Store
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
new: {
title: 'Apps',
body: resolveComponent('DashboardSettingsAppStore'),
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
}
: {}),
projMetaData: {
// Project Metadata
title: t('title.projMeta'),
icon: MultipleTableIcon,
appStore: {
// App Store
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
metaData: {
// Metadata
title: t('title.metadata'),
body: resolveComponent('DashboardSettingsMetadata'),
},
acl: {
// UI Access Control
title: t('title.uiACL'),
body: resolveComponent('DashboardSettingsUIAcl'),
onClick: () => {
$e('c:table:ui-acl')
},
},
erd: {
title: t('title.erdView'),
body: resolveComponent('DashboardSettingsErd'),
onClick: () => {
$e('c:settings:erd')
},
new: {
title: 'Apps',
body: AppStore,
},
misc: {
title: t('general.misc'),
body: resolveComponent('DashboardSettingsMisc'),
},
onClick: () => {
$e('c:settings:appstore')
},
},
dataSources: {
// Data Sources
title: 'Data Sources',
icon: MultipleTableIcon,
subTabs: {
dataSources: {
title: 'Data Sources',
body: DataSources,
},
},
onClick: () => {
$e('c:settings:proj-metadata')
vDataState.value = ''
$e('c:settings:data-sources')
},
},
audit: {
@ -135,29 +125,47 @@ const tabsInfo: TabGroup = {
$e('c:settings:audit')
},
},
projectSettings: {
// Project Settings
title: 'Project Settings',
icon: FolderCog,
subTabs: {
misc: {
// Misc
title: 'Misc',
body: Misc,
},
},
onClick: () => {
$e('c:settings:project-settings')
},
},
}
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
// Array of keys of tabs which are selected. In our case will be only one.
let selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)])
const selectedTabKeys = $computed<string[]>({
get: () => [Object.keys(tabsInfo).find((key) => key === vOpenKey.value) || firstKeyOfObject(tabsInfo)],
set: (value) => {
vOpenKey.value = value[0]
},
})
const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]])
let selectedSubTabKeys = $ref<string[]>([firstKeyOfObject(selectedTab.subTabs)])
const selectedSubTab = $computed(() => selectedTab.subTabs[selectedSubTabKeys[0]])
const handleAwaken = (val: boolean) => {
dataSourcesAwakened.value = val
}
watch(
() => selectedTabKeys[0],
(newTabKey) => {
selectedSubTabKeys = [firstKeyOfObject(tabsInfo[newTabKey].subTabs)]
},
)
watch(
() => props.openKey,
(nextOpenKey) => {
selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === nextOpenKey) || firstKeyOfObject(tabsInfo)]
},
)
</script>
<template>
@ -210,7 +218,12 @@ watch(
<!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<a-menu v-model:selectedKeys="selectedSubTabKeys" :open-keys="[]" mode="horizontal">
<a-menu
v-if="selectedTabKeys[0] !== 'dataSources'"
v-model:selectedKeys="selectedSubTabKeys"
:open-keys="[]"
mode="horizontal"
>
<a-menu-item
v-for="(tab, key) of selectedTab.subTabs"
:key="key"
@ -220,8 +233,59 @@ watch(
{{ tab.title }}
</a-menu-item>
</a-menu>
<div v-else>
<div class="flex items-center">
<a-breadcrumb class="w-full cursor-pointer">
<a-breadcrumb-item v-if="vDataState !== ''" @click="vDataState = ''">
<a class="!no-underline">Data Sources</a>
</a-breadcrumb-item>
<a-breadcrumb-item v-else @click="vDataState = ''">Data Sources</a-breadcrumb-item>
<a-breadcrumb-item v-if="vDataState !== ''">{{ vDataState }}</a-breadcrumb-item>
</a-breadcrumb>
<div v-if="vDataState === ''" class="flex flex-row justify-end items-center w-full gap-1">
<a-button
v-if="dataSourcesAwakened"
class="self-start nc-btn-new-datasource"
@click="vDataState = DataSourcesSubTab.New"
>
<div v-if="vDataState === ''" class="flex items-center gap-2 text-primary font-light">
<MdiDatabasePlusOutline class="text-lg group-hover:text-accent" />
New
</div>
</a-button>
<!-- Reload -->
<a-button
v-e="['a:proj-meta:data-sources:reload']"
class="self-start nc-btn-metasync-reload"
@click="dataSourcesReload = true"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': dataSourcesReload }" />
{{ $t('general.reload') }}
</div>
</a-button>
</div>
</div>
<a-divider style="margin: 10px 0" />
</div>
<component :is="selectedSubTab?.body" class="px-2 py-6" :data-testid="`nc-settings-subtab-${selectedSubTab.title}`" />
<div class="h-[600px]">
<component
:is="selectedSubTab?.body"
v-if="selectedSubTabKeys[0] === 'dataSources'"
v-model:state="vDataState"
v-model:reload="dataSourcesReload"
class="px-2 pb-2"
:data-testid="`nc-settings-subtab-${selectedSubTab.title}`"
@awaken="handleAwaken"
/>
<component
:is="selectedSubTab?.body"
v-else
class="px-2 py-6"
:data-testid="`nc-settings-subtab-${selectedSubTab.title}`"
/>
</div>
</a-layout-content>
</a-layout>
</a-modal>

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

@ -13,6 +13,10 @@ import {
viewIcons,
} from '#imports'
const props = defineProps<{
baseId: string
}>()
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
@ -32,8 +36,9 @@ const searchInput = $ref('')
const filteredTables = computed(() =>
tables.filter(
(el) =>
(typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase())),
el?.base_id === props.baseId &&
((typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase()))),
),
)

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

@ -0,0 +1,615 @@
<script lang="ts" setup>
import { Form, Modal, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { ProjectCreateForm } from '#imports'
import {
CertTypes,
ClientType,
DefaultConnection,
SQLiteConnection,
SSLUsage,
clientTypes,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
generateUniqueName,
getDefaultConnectionConfig,
getTestDatabaseName,
nextTick,
onMounted,
projectTitleValidator,
readFile,
ref,
useApi,
useI18n,
useNuxtApp,
watch,
} from '#imports'
const { connectionType } = defineProps<{ connectionType: ClientType }>()
const emit = defineEmits(['baseCreated'])
const { project, loadProject } = useProject()
const useForm = Form.useForm
const testSuccess = ref(false)
const form = ref<typeof Form>()
const { api } = useApi()
const { $e } = useNuxtApp()
const { t } = useI18n()
const toggleDialog = inject(ToggleDialogInj, () => {})
let formState = $ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
inflection: {
inflectionColumn: 'camelize',
inflectionTable: 'camelize',
},
sslUse: SSLUsage.No,
extraParameters: [],
})
const customFormState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
inflection: {
inflectionColumn: 'camelize',
inflectionTable: 'camelize',
},
sslUse: SSLUsage.No,
extraParameters: [],
})
const validators = computed(() => {
return {
'title': [
{
required: true,
message: 'Base name is required',
},
projectTitleValidator,
],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
...(formState.dataSource.client === ClientType.SQLITE
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
...([ClientType.PG, ClientType.MSSQL].includes(formState.dataSource.client)
? {
'dataSource.searchPath.0': [fieldRequiredValidator()],
}
: {}),
}),
}
})
const { validate, validateInfos } = useForm(formState, validators)
const populateName = (v: string) => {
formState.dataSource.connection.database = `${v.trim()}_noco`
}
const onClientChange = () => {
formState.dataSource = { ...getDefaultConnectionConfig(formState.dataSource.client) }
populateName(formState.title)
}
const onSSLModeChange = ((mode: SSLUsage) => {
if (formState.dataSource.client !== ClientType.SQLITE) {
const connection = formState.dataSource.connection as DefaultConnection
switch (mode) {
case SSLUsage.No:
delete connection.ssl
break
case SSLUsage.Allowed:
connection.ssl = 'true'
break
default:
connection.ssl = {
ca: '',
cert: '',
key: '',
}
break
}
}
}) as SelectHandler
const updateSSLUse = () => {
if (formState.dataSource.client !== ClientType.SQLITE) {
const connection = formState.dataSource.connection as DefaultConnection
if (connection.ssl) {
if (typeof connection.ssl === 'string') {
formState.sslUse = SSLUsage.Allowed
} else {
formState.sslUse = SSLUsage.Preferred
}
} else {
formState.sslUse = SSLUsage.No
}
}
}
const addNewParam = () => {
formState.extraParameters.push({ key: '', value: '' })
}
const removeParam = (index: number) => {
formState.extraParameters.splice(index, 1)
}
const inflectionTypes = ['camelize', 'none']
const importURL = ref('')
const configEditDlg = ref(false)
const importURLDlg = ref(false)
const caFileInput = ref<HTMLInputElement>()
const keyFileInput = ref<HTMLInputElement>()
const certFileInput = ref<HTMLInputElement>()
const onFileSelect = (key: CertTypes, el?: HTMLInputElement) => {
if (!el) return
readFile(el, (content) => {
if ('ssl' in formState.dataSource.connection && typeof formState.dataSource.connection.ssl === 'object')
formState.dataSource.connection.ssl[key] = content ?? ''
})
}
const sslFilesRequired = computed(
() => !!formState.sslUse && formState.sslUse !== SSLUsage.No && formState.sslUse !== SSLUsage.Allowed,
)
function getConnectionConfig() {
const extraParameters = Object.fromEntries(new Map(formState.extraParameters.map((object) => [object.key, object.value])))
const connection = {
...formState.dataSource.connection,
...extraParameters,
}
if ('ssl' in connection && connection.ssl) {
if (
formState.sslUse === SSLUsage.No ||
(typeof connection.ssl === 'object' && Object.values(connection.ssl).every((v) => v === null || v === undefined))
) {
delete connection.ssl
}
}
return connection
}
const focusInvalidInput = () => {
form.value?.$el.querySelector('.ant-form-item-explain-error')?.parentNode?.parentNode?.querySelector('input')?.focus()
}
const createBase = async () => {
try {
await validate()
} catch (e) {
focusInvalidInput()
return
}
try {
if (!project.value?.id) return
const connection = getConnectionConfig()
const config = { ...formState.dataSource, connection }
await api.base.create(project.value?.id, {
alias: formState.title,
type: formState.dataSource.client,
config,
inflection_column: formState.inflection.inflectionColumn,
inflection_table: formState.inflection.inflectionTable,
})
$e('a:base:create:extdb')
await loadProject()
emit('baseCreated')
message.success('Base created!')
toggleDialog(true, 'dataSources', '')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const testConnection = async () => {
try {
await validate()
} catch (e) {
focusInvalidInput()
return
}
$e('a:base:create:extdb:test-connection', [])
try {
if (formState.dataSource.client === ClientType.SQLITE) {
testSuccess.value = true
} else {
const connection = getConnectionConfig()
connection.database = getTestDatabaseName(formState.dataSource)!
const testConnectionConfig = {
...formState.dataSource,
connection,
}
const result = await api.utils.testConnection(testConnectionConfig)
if (result.code === 0) {
testSuccess.value = true
Modal.confirm({
title: t('msg.info.dbConnected'),
icon: null,
type: 'success',
okText: 'Ok & Add Base',
okType: 'primary',
cancelText: t('general.cancel'),
onOk: createBase,
style: 'top: 30%!important',
})
} else {
testSuccess.value = false
message.error(`${t('msg.error.dbConnectionFailed')} ${result.message}`)
}
}
} catch (e: any) {
testSuccess.value = false
message.error(await extractSdkResponseErrorMsg(e))
}
}
const handleImportURL = async () => {
if (!importURL.value || importURL.value === '') return
const connectionConfig = await api.utils.urlToConfig({ url: importURL.value })
if (connectionConfig) {
formState.dataSource.client = connectionConfig.client
formState.dataSource.connection = { ...connectionConfig.connection }
} else {
message.error(t('msg.error.invalidURL'))
}
importURLDlg.value = false
updateSSLUse()
}
const handleEditJSON = () => {
customFormState.value = { ...formState }
configEditDlg.value = true
}
const handleOk = () => {
formState = { ...customFormState.value }
configEditDlg.value = false
updateSSLUse()
}
// reset test status on config change
watch(
() => formState.dataSource,
() => (testSuccess.value = false),
{ deep: true },
)
// populate database name based on title
watch(
() => formState.title,
(v) => populateName(v),
)
// select and focus title field on load
onMounted(async () => {
formState.title = await generateUniqueName()
nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
watch(
() => connectionType,
(v) => {
formState.dataSource.client = v
onClientChange()
},
{ immediate: true },
)
</script>
<template>
<div class="create-base max-w-800px mx-auto bg-white relative flex flex-col justify-center gap-2 w-full p-8">
<h1 class="prose-2xl font-bold self-center my-4">New Base</h1>
<a-form
ref="form"
:model="formState"
name="external-project-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<a-form-item label="Base Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select
v-model:value="formState.dataSource.client"
class="nc-extdb-db-type"
dropdown-class-name="nc-dropdown-ext-db-type"
@change="onClientChange"
>
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
>{{ client.text }}
</a-select-option>
</a-select>
</a-form-item>
<!-- SQLite File -->
<a-form-item
v-if="formState.dataSource.client === ClientType.SQLITE"
:label="$t('labels.sqliteFile')"
v-bind="validateInfos['dataSource.connection.connection.filename']"
>
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).host" class="nc-extdb-host-address" />
</a-form-item>
<!-- Port Number -->
<a-form-item :label="$t('labels.port')" v-bind="validateInfos['dataSource.connection.port']">
<a-input-number
v-model:value="(formState.dataSource.connection as DefaultConnection).port"
class="!w-full nc-extdb-host-port"
/>
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).user" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')">
<a-input-password
v-model:value="(formState.dataSource.connection as DefaultConnection).password"
class="nc-extdb-host-password"
/>
</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
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
:label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.searchPath.0']"
>
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1">
<template #header>
<div class="flex items-center gap-2">
<!-- Use Connection URL -->
<a-button type="default" class="nc-extdb-btn-import-url" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</a-button>
<span>{{ $t('title.advancedParameters') }}</span>
</div>
</template>
<a-form-item label="SSL mode">
<a-select v-model:value="formState.sslUse" dropdown-class-name="nc-dropdown-ssl-mode" @select="onSSLModeChange">
<a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SSL keys">
<div class="flex gap-2">
<a-tooltip placement="top">
<!-- Select .cert file -->
<template #title>
<span>{{ $t('tooltip.clientCert') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" class="shadow" @click="certFileInput?.click()">
{{ $t('labels.clientCert') }}
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select .key file -->
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput?.click()">
{{ $t('labels.clientKey') }}
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select CA file -->
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" class="shadow" @click="caFileInput?.click()">
{{ $t('labels.serverCA') }}
</a-button>
</a-tooltip>
</div>
</a-form-item>
<input ref="caFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.ca, caFileInput)" />
<input ref="certFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.cert, certFileInput)" />
<input ref="keyFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.key, keyFileInput)" />
<a-divider />
<!-- Extra connection parameters -->
<a-form-item class="mb-2" :label="$t('labels.extraConnectionParameters')" v-bind="validateInfos.extraParameters">
<a-card>
<div v-for="(item, index) of formState.extraParameters" :key="index">
<div class="flex py-1 items-center gap-1">
<a-input v-model:value="item.key" />
<span>:</span>
<a-input v-model:value="item.value" />
<MdiClose :style="{ 'font-size': '1.5em', 'color': 'red' }" @click="removeParam(index)" />
</div>
</div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center"><MdiPlus /></div>
</a-button>
</a-card>
</a-form-item>
<a-divider />
<a-form-item :label="$t('labels.inflection.tableName')">
<a-select
v-model:value="formState.inflection.inflectionTable"
dropdown-class-name="nc-dropdown-inflection-table-name"
>
<a-select-option v-for="type in inflectionTypes" :key="type" :value="type">{{ type }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.inflection.columnName')">
<a-select
v-model:value="formState.inflection.inflectionColumn"
dropdown-class-name="nc-dropdown-inflection-column-name"
>
<a-select-option v-for="type in inflectionTypes" :key="type" :value="type">{{ type }}</a-select-option>
</a-select>
</a-form-item>
<div class="flex justify-end">
<a-button class="!shadow-md" @click="handleEditJSON()">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
</a-button>
</div>
</a-collapse-panel>
</a-collapse>
</template>
<a-form-item class="flex justify-center !mt-5">
<div class="flex justify-center gap-2">
<a-button type="primary" ghost class="nc-extdb-btn-test-connection" @click="testConnection">
{{ $t('activity.testDbConn') }}
</a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="createBase">
{{ $t('general.submit') }}
</a-button>
</div>
</a-form-item>
</a-form>
<a-modal
v-model:visible="configEditDlg"
:title="$t('activity.editConnJson')"
width="600px"
wrap-class-name="nc-modal-edit-connection-json"
@ok="handleOk"
>
<MonacoEditor v-if="configEditDlg" v-model="customFormState" class="h-[400px] w-full" />
</a-modal>
<!-- Use Connection URL -->
<a-modal
v-model:visible="importURLDlg"
:title="$t('activity.useConnectionUrl')"
width="600px"
:ok-text="$t('general.ok')"
:cancel-text="$t('general.cancel')"
wrap-class-name="nc-modal-connection-url"
@ok="handleImportURL"
>
<a-input v-model:value="importURL" />
</a-modal>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-collapse-header) {
@apply !pr-10 !-mt-4 text-right justify-end;
}
:deep(.ant-collapse-content-box) {
@apply !px-0;
}
:deep(.ant-form-item-explain-error) {
@apply !text-xs;
}
:deep(.ant-form-item) {
@apply mb-2;
}
:deep(.ant-form-item-with-help .ant-form-item-explain) {
@apply !min-h-0;
}
.create-base {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {
@apply !appearance-none border-1 border-solid rounded;
}
:deep(.ant-input-password) {
input {
@apply !border-none my-0;
}
}
}
</style>

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

@ -0,0 +1,612 @@
<script lang="ts" setup>
import type { BaseType } from 'nocodb-sdk'
import { Form, Modal, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { ProjectCreateForm } from '#imports'
import {
CertTypes,
ClientType,
DefaultConnection,
SQLiteConnection,
SSLUsage,
clientTypes,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
getDefaultConnectionConfig,
getTestDatabaseName,
onMounted,
projectTitleValidator,
readFile,
ref,
useApi,
useI18n,
useNuxtApp,
watch,
} from '#imports'
const props = defineProps<{
baseId: string
}>()
const emit = defineEmits(['baseUpdated'])
const { project, loadProject } = useProject()
const useForm = Form.useForm
const testSuccess = ref(false)
const form = ref<typeof Form>()
const { api } = useApi()
const { $e } = useNuxtApp()
const { t } = useI18n()
const toggleDialog = inject(ToggleDialogInj, () => {})
const formState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
inflection: {
inflectionColumn: 'camelize',
inflectionTable: 'camelize',
},
sslUse: SSLUsage.No,
extraParameters: [],
})
const customFormState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
inflection: {
inflectionColumn: 'camelize',
inflectionTable: 'camelize',
},
sslUse: SSLUsage.No,
extraParameters: [],
})
const validators = computed(() => {
return {
'title': [
{
required: true,
message: 'Base name is required',
},
projectTitleValidator,
],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
...(formState.value.dataSource.client === ClientType.SQLITE
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
...([ClientType.PG, ClientType.MSSQL].includes(formState.value.dataSource.client)
? {
'dataSource.searchPath.0': [fieldRequiredValidator()],
}
: {}),
}),
}
})
const { validate, validateInfos } = useForm(formState, validators)
const onClientChange = () => {
formState.value.dataSource = { ...getDefaultConnectionConfig(formState.value.dataSource.client) }
}
const onSSLModeChange = ((mode: SSLUsage) => {
if (formState.value.dataSource.client !== ClientType.SQLITE) {
const connection = formState.value.dataSource.connection as DefaultConnection
switch (mode) {
case SSLUsage.No:
delete connection.ssl
break
case SSLUsage.Allowed:
connection.ssl = 'true'
break
default:
connection.ssl = {
ca: '',
cert: '',
key: '',
}
break
}
}
}) as SelectHandler
const updateSSLUse = () => {
if (formState.value.dataSource.client !== ClientType.SQLITE) {
const connection = formState.value.dataSource.connection as DefaultConnection
if (connection.ssl) {
if (typeof connection.ssl === 'string') {
formState.value.sslUse = SSLUsage.Allowed
} else {
formState.value.sslUse = SSLUsage.Preferred
}
} else {
formState.value.sslUse = SSLUsage.No
}
}
}
const addNewParam = () => {
formState.value.extraParameters.push({ key: '', value: '' })
}
const removeParam = (index: number) => {
formState.value.extraParameters.splice(index, 1)
}
const inflectionTypes = ['camelize', 'none']
const importURL = ref('')
const configEditDlg = ref(false)
const importURLDlg = ref(false)
const caFileInput = ref<HTMLInputElement>()
const keyFileInput = ref<HTMLInputElement>()
const certFileInput = ref<HTMLInputElement>()
const onFileSelect = (key: CertTypes, el?: HTMLInputElement) => {
if (!el) return
readFile(el, (content) => {
if ('ssl' in formState.value.dataSource.connection && typeof formState.value.dataSource.connection.ssl === 'object')
formState.value.dataSource.connection.ssl[key] = content ?? ''
})
}
const sslFilesRequired = computed(
() => !!formState.value.sslUse && formState.value.sslUse !== SSLUsage.No && formState.value.sslUse !== SSLUsage.Allowed,
)
function getConnectionConfig() {
const extraParameters = Object.fromEntries(new Map(formState.value.extraParameters.map((object) => [object.key, object.value])))
const connection = {
...formState.value.dataSource.connection,
...extraParameters,
}
if ('ssl' in connection && connection.ssl) {
if (
formState.value.sslUse === SSLUsage.No ||
(typeof connection.ssl === 'object' && Object.values(connection.ssl).every((v) => v === null || v === undefined))
) {
delete connection.ssl
}
}
return connection
}
const focusInvalidInput = () => {
form.value?.$el.querySelector('.ant-form-item-explain-error')?.parentNode?.parentNode?.querySelector('input')?.focus()
}
const editBase = async () => {
try {
await validate()
} catch (e) {
focusInvalidInput()
return
}
try {
if (!project.value?.id) return
const connection = getConnectionConfig()
const config = { ...formState.value.dataSource, connection }
await api.base.update(project.value?.id, props.baseId, {
alias: formState.value.title,
type: formState.value.dataSource.client,
config,
inflection_column: formState.value.inflection.inflectionColumn,
inflection_table: formState.value.inflection.inflectionTable,
})
$e('a:base:edit:extdb')
await loadProject()
emit('baseUpdated')
message.success('Base updated!')
toggleDialog(true, 'dataSources', '')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const testConnection = async () => {
try {
await validate()
} catch (e) {
focusInvalidInput()
return
}
$e('a:base:edit:extdb:test-connection', [])
try {
if (formState.value.dataSource.client === ClientType.SQLITE) {
testSuccess.value = true
} else {
const connection = getConnectionConfig()
connection.database = getTestDatabaseName(formState.value.dataSource)!
const testConnectionConfig = {
...formState.value.dataSource,
connection,
}
const result = await api.utils.testConnection(testConnectionConfig)
if (result.code === 0) {
testSuccess.value = true
Modal.confirm({
title: t('msg.info.dbConnected'),
icon: null,
type: 'success',
okText: 'Ok & Edit Base',
okType: 'primary',
cancelText: t('general.cancel'),
onOk: editBase,
style: 'top: 30%!important',
})
} else {
testSuccess.value = false
message.error(`${t('msg.error.dbConnectionFailed')} ${result.message}`)
}
}
} catch (e: any) {
testSuccess.value = false
message.error(await extractSdkResponseErrorMsg(e))
}
}
const handleImportURL = async () => {
if (!importURL.value || importURL.value === '') return
const connectionConfig = await api.utils.urlToConfig({ url: importURL.value })
if (connectionConfig) {
formState.value.dataSource.client = connectionConfig.client
formState.value.dataSource.connection = { ...connectionConfig.connection }
} else {
message.error(t('msg.error.invalidURL'))
}
importURLDlg.value = false
updateSSLUse()
}
const handleEditJSON = () => {
customFormState.value = formState.value
configEditDlg.value = true
}
const handleOk = () => {
formState.value = customFormState.value
configEditDlg.value = false
updateSSLUse()
}
// reset test status on config change
watch(
() => formState.value.dataSource,
() => (testSuccess.value = false),
{ deep: true },
)
// load base config
onMounted(async () => {
if (project.value?.id) {
const definedParameters = ['host', 'port', 'user', 'password', 'database']
const activeBase = (await api.base.read(project.value?.id, props.baseId)) as BaseType
const tempParameters = Object.entries(activeBase.config.connection)
.filter(([key]) => !definedParameters.includes(key))
.map(([key, value]) => ({ key: key as string, value: value as string }))
formState.value = {
title: activeBase.alias || '',
dataSource: activeBase.config,
inflection: {
inflectionColumn: activeBase.inflection_column,
inflectionTable: activeBase.inflection_table,
},
extraParameters: tempParameters,
sslUse: SSLUsage.No,
}
updateSSLUse()
}
})
</script>
<template>
<div class="edit-base max-w-800px mx-auto bg-white relative flex flex-col justify-center gap-2 w-full p-8">
<h1 class="prose-2xl font-bold self-center my-4">Edit Base</h1>
<a-form
ref="form"
:model="formState"
name="external-project-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<a-form-item label="Base Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select
v-model:value="formState.dataSource.client"
class="nc-extdb-db-type"
dropdown-class-name="nc-dropdown-ext-db-type"
@change="onClientChange"
>
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
>{{ client.text }}
</a-select-option>
</a-select>
</a-form-item>
<!-- SQLite File -->
<a-form-item
v-if="formState.dataSource.client === ClientType.SQLITE"
:label="$t('labels.sqliteFile')"
v-bind="validateInfos['dataSource.connection.connection.filename']"
>
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).host" class="nc-extdb-host-address" />
</a-form-item>
<!-- Port Number -->
<a-form-item :label="$t('labels.port')" v-bind="validateInfos['dataSource.connection.port']">
<a-input-number
v-model:value="(formState.dataSource.connection as DefaultConnection).port"
class="!w-full nc-extdb-host-port"
/>
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).user" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')">
<a-input-password
v-model:value="(formState.dataSource.connection as DefaultConnection).password"
class="nc-extdb-host-password"
/>
</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
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
:label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.searchPath.0']"
>
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1">
<template #header>
<div class="flex items-center gap-2">
<!-- Use Connection URL -->
<a-button type="default" class="nc-extdb-btn-import-url" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</a-button>
<span>{{ $t('title.advancedParameters') }}</span>
</div>
</template>
<a-form-item label="SSL mode">
<a-select v-model:value="formState.sslUse" dropdown-class-name="nc-dropdown-ssl-mode" @select="onSSLModeChange">
<a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SSL keys">
<div class="flex gap-2">
<a-tooltip placement="top">
<!-- Select .cert file -->
<template #title>
<span>{{ $t('tooltip.clientCert') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" class="shadow" @click="certFileInput?.click()">
{{ $t('labels.clientCert') }}
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select .key file -->
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput?.click()">
{{ $t('labels.clientKey') }}
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select CA file -->
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" class="shadow" @click="caFileInput?.click()">
{{ $t('labels.serverCA') }}
</a-button>
</a-tooltip>
</div>
</a-form-item>
<input ref="caFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.ca, caFileInput)" />
<input ref="certFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.cert, certFileInput)" />
<input ref="keyFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.key, keyFileInput)" />
<a-divider />
<!-- Extra connection parameters -->
<a-form-item class="mb-2" :label="$t('labels.extraConnectionParameters')" v-bind="validateInfos.extraParameters">
<a-card>
<div v-for="(item, index) of formState.extraParameters" :key="index">
<div class="flex py-1 items-center gap-1">
<a-input v-model:value="item.key" />
<span>:</span>
<a-input v-model:value="item.value" />
<MdiClose :style="{ 'font-size': '1.5em', 'color': 'red' }" @click="removeParam(index)" />
</div>
</div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center"><MdiPlus /></div>
</a-button>
</a-card>
</a-form-item>
<a-divider />
<a-form-item :label="$t('labels.inflection.tableName')">
<a-select
v-model:value="formState.inflection.inflectionTable"
dropdown-class-name="nc-dropdown-inflection-table-name"
>
<a-select-option v-for="type in inflectionTypes" :key="type" :value="type">{{ type }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.inflection.columnName')">
<a-select
v-model:value="formState.inflection.inflectionColumn"
dropdown-class-name="nc-dropdown-inflection-column-name"
>
<a-select-option v-for="type in inflectionTypes" :key="type" :value="type">{{ type }}</a-select-option>
</a-select>
</a-form-item>
<div class="flex justify-end">
<a-button class="!shadow-md" @click="handleEditJSON()">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
</a-button>
</div>
</a-collapse-panel>
</a-collapse>
</template>
<a-form-item class="flex justify-center !mt-5">
<div class="flex justify-center gap-2">
<a-button type="primary" ghost class="nc-extdb-btn-test-connection" @click="testConnection">
{{ $t('activity.testDbConn') }}
</a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="editBase">
{{ $t('general.submit') }}
</a-button>
</div>
</a-form-item>
<div class="w-full flex items-center mt-2 text-[#e65100]">
<MdiWarning class="mr-1" />
Please make sure database you are trying to connect is valid! This operation can cause schema loss!!
</div>
</a-form>
<a-modal
v-model:visible="configEditDlg"
:title="$t('activity.editConnJson')"
width="600px"
wrap-class-name="nc-modal-edit-connection-json"
@ok="handleOk"
>
<MonacoEditor v-if="configEditDlg" v-model="customFormState" class="h-[400px] w-full" />
</a-modal>
<!-- Use Connection URL -->
<a-modal
v-model:visible="importURLDlg"
:title="$t('activity.useConnectionUrl')"
width="600px"
:ok-text="$t('general.ok')"
:cancel-text="$t('general.cancel')"
wrap-class-name="nc-modal-connection-url"
@ok="handleImportURL"
>
<a-input v-model:value="importURL" />
</a-modal>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-collapse-header) {
@apply !pr-10 !-mt-4 text-right justify-end;
}
:deep(.ant-collapse-content-box) {
@apply !px-0;
}
:deep(.ant-form-item-explain-error) {
@apply !text-xs;
}
:deep(.ant-form-item) {
@apply mb-2;
}
:deep(.ant-form-item-with-help .ant-form-item-explain) {
@apply !min-h-0;
}
.edit-base {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {
@apply !appearance-none border-1 border-solid rounded;
}
:deep(.ant-input-password) {
input {
@apply !border-none my-0;
}
}
}
</style>

7
packages/nc-gui/components/dlg/AirtableImport.vue

@ -18,8 +18,9 @@ import {
watch,
} from '#imports'
const { modelValue } = defineProps<{
const { modelValue, baseId } = defineProps<{
modelValue: boolean
baseId: string
}>()
const emit = defineEmits(['update:modelValue'])
@ -100,7 +101,7 @@ async function createOrUpdate() {
body: payload,
})
} else {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs`, {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
@ -113,7 +114,7 @@ async function createOrUpdate() {
}
async function loadSyncSrc() {
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs`, {
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, {
baseURL,
method: 'GET',
headers: { 'xc-auth': $state.token.value as string },

2
packages/nc-gui/components/dlg/QuickImport.vue

@ -27,6 +27,7 @@ import type { importFileList, streamImportFileList } from '~/lib'
interface Props {
modelValue: boolean
importType: 'csv' | 'json' | 'excel'
baseId: string
importDataOnly?: boolean
}
@ -364,6 +365,7 @@ const beforeUpload = (file: UploadFile) => {
:import-data-only="importDataOnly"
:quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse"
:base-id="baseId"
class="nc-quick-import-template-editor"
@import="handleImport"
/>

9
packages/nc-gui/components/dlg/TableCreate.vue

@ -4,6 +4,7 @@ import { TabType } from '~/lib'
const props = defineProps<{
modelValue: boolean
baseId: string
}>()
const emit = defineEmits(['update:modelValue'])
@ -28,7 +29,7 @@ const { table, createTable, generateUniqueTitle, tables, project } = useTable(as
})
dialogShow.value = false
})
}, props.baseId)
const useForm = Form.useForm
@ -51,11 +52,11 @@ const validators = computed(() => {
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql) {
if (isMysql(props.baseId)) {
tableNameLengthLimit = 64
} else if (isPg) {
} else if (isPg(props.baseId)) {
tableNameLengthLimit = 63
} else if (isMssql) {
} else if (isMssql(props.baseId)) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''

9
packages/nc-gui/components/dlg/TableRename.vue

@ -21,9 +21,10 @@ import {
interface Props {
modelValue?: boolean
tableMeta: TableType
baseId: string
}
const { tableMeta, ...props } = defineProps<Props>()
const { tableMeta, baseId, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
@ -57,11 +58,11 @@ const validators = computed(() => {
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql) {
if (isMysql(baseId)) {
tableNameLengthLimit = 64
} else if (isPg) {
} else if (isPg(baseId)) {
tableNameLengthLimit = 63
} else if (isMssql) {
} else if (isMssql(baseId)) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''

19
packages/nc-gui/components/erd/View.vue

@ -4,7 +4,7 @@ import { UITypes } from 'nocodb-sdk'
import type { ERDConfig } from './utils'
import { reactive, ref, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>()
const props = defineProps<{ table?: TableType; baseId?: string }>()
const { tables: projectTables } = useProject()
@ -18,7 +18,7 @@ const config = reactive<ERDConfig>({
showPkAndFk: true,
showViews: false,
showAllColumns: true,
singleTableMode: !!table,
singleTableMode: !!props.table,
showMMTables: false,
showJunctionTableNames: false,
})
@ -34,14 +34,13 @@ const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
}
const populateTables = async () => {
let localTables: TableType[]
if (table) {
let localTables: TableType[] = []
if (props.table) {
// if table is provided only get the table and its related tables
localTables = projectTables.value.filter(
(t) =>
t.id === table.id ||
table.columns?.find(
t.id === props.table.id ||
props.table.columns?.find(
(column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,
@ -59,7 +58,7 @@ const populateTables = async () => {
config.showMMTables ||
(!config.showMMTables && !t.mm) ||
// Show mm table if it's the selected table
t.id === table?.id,
t.id === props.table?.id,
)
.filter((t) => config.singleTableMode || (!config.showViews && t.type !== 'view') || config.showViews)
@ -76,6 +75,8 @@ watch(config, populateTables, {
deep: true,
})
const filteredTables = computed(() => tables.value.filter((t) => !props.baseId || t.base_id === props.baseId))
watch(
() => config.showAllColumns,
() => {
@ -87,7 +88,7 @@ watch(
<template>
<div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]">
<div class="relative h-full">
<LazyErdFlow :tables="tables" :config="config">
<LazyErdFlow :tables="filteredTables" :config="config">
<GeneralOverlay v-model="isLoading" inline class="bg-gray-300/50">
<div class="h-full w-full flex flex-col justify-center items-center">
<a-spin size="large" />

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

@ -0,0 +1,22 @@
<script setup lang="ts">
const { isUIAllowed } = useUIPermission()
const { t } = useI18n()
const toggleDialog = inject(ToggleDialogInj, () => {})
</script>
<template>
<div
v-if="isUIAllowed('settings')"
class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)"
@click="toggleDialog(true)"
>
<div>
<div class="flex items-center space-x-1">
<RiTeamFill class="mr-1 nc-new-base" />
<div>{{ t('title.teamAndSettings') }}</div>
</div>
</div>
</div>
</template>

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

@ -0,0 +1,28 @@
<script setup lang="ts">
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 MdiDatabaseOutline from '~icons/mdi/database-outline'
const { baseType } = defineProps<{ baseType?: string }>()
const baseIcon = computed(() => {
switch (baseType) {
case ClientType.MYSQL:
return LogosMysqlIcon
case ClientType.PG:
return LogosPostgresql
case ClientType.SQLITE:
return VscodeIconsFileTypeSqlite
case ClientType.MSSQL:
return SimpleIconsMicrosoftsqlserver
default:
return MdiDatabaseOutline
}
})
</script>
<template>
<component :is="baseIcon" />
</template>

15
packages/nc-gui/components/general/ShareBaseButton.vue

@ -37,13 +37,16 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</script>
<template>
<div class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)" @click="showUserModal = true">
<div class="flex items-center h-full" @click="showUserModal = true">
<div v-if="isShareBaseAllowed">
<div class="flex items-center space-x-1">
<MdiAccountPlusOutline class="mr-1 nc-share-base" />
<div>{{ $t('activity.inviteTeam') }}</div>
</div>
<a-tooltip placement="left">
<template #title>
<span class="text-xs">{{ $t('activity.inviteTeam') }}</span>
</template>
<div class="flex items-center space-x-1 cursor-pointer">
<MdiAccountPlusOutline class="mr-1 nc-share-base text-gray-300 hover:text-accent" />
</div>
</a-tooltip>
</div>
<LazyTabsAuthUserManagementUsersModal :show="showUserModal" @closed="showUserModal = false" />

11
packages/nc-gui/components/general/Tooltip.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core'
import type { CSSProperties } from '@vue/runtime-dom'
import { controlledRef, ref, useElementHover, watch } from '#imports'
import { controlledRef, ref, useAttrs, useElementHover, watch } from '#imports'
interface Props {
// Key to be pressed on hover to trigger the tooltip
@ -23,6 +23,8 @@ const showTooltip = controlledRef(false, {
const isHovering = useElementHover(() => el.value)
const attrs = useAttrs()
const isKeyPressed = ref(false)
onKeyStroke(
@ -73,6 +75,11 @@ watch([isHovering, () => modifierKey, () => disabled], ([hovering, key, isDisabl
showTooltip.value = true
}
})
const divStyles = $computed(() => ({
style: attrs.style as CSSProperties,
class: attrs.class as string,
}))
</script>
<template>
@ -81,7 +88,7 @@ watch([isHovering, () => modifierKey, () => disabled], ([hovering, key, isDisabl
<slot name="title" />
</template>
<div ref="el" class="w-full" :class="$attrs.class" :style="$attrs.style">
<div ref="el" class="w-full" v-bind="divStyles">
<slot />
</div>
</a-tooltip>

9
packages/nc-gui/components/smartsheet/ApiSnippet.vue

@ -172,13 +172,8 @@ watch($$(activeLang), (newLang) => {
hide-minimap
/>
<div v-if="activeLang.clients" class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select
v-if="activeLang"
v-model:value="selectedClient"
style="width: 6rem"
dropdown-class-name="nc-dropdown-snippet-active-lang"
>
<div v-if="activeLang?.clients" class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select v-model:value="selectedClient" style="width: 6rem" dropdown-class-name="nc-dropdown-snippet-active-lang">
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>

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

@ -82,7 +82,9 @@ const isLocked = inject(IsLockedInj, ref(false))
const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUi } = useProject()
const { sqlUis } = useProject()
const sqlUi = ref(column.value?.base_id ? sqlUis.value[column.value?.base_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

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

@ -92,6 +92,7 @@ const contextMenu = computed({
},
})
const routeQuery = $computed(() => route.query as Record<string, string>)
const contextMenuTarget = ref<{ row: number; col: number } | null>(null)
const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>()
@ -226,7 +227,6 @@ 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
scrollToCell?.()
@ -234,7 +234,6 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
return true
case 'ArrowDown':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowDown' })
selectedCell.row = data.value.length - 1
selectedCell.col = selectedCell.col ?? 0
scrollToCell?.()
@ -242,7 +241,6 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
return true
case 'ArrowRight':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowRight' })
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = fields.value?.length - 1
scrollToCell?.()
@ -250,7 +248,6 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa
return true
case 'ArrowLeft':
e.preventDefault()
$e('c:shortcut', { key: 'CTRL + ArrowLeft' })
selectedCell.row = selectedCell.row ?? 0
selectedCell.col = 0
scrollToCell?.()
@ -365,7 +362,7 @@ function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false)
if (rowId) {
router.push({
query: {
...route.query,
...routeQuery,
rowId,
},
})
@ -569,13 +566,13 @@ onBeforeUnmount(() => {
const expandedFormOnRowIdDlg = computed({
get() {
return !!route.query.rowId
return !!routeQuery.rowId
},
set(val) {
if (!val)
router.push({
query: {
...route.query,
...routeQuery,
rowId: undefined,
},
})
@ -903,11 +900,11 @@ const closeAddColumnDropdown = () => {
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
:key="route.query.rowId"
:key="routeQuery.rowId"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:row-id="route.query.rowId"
:row-id="routeQuery.rowId"
:view="view"
/>
</Suspense>

2
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -24,7 +24,7 @@ import { NavigateDir } from '~/lib'
const props = defineProps<{
column: ColumnType
modelValue: any
row: Row
row?: Row
active?: boolean
}>()

6
packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import { computed, useColumnCreateStoreOrThrow, useProject, useVModel } from '#imports'
import { computed, useColumnCreateStoreOrThrow, useVModel } from '#imports'
const props = defineProps<{
value: any
@ -10,9 +10,7 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { sqlUi } = useProject()
const { onAlter, onDataTypeChange, validateInfos } = useColumnCreateStoreOrThrow()
const { onAlter, onDataTypeChange, validateInfos, sqlUi } = useColumnCreateStoreOrThrow()
// todo: 2nd argument of `getDataTypeListForUiType` is missing!
const dataTypes = computed(() => sqlUi.value.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any))

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

@ -49,14 +49,12 @@ const validators = {
],
}
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, isPg } = useColumnCreateStoreOrThrow()
setAdditionalValidations({
...validators,
})
const { isPg } = useProject()
const currencyList = currencyCodes || []
const currencyLocaleList = currencyLocales() || []
@ -64,7 +62,7 @@ const currencyLocaleList = currencyLocales() || []
const isMoney = computed(() => vModel.value.dt === 'money')
const message = computed(() => {
if (isMoney.value && isPg) return "PostgreSQL 'money' type has own currency settings"
if (isMoney.value && isPg.value) return "PostgreSQL 'money' type has own currency settings"
return ''
})

2
packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -4,7 +4,7 @@ import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#imports'
interface Props {
column?: ColumnType & { meta: any }
column?: ColumnType
columnPosition?: Pick<ColumnReqType, 'column_order'>
}

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

@ -2,8 +2,8 @@
import type { Ref } from 'vue'
import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep'
import type { ColumnType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk'
import type { ColumnType, FormulaType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import {
MetaInj,
NcAutocompleteTree,
@ -604,7 +604,16 @@ function scrollToSelectedOption() {
}
// set default value
vModel.value.formula_raw = (column?.value?.colOptions as Record<string, any>)?.formula_raw || ''
if ((column.value?.colOptions as any)?.formula_raw) {
vModel.value.formula_raw =
substituteColumnIdWithAliasInFormula(
(column.value?.colOptions as FormulaType)?.formula,
meta?.value?.columns as ColumnType[],
(column.value?.colOptions as any)?.formula_raw,
) || ''
} else {
vModel.value.formula_raw = ''
}
// set additional validations
setAdditionalValidations({

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

@ -14,9 +14,9 @@ const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi } = useColumnCreateStoreOrThrow()
const { tables, sqlUi } = $(useProject())
const { tables } = $(useProject())
setAdditionalValidations({
childId: [{ required: true, message: 'Required' }],
@ -44,8 +44,10 @@ const refTables = $computed(() => {
return []
}
return tables.filter((t) => t.type === ModelTypes.TABLE)
return tables.filter((t) => t.type === ModelTypes.TABLE && t.base_id === meta?.base_id)
})
const filterOption = (value: string, option: { key: string }) => option.key.toLowerCase().includes(value.toLowerCase())
</script>
<template>
@ -66,11 +68,11 @@ const refTables = $computed(() => {
<a-select
v-model:value="vModel.childId"
show-search
:filter-option="(value, option) => option.key.toLowerCase().includes(value.toLowerCase())"
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<a-select-option v-for="table in refTables" :key="table.title" :value="table.id">
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
{{ table.title }}
</a-select-option>
</a-select>
@ -96,7 +98,7 @@ const refTables = $computed(() => {
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
@ -110,7 +112,7 @@ const refTables = $computed(() => {
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>

39
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { getRelationName } from './utils'
import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
const props = defineProps<{
@ -27,36 +28,28 @@ setAdditionalValidations({
if (!vModel.value.fk_relation_column_id) vModel.value.fk_relation_column_id = null
if (!vModel.value.fk_lookup_column_id) vModel.value.fk_lookup_column_id = null
const relationNames = {
mm: 'Many To Many',
hm: 'Has Many',
bt: 'Belongs To',
}
const refTables = $computed(() => {
if (!tables || !tables.length) {
if (!tables || !tables.length || !meta || !meta.columns) {
return []
}
return meta?.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && !c.system)
.map((c: ColumnType) => ({
col: c.colOptions,
column: c,
...tables.find((t) => t.id === (c.colOptions as LinkToAnotherRecordType).fk_related_model_id),
const _refTables = meta.columns
.filter((column) => column.uidt === UITypes.LinkToAnotherRecord && !column.system && column.base_id === meta?.base_id)
.map((column) => ({
col: column.colOptions,
column,
...tables.find((table) => table.id === (column.colOptions as LinkToAnotherRecordType).fk_related_model_id),
}))
.filter((table: any) => table.col.fk_related_model_id === table.id && !table.mm)
.filter((table) => (table.col as LinkToAnotherRecordType)?.fk_related_model_id === table.id && !table.mm)
return _refTables as Required<TableType & { column: ColumnType; col: Required<LinkToAnotherRecordType> }>[]
})
const columns = $computed(() => {
const selectedTable = refTables?.find((t) => t.column.id === vModel.value.fk_relation_column_id)
const columns = $computed<ColumnType[]>(() => {
const selectedTable = refTables.find((t) => t.column.id === vModel.value.fk_relation_column_id)
if (!selectedTable?.id) {
return []
}
return metas[selectedTable.id].columns.filter((c: any) => {
return !(isSystemColumn(c) || c.uidt === UITypes.QrCode)
})
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isSystemColumn(c))
})
</script>
@ -69,11 +62,11 @@ const columns = $computed(() => {
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange"
>
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600">
{{ relationNames[table.col.type] }} {{ table.title || table.table_name }}
{{ getRelationName(table.col.type) }} {{ table.title || table.table_name }}
</div>
</div>
</a-select-option>

5
packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue

@ -25,10 +25,7 @@ const { setAdditionalValidations, validateInfos, column } = useColumnCreateStore
const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) =>
el.fk_column_id &&
// AllowedColumnTypesForQrCode.map((el) => el.toString()).includes(metaColumnById.value[el.fk_column_id].uidt),
AllowedColumnTypesForQrCode.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
(el) => el.fk_column_id && AllowedColumnTypesForQrCode.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {

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

@ -1,5 +1,7 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { getRelationName } from './utils'
import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
const props = defineProps<{
@ -24,11 +26,6 @@ setAdditionalValidations({
rollup_function: [{ required: true, message: 'Required' }],
})
const relationNames = {
mm: 'Many To Many',
hm: 'Has Many',
}
const aggrFunctionsList = [
{ text: 'count', value: 'count' },
{ text: 'min', value: 'min' },
@ -45,19 +42,24 @@ if (!vModel.value.fk_rollup_column_id) vModel.value.fk_rollup_column_id = null
if (!vModel.value.rollup_function) vModel.value.rollup_function = null
const refTables = $computed(() => {
if (!tables || !tables.length) {
if (!tables || !tables.length || !meta || !meta.columns) {
return []
}
return (
meta?.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system)
.map((c) => ({
col: c.colOptions,
column: c,
...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id),
})) ?? []
)
const _refTables = meta.columns
.filter(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
(c.colOptions as LinkToAnotherRecordType).type !== 'bt' &&
!c.system &&
c.base_id === meta?.base_id,
)
.map((c) => ({
col: c.colOptions,
column: c,
...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id),
}))
return _refTables as Required<TableType & { column: ColumnType; col: Required<LinkToAnotherRecordType> }>[]
})
const columns = $computed(() => {
@ -67,7 +69,7 @@ const columns = $computed(() => {
return []
}
return metas[selectedTable.id].columns.filter((c: any) => !isVirtualCol(c.uidt) && !isSystemColumn(c))
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isVirtualCol(c.uidt as UITypes) && !isSystemColumn(c))
})
</script>
@ -80,11 +82,11 @@ const columns = $computed(() => {
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange"
>
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<a-select-option v-for="(table, i) of refTables" :key="i" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600">
({{ relationNames[table.col.type] }} {{ table.title || table.table_name }})
({{ getRelationName(table.col.type) }} {{ table.title || table.table_name }})
</div>
</div>
</a-select-option>

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

@ -11,9 +11,7 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { isPg, isMysql } = useProject()
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, isPg, isMysql } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([])

9
packages/nc-gui/components/smartsheet/column/utils.ts

@ -0,0 +1,9 @@
const relationNames = {
mm: 'Many To Many',
hm: 'Has Many',
bt: 'Belongs To',
} as const
export function getRelationName(type: string) {
return relationNames[type as keyof typeof relationNames]
}

7
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -2,7 +2,12 @@
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { ColumnInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useUIPermission } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; required?: boolean | number; hideMenu?: boolean }>()
interface Props {
column: ColumnType
required?: boolean | number
hideMenu?: boolean
}
const props = defineProps<Props>()
const hideMenu = toRef(props, 'hideMenu')

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

@ -19,6 +19,7 @@ import {
isJSON,
isPercent,
isPhoneNumber,
isPrimaryKey,
isRating,
isSet,
isSingleSelect,
@ -118,7 +119,9 @@ export default defineComponent({
const column = inject(ColumnInj, columnMeta)
const { sqlUi } = useProject()
const { sqlUis } = useProject()
const sqlUi = ref(column.value?.base_id ? sqlUis.value[column.value?.base_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))

2
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -124,7 +124,7 @@ const closeAddColumnDropdown = () => {
<span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span>
</a-tooltip>
<span v-if="isVirtualColRequired(column, meta.columns) || required" class="text-red-500">&nbsp;*</span>
<span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span>
<template v-if="!hideMenu">
<div class="flex-1" />

12
packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue

@ -1,5 +1,15 @@
<script setup lang="ts">
import { IsLockedInj, IsPublicInj, useKanbanViewStoreOrThrow, useMenuCloseOnEsc } from '#imports'
import {
IsKanbanInj,
IsLockedInj,
IsPublicInj,
inject,
provide,
ref,
useKanbanViewStoreOrThrow,
useMenuCloseOnEsc,
useUIPermission,
} from '#imports'
const { isUIAllowed } = useUIPermission()

4
packages/nc-gui/components/smartsheet/toolbar/MoreActions.vue

@ -14,6 +14,7 @@ import {
useI18n,
useNuxtApp,
useProject,
useSharedView,
useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports'
@ -38,6 +39,8 @@ const selectedView = inject(ActiveViewInj, ref())
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { exportFile: sharedViewExportFile } = useSharedView()
const isLocked = inject(IsLockedInj)
const showWebhookDrawer = ref(false)
@ -58,7 +61,6 @@ const exportFile = async (exportType: ExportTypes) => {
while (!isNaN(offset) && offset > -1) {
let res
if (isPublicView.value) {
const { exportFile: sharedViewExportFile } = useSharedView()
res = await sharedViewExportFile(fields.value, offset, exportType, responseType)
} else {
res = await $api.dbViewRow.export(

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

@ -1,4 +1,5 @@
<script lang="ts" setup>
import type { TableType } from 'nocodb-sdk'
import {
ActiveViewInj,
ReloadViewDataHookInj,
@ -25,9 +26,9 @@ const searchDropdown = ref(null)
onClickOutside(searchDropdown, () => (isDropdownOpen.value = false))
const columns = computed(() =>
meta.value?.columns?.map((c) => ({
value: c.id,
label: c.title,
(meta.value as TableType)?.columns?.map((column) => ({
value: column.id,
label: column.title,
})),
)

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

@ -40,6 +40,11 @@ const columnByID = computed(() =>
}, {} as Record<string, ColumnType>),
)
const getColumnUidtByID = (key?: string) => {
if (!key) return ''
return columnByID.value[key]?.uidt || ''
}
watch(
() => view.value?.id,
(viewId) => {
@ -74,7 +79,7 @@ useMenuCloseOnEsc(open)
data-testid="nc-sorts-menu"
>
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i">
<template v-for="(sort, i) of sorts" :key="i">
<MdiCloseBox class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" />
<LazySmartsheetToolbarFieldListAutoCompleteDropdown
@ -95,7 +100,7 @@ useMenuCloseOnEsc(open)
@select="saveOrUpdate(sort, i)"
>
<a-select-option
v-for="(option, j) in getSortDirectionOptions(columnByID[sort.fk_column_id]?.uidt)"
v-for="(option, j) of getSortDirectionOptions(getColumnUidtByID(sort.fk_column_id))"
:key="j"
:value="option.value"
>

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

@ -4,6 +4,7 @@ import {
IsLockedInj,
IsPublicInj,
extractSdkResponseErrorMsg,
getViewIcon,
inject,
message,
ref,
@ -13,7 +14,6 @@ import {
useProject,
useSmartsheetStoreOrThrow,
useUIPermission,
viewIcons,
} from '#imports'
import { LockType } from '~/lib'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
@ -30,9 +30,11 @@ const isView = false
const { $api, $e } = useNuxtApp()
const selectedView = inject(ActiveViewInj)
const { isSqlView } = useSmartsheetStoreOrThrow()
const selectedView = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj)
const isLocked = inject(IsLockedInj, ref(false))
const showWebhookDrawer = ref(false)
@ -47,7 +49,7 @@ const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject()
const Icon = computed(() => {
switch (selectedView?.value.lock_type) {
switch (selectedView.value?.lock_type) {
case LockType.Personal:
return MdiAccountIcon
case LockType.Locked:
@ -58,10 +60,12 @@ const Icon = computed(() => {
}
})
const lockType = $computed(() => (selectedView.value?.lock_type as LockType) || LockType.Collaborative)
async function changeLockType(type: LockType) {
$e('a:grid:lockmenu', { lockType: type })
if (!selectedView?.value) return
if (!selectedView.value) return
if (type === 'personal') {
// Coming soon
@ -79,8 +83,6 @@ async function changeLockType(type: LockType) {
}
}
const { isSqlView } = useSmartsheetStoreOrThrow()
const open = ref(false)
useMenuCloseOnEsc(open)
@ -92,9 +94,9 @@ useMenuCloseOnEsc(open)
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<component
:is="viewIcons[selectedView?.type].icon"
:is="getViewIcon(selectedView?.type)?.icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
:style="{ color: getViewIcon(selectedView?.type)?.color }"
/>
<span class="!text-sm font-weight-normal">
@ -117,7 +119,7 @@ useMenuCloseOnEsc(open)
>
<template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0">
<LazySmartsheetToolbarLockType hide-tick :type="selectedView?.lock_type || LockType.Collaborative" />
<LazySmartsheetToolbarLockType hide-tick :type="lockType" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"

270
packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue

@ -88,152 +88,158 @@ onMounted(() => {
</script>
<template>
<a-modal
v-model:visible="showNewTokenModal"
:class="{ active: showNewTokenModal }"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-generate-token"
>
<div class="relative flex flex-col h-full">
<a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false">
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
<!-- Generate Token -->
<div class="flex flex-row justify-center w-full -mt-1 mb-3">
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title>
</div>
<div>
<a-modal
v-model:visible="showNewTokenModal"
:class="{ active: showNewTokenModal }"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-generate-token"
>
<div class="relative flex flex-col h-full">
<a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false">
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
<!-- Description -->
<a-form
ref="form"
:model="selectedTokenData"
name="basic"
layout="vertical"
class="flex flex-col justify-center space-y-6"
no-style
autocomplete="off"
@finish="generateToken"
>
<a-input v-model:value="selectedTokenData.description" :placeholder="$t('labels.description')" />
<!-- Generate -->
<div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit">
{{ $t('general.generate') }}
</a-button>
<!-- Generate Token -->
<div class="flex flex-row justify-center w-full -mt-1 mb-3">
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title>
</div>
</a-form>
</div>
</a-modal>
<a-modal
v-model:visible="showDeleteTokenModal"
:class="{ active: showDeleteTokenModal }"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-delete-token"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">This action will remove this API Token</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
</a-modal>
<div class="flex flex-col px-10 mt-6">
<div class="flex flex-row justify-end">
<div class="flex flex-row space-x-1">
<a-button size="middle" type="text" @click="loadApiTokens()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiReload class="text-gray-500" />
<div class="text-gray-500">{{ $t('general.reload') }}</div>
</div>
</a-button>
<!-- Add New Token -->
<a-button size="middle" type="primary" ghost @click="openNewTokenModal">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiPlus />
<div>{{ $t('activity.newToken') }}</div>
<!-- Description -->
<a-form
ref="form"
:model="selectedTokenData"
name="basic"
layout="vertical"
class="flex flex-col justify-center space-y-6"
no-style
autocomplete="off"
@finish="generateToken"
>
<a-input v-model:value="selectedTokenData.description" :placeholder="$t('labels.description')" />
<!-- Generate -->
<div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit">
{{ $t('general.generate') }}
</a-button>
</div>
</a-button>
</a-form>
</div>
</div>
<div v-if="tokensInfo" class="w-full flex flex-col mt-2 px-1">
<div class="flex flex-row border-b-1 text-gray-600 text-xs pb-2 pt-2">
<div class="flex w-4/10 pl-2">{{ $t('labels.description') }}</div>
<div class="flex w-4/10 justify-center">{{ $t('labels.token') }}</div>
<div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div>
</a-modal>
<a-modal
v-model:visible="showDeleteTokenModal"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-delete-token"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">This action will remove this API Token</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
</a-modal>
<div class="flex flex-col px-10 mt-6">
<div class="flex flex-row justify-end">
<div class="flex flex-row space-x-1">
<a-button size="middle" type="text" @click="loadApiTokens()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiReload class="text-gray-500" />
<div class="text-gray-500">{{ $t('general.reload') }}</div>
</div>
</a-button>
<div v-for="(item, index) in tokensInfo" :key="index" class="flex flex-col">
<div class="flex flex-row border-b-1 items-center px-2 py-2">
<div class="flex flex-row w-4/10 flex-wrap overflow-ellipsis">
{{ item.description }}
</div>
<!-- Add New Token -->
<a-button size="middle" type="primary" ghost @click="openNewTokenModal">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiPlus />
<div>{{ $t('activity.newToken') }}</div>
</div>
</a-button>
</div>
</div>
<div class="flex w-4/10 justify-center flex-wrap overflow-ellipsis">
<span v-if="item.show">{{ item.token }}</span>
<span v-else>****************************************</span>
</div>
<div v-if="tokensInfo" class="w-full flex flex-col mt-2 px-1">
<div class="flex flex-row border-b-1 text-gray-600 text-xs pb-2 pt-2">
<div class="flex w-4/10 pl-2">{{ $t('labels.description') }}</div>
<div class="flex w-4/10 justify-center">{{ $t('labels.token') }}</div>
<div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div>
</div>
<div class="flex flex-row w-2/10 justify-end">
<a-tooltip placement="bottom">
<template #title>
<span v-if="item.show"> {{ $t('general.hide') }} </span>
<span v-else> {{ $t('general.show') }} </span>
</template>
<a-button type="text" class="!rounded-md" @click="item.show = !item.show">
<template #icon>
<MaterialSymbolsVisibilityOff v-if="item.show" class="flex mx-auto h-[1.1rem]" />
<MaterialSymbolsVisibility v-else class="flex mx-auto h-[1rem]" />
<div v-for="(item, index) in tokensInfo" :key="index" class="flex flex-col">
<div class="flex flex-row border-b-1 items-center px-2 py-2">
<div class="flex flex-row w-4/10 flex-wrap overflow-ellipsis">
{{ item.description }}
</div>
<div class="flex w-4/10 justify-center flex-wrap overflow-ellipsis">
<span v-if="item.show">{{ item.token }}</span>
<span v-else>****************************************</span>
</div>
<div class="flex flex-row w-2/10 justify-end">
<a-tooltip placement="bottom">
<template #title>
<span v-if="item.show"> {{ $t('general.hide') }} </span>
<span v-else> {{ $t('general.show') }} </span>
</template>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> {{ $t('general.copy') }} </template>
<a-button type="text" class="!rounded-md" @click="item.show = !item.show">
<template #icon>
<MaterialSymbolsVisibilityOff v-if="item.show" class="flex mx-auto h-[1.1rem]" />
<MaterialSymbolsVisibility v-else class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</a-tooltip>
<a-button type="text" class="!rounded-md" @click="copyToken(item.token)">
<template #icon>
<MdiContentCopy class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</a-tooltip>
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-api-token-mgmt">
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert />
</div>
<a-tooltip placement="bottom">
<template #title> {{ $t('general.copy') }} </template>
<a-button type="text" class="!rounded-md" @click="copyToken(item.token)">
<template #icon>
<MdiContentCopy class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</div>
<template #overlay>
<a-menu>
<a-menu-item>
<div class="flex flex-row items-center py-3 h-[1rem]" @click="openDeleteModal(item)">
<MdiDeleteOutline class="flex" />
<div class="text-xs pl-2">{{ $t('general.remove') }}</div>
</a-tooltip>
<a-dropdown
:trigger="['click']"
class="flex"
placement="bottomRight"
overlay-class-name="nc-dropdown-api-token-mgmt"
>
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert />
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-button>
</div>
<template #overlay>
<a-menu>
<a-menu-item>
<div class="flex flex-row items-center py-3 h-[1rem]" @click="openDeleteModal(item)">
<MdiDeleteOutline class="flex" />
<div class="text-xs pl-2">{{ $t('general.remove') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</div>
</div>

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

@ -174,7 +174,7 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
<div v-else class="flex flex-col w-full px-6">
<LazyTabsAuthUserManagementUsersModal
:key="showUserModal"
:key="`${showUserModal}`"
:show="showUserModal"
:selected-user="selectedUser"
@closed="showUserModal = false"

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

@ -21,7 +21,7 @@ import { ProjectRole } from '~/lib'
interface Props {
show: boolean
selectedUser?: User
selectedUser?: User | null
}
interface Users {

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

@ -30,7 +30,8 @@ import {
} from '#imports'
import { TabType } from '~/lib'
const { quickImportType, projectTemplate, importData, importColumns, importDataOnly, maxRowsToParse } = defineProps<Props>()
const { quickImportType, projectTemplate, importData, importColumns, importDataOnly, maxRowsToParse, baseId } =
defineProps<Props>()
const emit = defineEmits(['import'])
@ -45,6 +46,7 @@ interface Props {
importColumns: any[]
importDataOnly: boolean
maxRowsToParse: number
baseId: string
}
interface Option {
@ -64,7 +66,9 @@ const { $api } = useNuxtApp()
const { addTab } = useTabs()
const { sqlUi, project, loadTables } = useProject()
const { sqlUis, project, loadTables } = useProject()
const sqlUi = ref(sqlUis.value[baseId] || Object.values(sqlUis.value)[0])
const hasSelectColumn = ref<boolean[]>([])
@ -395,7 +399,7 @@ async function importTemplate() {
try {
isImporting.value = true
const tableName = meta.value?.title
const tableId = meta.value?.id
const projectName = project.value.title!
await Promise.all(
@ -427,15 +431,17 @@ async function importTemplate() {
input = null
}
} else if (v.uidt === UITypes.Date) {
input = parseStringDate(input, v.meta.date_format)
if (input) {
input = parseStringDate(input, v.meta.date_format)
}
}
res[col.destCn] = input
}
return res
}, {}),
)
await $api.dbTableRow.bulkCreate('noco', projectName, tableName!, batchData)
updateImportTips(projectName, tableName!, progress, total)
await $api.dbTableRow.bulkCreate('noco', projectName, tableId!, batchData)
updateImportTips(projectName, tableId!, progress, total)
progress += batchData.length
}
})(key),
@ -495,24 +501,24 @@ async function importTemplate() {
}
}
}
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
const createdTable = await $api.base.tableCreate(project?.value?.id as string, baseId as string, {
table_name: table.table_name,
// leave title empty to get a generated one based on table_name
title: '',
columns: table.columns || [],
})
table.title = tableMeta.title
table.id = createdTable.id
table.title = createdTable.title
// open the first table after import
if (tab.id === '' && tab.title === '') {
tab.id = tableMeta.id as string
tab.title = tableMeta.title as string
tab.id = createdTable.id as string
tab.title = createdTable.title as string
}
// set primary value
if (tableMeta?.columns?.[0]?.id) {
await $api.dbTableColumn.primaryColumnSet(tableMeta.columns[0].id as string)
if (createdTable?.columns?.[0]?.id) {
await $api.dbTableColumn.primaryColumnSet(createdTable.columns[0].id as string)
}
}
// bulk insert data
@ -532,7 +538,7 @@ async function importTemplate() {
for (let i = 0; i < data.length; i += offset) {
updateImportTips(projectName, tableMeta.title, progress, total)
const batchData = remapColNames(data.slice(i, i + offset), tableMeta.columns)
await $api.dbTableRow.bulkCreate('noco', projectName, tableMeta.title, batchData)
await $api.dbTableRow.bulkCreate('noco', projectName, tableMeta.id, batchData)
progress += batchData.length
}
updateImportTips(projectName, tableMeta.title, total, total)
@ -693,7 +699,7 @@ function isSelectDisabled(uidt: string, disableSelect = false) {
size="large"
hide-details
:bordered="false"
@click="$event.stopPropagation()"
@click.stop
@blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)"
/>
@ -749,14 +755,7 @@ function isSelectDisabled(uidt: string, disableSelect = false) {
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'column_name'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-input
:ref="
(el) => {
inputRefs[record.key] = el
}
"
v-model:value="record.column_name"
/>
<a-input :ref="(el: HTMLInputElement) => (inputRefs[record.key] = el)" v-model:value="record.column_name" />
</a-form-item>
</template>

2
packages/nc-gui/components/virtual-cell/Formula.vue

@ -10,7 +10,7 @@ const cellValue = inject(CellValueInj)
const { isPg } = useProject()
const result = computed(() => (isPg.value ? handleTZ(cellValue?.value) : cellValue?.value))
const result = computed(() => (isPg(column.value.base_id) ? handleTZ(cellValue?.value) : cellValue?.value))
const urls = computed(() => replaceUrlsWithLink(result.value))

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

@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import {
ColumnInj,
Empty,
@ -27,7 +28,7 @@ const isForm = inject(IsFormInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const column = inject(ColumnInj)
const column = inject(ColumnInj, ref())
const readonly = inject(ReadonlyInj, ref(false))
@ -81,6 +82,8 @@ const expandedFormDlg = ref(false)
const expandedFormRow = ref()
const colTitle = $computed(() => column.value?.title || '')
/** reload children list whenever cell value changes and list is visible */
watch(
() => props.cellValue,
@ -88,6 +91,12 @@ watch(
if (!isNew.value && vModel.value) loadChildrenList()
},
)
const onClick = (row: Row) => {
if (readonly.value) return
expandedFormRow.value = row
expandedFormDlg.value = true
}
</script>
<template>
@ -119,25 +128,19 @@ watch(
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1">
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
<MdiLinkVariant class="text-xs" type="primary" />
Link to '{{ relatedTableMeta.title }}'
</div>
</a-button>
</div>
<template v-if="(isNew && state?.[column?.title]?.length) || childrenList?.pageInfo?.totalRows">
<template v-if="(isNew && state?.[colTitle]?.length) || childrenList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12 cursor-pointer">
<a-card
v-for="(row, i) of childrenList?.list ?? state?.[column?.title] ?? []"
v-for="(row, i) of childrenList?.list ?? state?.[colTitle] ?? []"
:key="i"
class="!my-4 hover:(!bg-gray-200/50 shadow-md)"
@click="
() => {
if (readonly) return
expandedFormRow = row
expandedFormDlg = true
}
"
@click="onClick(row)"
>
<div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0">
@ -169,7 +172,7 @@ watch(
v-model:page-size="childrenListPagination.size"
class="mt-2 mx-auto"
size="small"
:total="childrenList.pageInfo.totalRows"
:total="childrenList?.pageInfo.totalRows"
show-less-items
/>
</div>

2
packages/nc-gui/components/webhook/List.vue

@ -52,7 +52,7 @@ onMounted(() => {
<template>
<div class="">
<div class="mb-2">
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div>
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta?.title }} : Webhooks</div>
<a-button
v-e="['c:webhook:add']"

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

@ -2,6 +2,7 @@ import clone from 'just-clone'
import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form'
import {
Form,
computed,
@ -20,10 +21,13 @@ const useForm = Form.useForm
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
interface ValidationsObj {
[key: string]: RuleObject[]
}
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUi } = useProject()
const { sqlUis, isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = useProject()
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
@ -32,14 +36,22 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { $e } = useNuxtApp()
const isEdit = computed(() => !!column.value?.id)
const sqlUi = ref(meta.value?.base_id ? sqlUis.value[meta.value?.base_id] : Object.values(sqlUis.value)[0])
const isEdit = computed(() => !!column?.value?.id)
const isMysql = computed(() => isMysqlFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const isPg = computed(() => isPgFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const isMssql = computed(() => isMssqlFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const idType = null
const additionalValidations = ref<Record<string, any>>({})
const additionalValidations = ref<ValidationsObj>({})
const setAdditionalValidations = (validations: Record<string, any>) => {
additionalValidations.value = validations
const setAdditionalValidations = (validations: ValidationsObj) => {
additionalValidations.value = { ...additionalValidations.value, ...validations }
}
const formState = ref<Record<string, any>>({
@ -267,6 +279,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isEdit,
column,
sqlUi,
isMssql,
isPg,
isMysql,
}
},
)

2
packages/nc-gui/composables/useExpandedFormDetached/index.ts

@ -4,7 +4,7 @@ import type { Row } from '~/lib'
interface UseExpandedFormDetachedProps {
'isOpen'?: boolean
'row': Row | null
'row': Row
'state'?: Record<string, any> | null
'meta': TableType
'loadRow'?: boolean

4
packages/nc-gui/composables/useGlobal/state.ts

@ -84,8 +84,10 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
set: (val) => (storage.value.token = val),
})
const config = useRuntimeConfig()
const appInfo = ref<AppInfo>({
ncSiteUrl: BASE_FALLBACK_URL,
ncSiteUrl: config.public.ncBackendUrl || BASE_FALLBACK_URL,
authType: 'jwt',
connectToExternalDB: false,
defaultLimit: 0,

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

@ -94,7 +94,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}
const relatedTablePrimaryValueProp = computed(() => {
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || ''
})
const relatedTablePrimaryKeyProps = computed(() => {

52
packages/nc-gui/composables/useProject.ts

@ -1,4 +1,4 @@
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { BaseType, OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import { SqlUiFactory } from 'nocodb-sdk'
import { isString } from '@vueuse/core'
import {
@ -35,14 +35,16 @@ const [setup, use] = useInjectionState(() => {
const projectLoadedHook = createEventHook<ProjectType>()
const project = ref<ProjectType>({})
const bases = computed<BaseType[]>(() => project.value?.bases || [])
const tables = ref<TableType[]>([])
const projectMetaInfo = ref<ProjectMetaInfo | undefined>()
const lastOpenedViewMap = ref<Record<string, string>>({})
const projectId = computed(() => route.params.projectId as string)
let forcedProjectId: string | undefined
const projectId = computed(() => forcedProjectId || (route.params.projectId as string))
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
@ -55,15 +57,35 @@ const [setup, use] = useInjectionState(() => {
}
})
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || ClientType.MYSQL)
const sqlUis = computed(() => {
const temp: Record<string, any> = {}
for (const base of bases.value) {
if (base.id) {
temp[base.id] = SqlUiFactory.create({ client: base.type }) as Exclude<
ReturnType<typeof SqlUiFactory['create']>,
typeof OracleUi
>
}
}
return temp
})
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
function getBaseType(baseId?: string) {
return bases.value.find((base) => base.id === baseId)?.type || ClientType.MYSQL
}
function isMysql(baseId?: string) {
return ['mysql', ClientType.MYSQL].includes(getBaseType(baseId))
}
function isMssql(baseId?: string) {
return getBaseType(baseId) === 'mssql'
}
function isPg(baseId?: string) {
return getBaseType(baseId) === 'pg'
}
const isMysql = computed(() => ['mysql', ClientType.MYSQL].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg')
const isSharedBase = computed(() => projectType === 'base')
async function loadProjectMetaInfo(force?: boolean) {
@ -78,11 +100,14 @@ const [setup, use] = useInjectionState(() => {
includeM2M: includeM2M.value,
})
if (tablesResponse.list) tables.value = tablesResponse.list
if (tablesResponse.list) {
tables.value = tablesResponse.list.filter((table) => bases.value.find((base) => base.id === table.base_id)?.enabled)
}
}
}
async function loadProject(withTheme = true) {
async function loadProject(withTheme = true, forcedId?: string) {
if (forcedId) forcedProjectId = forcedId
if (projectType === 'base') {
try {
const baseData = await api.public.sharedBaseGet(route.params.projectId as string)
@ -151,6 +176,7 @@ const [setup, use] = useInjectionState(() => {
return {
project,
bases,
tables,
loadProjectRoles,
loadProject,
@ -159,7 +185,7 @@ const [setup, use] = useInjectionState(() => {
isMysql,
isMssql,
isPg,
sqlUi,
sqlUis,
isSharedBase,
loadProjectMetaInfo,
projectMetaInfo,

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

@ -43,6 +43,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const { metas, setMeta } = useMetas()
const { loadProject } = useProject()
const { t } = useI18n()
const formState = ref<Record<string, any>>({})
@ -84,6 +86,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
await setMeta(viewMeta.model)
await loadProject(true, viewMeta.project_id)
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))

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

@ -17,6 +17,8 @@ export function useSharedView() {
const { appInfo } = $(useGlobal())
const { loadProject } = useProject()
const appInfoDefaultLimit = appInfo.defaultLimit || 25
const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: appInfoDefaultLimit }))
@ -71,6 +73,8 @@ export function useSharedView() {
await setMeta(viewMeta.model)
await loadProject(true, viewMeta.project_id)
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
}
@ -103,7 +107,7 @@ export function useSharedView() {
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit
const data = await $api.public.groupedDataList(
return await $api.public.groupedDataList(
sharedView.value.uuid!,
columnId,
{
@ -118,7 +122,6 @@ export function useSharedView() {
},
},
)
return data
}
const exportFile = async (
@ -126,7 +129,7 @@ export function useSharedView() {
offset: number,
type: ExportTypes.EXCEL | ExportTypes.CSV,
responseType: 'base64' | 'blob',
{ sortsArr, filtersArr }: { sortsArr: SortType[]; filtersArr: FilterType[] },
{ sortsArr, filtersArr }: { sortsArr: SortType[]; filtersArr: FilterType[] } = { sortsArr: [], filtersArr: [] },
) => {
return await $api.public.csvExport(sharedView.value!.uuid!, type, {
format: responseType,

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

@ -13,8 +13,9 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
initialFilters?: Ref<FilterType[]>,
) => {
const { $api } = useNuxtApp()
const { sqlUis } = useProject()
const { sqlUi } = useProject()
const sqlUi = ref(meta.value?.base_id ? sqlUis.value[meta.value?.base_id] : Object.values(sqlUis.value)[0])
const cellRefs = ref<HTMLTableDataCellElement[]>([])
@ -47,7 +48,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
})
const isSqlView = computed(() => (meta.value as TableType)?.type === 'view')
const sorts = ref<SortType[]>(unref(initialSorts) ?? [])
const sorts = ref<Required<SortType>[]>((unref(initialSorts) as Required<SortType>[]) ?? [])
const nestedFilters = ref<FilterType[]>(unref(initialFilters) ?? [])
return {

9
packages/nc-gui/composables/useTable.ts

@ -16,7 +16,7 @@ import {
} from '#imports'
import { TabType } from '~/lib'
export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?: string) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({
title: '',
table_name: '',
@ -32,8 +32,9 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
const { loadTables } = useProject()
const { closeTab } = useTabs()
const { sqlUis, project, tables } = useProject()
const { sqlUi, project, tables } = useProject()
const sqlUi = computed(() => (baseId && sqlUis.value[baseId] ? sqlUis.value[baseId] : Object.values(sqlUis.value)[0]))
const createTable = async () => {
if (!sqlUi?.value) return
@ -48,7 +49,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
})
try {
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
const tableMeta = await $api.base.tableCreate(project?.value?.id as string, baseId as string, {
...table,
columns,
})
@ -62,7 +63,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
watch(
() => table.title,
(title) => {
table.table_name = `${project?.value?.prefix || ''}${title}`
table.table_name = `${title}`
},
)

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

@ -17,7 +17,7 @@ const [setup, use] = useInjectionState(() => {
const router = useRouter()
const { tables } = useProject()
const { bases, tables } = useProject()
const projectType = $computed(() => route.params.projectType as string)
@ -29,14 +29,22 @@ const [setup, use] = useInjectionState(() => {
if (routeName.startsWith('projectType-projectId-index-index-type-title-viewTitle') && tables.value.length) {
const tab: TabItem = { type: route.params.type as TabType, title: route.params.title as string }
const currentTable = tables.value.find((t) => t.title === tab.title)
const currentTable = tables.value.find((t) => t.id === tab.title || t.title === tab.title)
if (!currentTable) return -1
const currentBase = bases.value.find((b) => b.id === currentTable.base_id)
tab.id = currentTable.id
let index = tabs.value.findIndex((t) => t.id === tab.id)
tab.title = currentTable.title
// 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})` : ``}`
if (index === -1) {
tab.sortsState = tab.sortsState || new Map()
tab.filterState = tab.filterState || new Map()
@ -81,6 +89,13 @@ const [setup, use] = useInjectionState(() => {
}
// if tab not found add it
else {
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)
// 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})` : ``}`
tabs.value = [...(tabs.value || []), tabMeta]
activeTabIndex.value = tabs.value.length - 1
}
@ -111,13 +126,9 @@ const [setup, use] = useInjectionState(() => {
function navigateToTab(tab: TabItem) {
switch (tab.type) {
case TabType.TABLE:
return navigateTo(
`/${projectType}/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
return navigateTo(`/${projectType}/${route.params.projectId}/table/${tab?.id}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
case TabType.VIEW:
return navigateTo(
`/${projectType}/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
return navigateTo(`/${projectType}/${route.params.projectId}/view/${tab?.id}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
case TabType.AUTH:
return navigateTo(`/${projectType}/${route.params.projectId}/auth`)
}
@ -131,10 +142,10 @@ const [setup, use] = useInjectionState(() => {
Object.assign(tab, newTabItemProps)
if (isActive && tab.title)
if (isActive && tab.id)
router.replace({
params: {
title: tab.title,
title: tab.id,
},
})
}

1
packages/nc-gui/context/index.ts

@ -31,3 +31,4 @@ export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injecti
export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection')
export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url')
export const DropZoneRef: InjectionKey<Ref<Element | undefined>> = Symbol('drop-zone-ref')
export const ToggleDialogInj: InjectionKey<Function> = Symbol('toggle-dialog-injection')

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": {
@ -668,13 +668,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",

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

@ -248,7 +248,7 @@
"sqlOutput": "Sortie SQL",
"addOption": "Ajouter une option",
"qrCodeValueColumn": "Column with QR code value",
"qrCodeValueTooLong": "Too many characters for a QR code",
"qrCodeValueTooLong": "Trop de caractères pour un code QR",
"aggregateFunction": "Fonction agrégée",
"dbCreateIfNotExists": "Base de données : la créer si elle n'existe pas",
"clientKey": "Clé client",
@ -276,11 +276,11 @@
"childColumn": "Colonne enfant",
"onUpdate": "Mise à jour en cours",
"onDelete": "Suppression en cours",
"account": "Account",
"account": "Compte",
"language": "Language",
"primaryColor": "Primary Color",
"accentColor": "Accent Color",
"customTheme": "Custom Theme",
"customTheme": "Thème personnalisé",
"requestDataSource": "Request a data source you need?",
"apiKey": "Clé d'API",
"sharedBase": "Shared Base",
@ -304,8 +304,8 @@
"commentsOnly": "Comments only",
"documentation": "Documentation",
"subscribeNewsletter": "Subscribe to our weekly newsletter",
"signUpWithGoogle": "Sign up with Google",
"signInWithGoogle": "Sign in with Google",
"signUpWithGoogle": "S’enregistrer avec Google",
"signInWithGoogle": "Se connecter avec Google",
"agreeToTos": "By signing up, you agree to the Terms of Service",
"welcomeToNc": "Welcome to NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url"

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

@ -16,7 +16,7 @@
"cancel": "キャンセル",
"submit": "送信",
"create": "作成",
"duplicate": "Duplicate",
"duplicate": "複製",
"insert": "挿入",
"delete": "削除",
"update": "更新",
@ -56,25 +56,25 @@
"notification": "通知",
"reference": "リファレンス",
"function": "関数",
"confirm": "Confirm",
"generate": "Generate",
"copy": "Copy",
"confirm": "確認",
"generate": "生成",
"copy": "コピー",
"misc": "Miscellaneous",
"lock": "Lock",
"unlock": "Unlock",
"credentials": "Credentials",
"help": "Help",
"credentials": "認証情報",
"help": "ヘルプ",
"questions": "Questions",
"reachOut": "Reach out here",
"betaNote": "This feature is currently in beta.",
"betaNote": "この機能はまだベータ版です",
"moreInfo": "More information can be found here",
"logs": "Logs",
"groupingField": "Grouping Field",
"insertAfter": "Insert After",
"insertBefore": "Insert Before",
"logs": "ログ",
"groupingField": "グループ フィールド",
"insertAfter": "後に挿入",
"insertBefore": "前に挿入",
"hideField": "Hide Field",
"sortAsc": "Sort Ascending",
"sortDesc": "Sort Descending"
"sortAsc": "昇順",
"sortDesc": "降順"
},
"objects": {
"project": "プロジェクト",
@ -113,7 +113,7 @@
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
"sqlVIew": "SQL View"
"sqlVIew": "SQL ビュー"
},
"datatype": {
"ID": "ID",
@ -195,17 +195,17 @@
"headLogin": "ログイン | NocoDB",
"resetPassword": "パスワードをリセットする",
"teamAndSettings": "チームと設定",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable",
"generateToken": "Generate Token",
"apiDocs": "API ドキュメント",
"importFromAirtable": "Airtable からインポート",
"generateToken": "トークンを生成",
"APIsAndSupport": "APIとサポート",
"helpCenter": "Help center",
"swaggerDocumentation": "Swagger Documentation",
"helpCenter": "ヘルプセンター",
"swaggerDocumentation": "Swagger ドキュメント",
"quickImportFrom": "Quick Import From",
"quickImport": "Quick Import",
"advancedSettings": "Advanced Settings",
"codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts"
"advancedSettings": "詳細設定",
"codeSnippet": "コードスニペット",
"keyboardShortcut": "キーボードショートカット"
},
"labels": {
"createdBy": "Created By",
@ -226,7 +226,7 @@
"port": "ポート番号",
"username": "ユーザー名",
"password": "パスワード",
"schemaName": "Schema name",
"schemaName": "スキーマ名",
"database": "データベース",
"action": "アクション",
"actions": "アクション",
@ -266,7 +266,7 @@
"bookDemo": "デモを予約",
"getAnswered": "あなたの質問に回答しましょう",
"joinDiscord": "Discord に参加",
"joinCommunity": "Join NocoDB Community",
"joinCommunity": "NocoDB のコミュニティに参加",
"joinReddit": "/r/NocoDB に参加",
"followNocodb": "NocoDB をフォロー"
},
@ -278,11 +278,11 @@
"onDelete": "削除中",
"account": "アカウント",
"language": "言語",
"primaryColor": "Primary Color",
"accentColor": "Accent Color",
"customTheme": "Custom Theme",
"primaryColor": "プライマリカラー",
"accentColor": "アクセントカラー",
"customTheme": "カスタムテーマ",
"requestDataSource": "Request a data source you need?",
"apiKey": "API Key",
"apiKey": "API キー",
"sharedBase": "Shared Base",
"importData": "Import Data",
"importSecondaryViews": "Import Secondary Views",
@ -290,25 +290,25 @@
"importLookupColumns": "Import Lookup Columns",
"importAttachmentColumns": "Import Attachment Columns",
"importFormulaColumns": "Import Formula Columns",
"noData": "No Data",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",
"flattenNested": "Flatten Nested",
"noData": "データがありません",
"goToDashboard": "ダッシュボードに移動",
"importing": "インポート中",
"flattenNested": "入れ子を平坦化",
"downloadAllowed": "ダウンロードを許可",
"weAreHiring": "We are Hiring!",
"primaryKey": "Primary key",
"primaryKey": "プライマリーキー",
"hasMany": "has many",
"belongsTo": "belongs to",
"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",
"agreeToTos": "By signing up, you agree to the Terms of Service",
"welcomeToNc": "Welcome to NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url"
"commentsOnly": "コメントのみ",
"documentation": "ドキュメント",
"subscribeNewsletter": "週刊ニュースレターを購読する",
"signUpWithGoogle": "Googleでログイン",
"signInWithGoogle": "Googleでログイン",
"agreeToTos": "サインアップすることで、利用規約に同意するものとみなされます。",
"welcomeToNc": "NocoDB へようこそ!",
"inviteOnlySignup": "招待URL からのサインアップのみ許可"
},
"activity": {
"createProject": "プロジェクトを作成",
@ -336,7 +336,7 @@
"translate": "翻訳に協力する",
"account": {
"authToken": "Auth Tokenをコピー",
"swagger": "Swagger: REST APIs",
"swagger": "Swagger: REST API",
"projInfo": "プロジェクト情報をコピー",
"themes": "テーマ"
},
@ -353,14 +353,14 @@
"invite": "招待",
"inviteMore": "さらに招待",
"inviteTeam": "チームへ招待",
"inviteUser": "Invite User",
"inviteUser": "ユーザーを招待",
"inviteToken": "招待用トークン",
"newUser": "ユーザーを作成",
"editUser": "ユーザーを編集",
"deleteUser": "プロジェクトからユーザーを削除",
"resendInvite": "招待用メールを再送信",
"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": "エクセルファイルをインポート",
"importCSV": "Import CSV",
"importCSV": "CSV のインポート",
"downloadCSV": "CSVをダウンロード",
"downloadExcel": "XLSXをダウンロード",
"uploadCSV": "CSVをアップロード",
@ -421,21 +421,21 @@
"editConnJson": "コネクション JSON を編集",
"sponsorUs": "スポンサーになる",
"sendEmail": "メールを送信",
"addUserToProject": "Add user to project",
"addUserToProject": "ユーザーをプロジェクトに追加",
"getApiSnippet": "APIスニペットを取得",
"clearCell": "Clear cell",
"clearCell": "セルをクリア",
"addFilterGroup": "フィルターグループを追加",
"linkRecord": "Link record",
"addNewRecord": "Add new record",
"useConnectionUrl": "Use Connection URL",
"linkRecord": "レコードをリンク",
"addNewRecord": "レコードを追加",
"useConnectionUrl": "接続 URL を使用",
"toggleCommentsDraw": "Toggle comments draw",
"expandRecord": "Expand Record",
"deleteRecord": "Delete Record",
"expandRecord": "レコードを展開",
"deleteRecord": "レコードを削除",
"erd": {
"showColumns": "Show Columns",
"showPkAndFk": "Show Primary and Foreign Keys",
"showSqlViews": "Show SQL Views",
"showMMTables": "Show Many to Many tables",
"showColumns": "列を表示",
"showPkAndFk": "プライマリキーと外部キーを表示",
"showSqlViews": "SQL ビューを表示",
"showMMTables": "多対多のテーブルを表示",
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
@ -492,20 +492,20 @@
"defaultValue": "デフォルト値",
"filterByEmail": "メールアドレスでフィルタ",
"filterQuery": "クエリを入力",
"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."
"orgCreator": "「作成者」は新しいプロジェクトを作成でき、かつ招待されたプロジェクトにもアクセスできます。",
"orgViewer": "ビューアーは新規プロジェクトを作成することはできませんが、招待されたプロジェクトにアクセスできます。"
},
"footerInfo": "1ページあたりの行数",
"upload": "アップロードするファイルを選択してください",
@ -600,29 +600,29 @@
"credentials": "Where to find this?"
},
"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",
"autoIncFieldNotEditable": "Auto increment field is not editable",
"editingPKnotSupported": "Editing primary key not supported",
"deletedCache": "Deleted cache successfully",
"cacheEmpty": "Cache is empty",
"exportedCache": "Exported Cache Successfully",
"valueAlreadyInList": "This value is already in the list",
"noColumnsToUpdate": "No columns to update",
"tableDeleted": "Deleted table successfully",
"copiedToClipboard": "クリップボードにコピーしました",
"requriedFieldsCantBeMoved": "必須フィールドは移動できません",
"updateNotAllowedWithoutPK": "プライマリキーを持たないテーブルは更新できません",
"autoIncFieldNotEditable": "オートインクリメントフィールドは編集できません",
"editingPKnotSupported": "プライマリキーは編集できません",
"deletedCache": "キャッシュをクリアしました",
"cacheEmpty": "キャッシュは空です",
"exportedCache": "キャッシュをエクスポートしました",
"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": "スタックを削除すると、`{stackToBeDeleted}`から選択肢`{groupingField}`も削除されます。レコードは未分類スタックに移動します。",
"computedFieldEditWarning": "Computed field: contents are read-only. Use column edit menu to reconfigure",
"computedFieldDeleteWarning": "Computed field: contents are read-only. Unable to clear content."
"computedFieldEditWarning": "計算フィールド: コンテンツは読み取り専用です。列編集メニューを使用して再設定してください。",
"computedFieldDeleteWarning": "計算フィールド: コンテンツは読み取り専用です。コンテンツを消去できません。"
},
"error": {
"searchProject": "{search} の検索結果が見つかりませんでした",
@ -638,53 +638,53 @@
"passwdRequired": "パスワードが必要です",
"passwdLength": "パスワードは8文字以上にしてください",
"passwdMismatch": "パスワードが一致していません",
"completeRuleSet": "At least 8 characters with one Uppercase, one number and one special character",
"atLeast8Char": "At least 8 characters",
"atLeastOneUppercase": "One Uppercase letter",
"atLeastOneNumber": "One Number",
"atLeastOneSpecialChar": "One special character",
"allowedSpecialCharList": "Allowed special character list"
"completeRuleSet": "英大文字、数字、記号をそれぞれ1文字以上含みかつ 8 文字以上",
"atLeast8Char": "8 文字以上",
"atLeastOneUppercase": "英大文字 1 文字",
"atLeastOneNumber": "数字 1 文字",
"atLeastOneSpecialChar": "記号 1 文字",
"allowedSpecialCharList": "利用できる記号の一覧"
},
"invalidURL": "Invalid URL",
"internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file",
"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",
"columnDescriptionNotFound": "Cannot find the destination column for",
"invalidURL": "無効なURL",
"internalError": "内部エラーが発生しました",
"templateGeneratorNotFound": "テンプレートジェネレーターが見つかりません!",
"fileUploadFailed": "ファイルのアップロードに失敗しました",
"primaryColumnUpdateFailed": "プライマリカラムの更新に失敗しました",
"formDescriptionTooLong": "フォームの説明が長すぎます",
"columnsRequired": "以下のカラムが必要です",
"selectAtleastOneColumn": "少なくとも1つのカラムを選択してください",
"columnDescriptionNotFound": "次の宛先カラムが見つかりません:",
"duplicateMappingFound": "Duplicate mapping found, please remove one of the mapping",
"nullValueViolatesNotNull": "Null value violates not-null constraint",
"sourceHasInvalidNumbers": "Source data contains some invalid numbers",
"sourceHasInvalidBoolean": "Source data contains some invalid boolean values",
"sourceHasInvalidNumbers": "ソースデータに無効な数値が含まれています",
"sourceHasInvalidBoolean": "ソースデータに無効なブール値が含まれています",
"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": "行の更新に失敗",
"deleteRowFailed": "行の削除に失敗",
"setFormDataFailed": "Failed to set form data",
"formViewUpdateFailed": "Failed to update form view",
"tableNameRequired": "Table name is required",
"nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _",
"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",
"ipNotAllowed": "IP not allowed",
"tableNameRequired": "テーブル名が必要です",
"nameShouldStartWithAnAlphabetOr_": "名前はアルファベットまたは_で始まる必要があります",
"followingCharactersAreNotAllowed": "以下の文字種は使用できません",
"columnNameRequired": "列名が必要です",
"projectNameExceeds50Characters": "プロジェクト名が50文字を超えています",
"projectNameCannotStartWithSpace": "プロジェクト名の先頭にはスペースは利用できません",
"requiredField": "必須フィールド",
"ipNotAllowed": "IPアドレスが許可されていません",
"targetFileIsNotAnAcceptedFileType": "Target file is not an accepted file type",
"theAcceptedFileTypeIsCsv": "The accepted file type is .csv",
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key 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"
"parameterKeyCannotBeEmpty": "パラメータキーは空にできません",
"duplicateParameterKeysAreNotAllowed": "パラメータキーの重複は許可されていません",
"fieldRequired": "{value} を空にすることはできません。",
"projectNotAccessible": "このプロジェクトにはアクセスできません",
"copyToClipboardError": "クリップボードへのコピーに失敗しました"
},
"toast": {
"exportMetadata": "プロジェクトメタデータは正常にエクスポートされました",
@ -704,39 +704,39 @@
"futureRelease": "近日公開!"
},
"success": {
"columnDuplicated": "Column duplicated successfully",
"updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully",
"pluginTested": "Successfully tested plugin settings",
"tableRenamed": "Table renamed successfully",
"viewDeleted": "View deleted successfully",
"columnDuplicated": "カラムを複製しました",
"updatedUIACL": "テーブルの UI ACL を更新しました",
"pluginUninstalled": "プラグインをアンインストールしました",
"pluginSettingsSaved": "プラグインの設定を保存しました",
"pluginTested": "プラグイン設定のテストに成功しました",
"tableRenamed": "テーブル名を変更しました",
"viewDeleted": "ビューを削除しました",
"primaryColumnUpdated": "Successfully updated as primary column",
"tableDataExported": "Successfully exported all table data",
"updated": "Successfully updated",
"sharedViewDeleted": "Deleted shared view successfully",
"userDeleted": "User deleted successfully",
"viewRenamed": "View renamed successfully",
"tokenGenerated": "Token generated successfully",
"tokenDeleted": "Token deleted successfully",
"userAddedToProject": "Successfully added user to project",
"userAdded": "Successfully added user",
"userDeletedFromProject": "Successfully deleted user from project",
"inviteEmailSent": "Invite Email sent successfully",
"inviteURLCopied": "Invite URL copied to clipboard",
"passwordResetURLCopied": "Password reset URL copied to clipboard",
"tableDataExported": "テーブルの全データをエクスポートしました",
"updated": "正常に更新されました",
"sharedViewDeleted": "共有ビューを削除しました",
"userDeleted": "ユーザを削除しました",
"viewRenamed": "ビュー名を変更しました",
"tokenGenerated": "トークンを生成しました",
"tokenDeleted": "トークンを削除しました",
"userAddedToProject": "プロジェクトにユーザーを追加しました",
"userAdded": "ユーザーを追加しました",
"userDeletedFromProject": "プロジェクトからユーザーを削除しました",
"inviteEmailSent": "招待メールを送信しました",
"inviteURLCopied": "招待 URL をクリップボードにコピーしました",
"passwordResetURLCopied": "パスワードリセット URL をクリップボードにコピーしました",
"shareableURLCopied": "Copied shareable base URL to clipboard!",
"embeddableHTMLCodeCopied": "Copied embeddable HTML code!",
"userDetailsUpdated": "Successfully updated the user details",
"tableDataImported": "Successfully imported table data",
"webhookUpdated": "Webhook details updated successfully",
"embeddableHTMLCodeCopied": "埋め込み用 HTML をコピーしました",
"userDetailsUpdated": "ユーザーの詳細を更新しました",
"tableDataImported": "テーブルへデータをインポートしました",
"webhookUpdated": "Webhook の詳細を更新しました",
"webhookDeleted": "Hook deleted successfully",
"webhookTested": "Webhook tested successfully",
"webhookTested": "Webhook のテストに成功しました",
"columnUpdated": "列が更新されました",
"columnCreated": "Column created",
"passwordChanged": "Password changed successfully. Please login again.",
"settingsSaved": "Settings saved successfully",
"roleUpdated": "Role updated successfully"
"columnCreated": "列を作成しました",
"passwordChanged": "パスワードが変更されました。もう一度ログインしてください。",
"settingsSaved": "設定を保存しました",
"roleUpdated": "ロールを更新しました"
}
}
}

126
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,11 +276,11 @@
"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",
@ -429,12 +429,12 @@
"addNewRecord": "Add new record",
"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,25 +597,25 @@
"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",

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

@ -85,3 +85,12 @@ export enum SmartsheetStoreEvents {
FIELD_RELOAD = 'field-reload',
FIELD_ADD = 'field-add',
}
export enum DataSourcesSubTab {
New = 'New',
Metadata = 'Metadata',
ERD = 'ERD',
UIAcl = 'UI ACL',
Misc = 'Misc',
Edit = 'Edit',
}

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

@ -129,6 +129,8 @@ export default defineNuxtConfig({
'ph',
'ri',
'system-uicons',
'vscode-icons',
'simple-icons',
],
}),
],

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

@ -9,6 +9,7 @@
"license": "AGPL-3.0-or-later",
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@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",
@ -56,7 +57,9 @@
"@iconify-json/mi": "^1.1.2",
"@iconify-json/ph": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@iconify-json/simple-icons": "^1.1.29",
"@iconify-json/system-uicons": "^1.1.4",
"@iconify-json/vscode-icons": "^1.1.14",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
@ -1174,6 +1177,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/simple-icons": {
"version": "1.1.29",
"resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.1.29.tgz",
"integrity": "sha512-uP4fKlNoh9IuVTf1e1bl0TWZs+IDE19zCpKLNy+bKmJL5F9zOPwiIMqSsg4KRPXSdoZ2x7lTtt9BrO/O/3Phyg==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/system-uicons": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@iconify-json/system-uicons/-/system-uicons-1.1.4.tgz",
@ -1183,6 +1195,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/vscode-icons": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.1.14.tgz",
"integrity": "sha512-we1er6h6WNDuV/dNSey/f4RDea5DILfDPCM7QN32EkjINr3ggAh7JhfTuQPP9A5lUQkjq5Xu2BpLyiz+E0Yd8w==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
@ -3021,6 +3042,11 @@
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"dev": true
},
"node_modules/@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ=="
},
"node_modules/@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
@ -18408,6 +18434,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/simple-icons": {
"version": "1.1.29",
"resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.1.29.tgz",
"integrity": "sha512-uP4fKlNoh9IuVTf1e1bl0TWZs+IDE19zCpKLNy+bKmJL5F9zOPwiIMqSsg4KRPXSdoZ2x7lTtt9BrO/O/3Phyg==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/system-uicons": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@iconify-json/system-uicons/-/system-uicons-1.1.4.tgz",
@ -18417,6 +18452,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/vscode-icons": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.1.14.tgz",
"integrity": "sha512-we1er6h6WNDuV/dNSey/f4RDea5DILfDPCM7QN32EkjINr3ggAh7JhfTuQPP9A5lUQkjq5Xu2BpLyiz+E0Yd8w==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify/types": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-1.1.0.tgz",
@ -19714,6 +19758,11 @@
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
"dev": true
},
"@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ=="
},
"@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",

3
packages/nc-gui/package.json

@ -41,6 +41,7 @@
"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",
@ -79,7 +80,9 @@
"@iconify-json/mi": "^1.1.2",
"@iconify-json/ph": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@iconify-json/simple-icons": "^1.1.29",
"@iconify-json/system-uicons": "^1.1.4",
"@iconify-json/vscode-icons": "^1.1.14",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",

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

@ -59,7 +59,9 @@ const { isOpen, toggle, toggleHasSidebar } = useSidebar('nc-left-sidebar', { has
const dialogOpen = ref(false)
const openDialogKey = ref<string>()
const openDialogKey = ref<string>('')
const dataSourcesState = ref<string>('')
const dropdownOpen = ref(false)
@ -73,11 +75,14 @@ const logout = () => {
navigateTo('/signin')
}
function toggleDialog(value?: boolean, key?: string) {
function toggleDialog(value?: boolean, key?: string, dsState?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key
openDialogKey.value = key || ''
dataSourcesState.value = dsState || ''
}
provide(ToggleDialogInj, toggleDialog)
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => {
switch (mode) {
case 'swatch': {
@ -234,7 +239,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</script>
<template>
<NuxtLayout id="content">
<NuxtLayout>
<template #sidebar>
<a-layout-sider
ref="sidebar"
@ -559,12 +564,16 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div>
</div>
<LazyDashboardTreeView />
<LazyDashboardTreeView @create-base-dlg="toggleDialog(true, 'dataSources')" />
</a-layout-sider>
</template>
<div>
<LazyDashboardSettingsModal v-model="dialogOpen" :open-key="openDialogKey" />
<LazyDashboardSettingsModal
v-model:model-value="dialogOpen"
v-model:open-key="openDialogKey"
v-model:data-sources-state="dataSourcesState"
/>
<NuxtPage :page-key="$route.params.projectId" />

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

@ -75,6 +75,7 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
</div>
</div>
<LazyGeneralShareBaseButton />
<LazyGeneralFullScreen class="nc-fullscreen-icon" />
</div>

5
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -84,9 +84,10 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<div>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
:model-value="null"
class="mt-0 nc-input"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title.replaceAll(' ', '')}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
/>
@ -95,7 +96,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
v-model="formState[field.title]"
class="nc-input"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title.replaceAll(' ', '')}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="true"
/>

2
packages/nc-gui/pages/[projectType]/gallery/[viewId]/index.vue

@ -26,7 +26,7 @@ try {
</script>
<template>
<NuxtLayout id="content" class="flex" name="shared-view">
<NuxtLayout class="flex" name="shared-view">
<div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" />
</div>

2
packages/nc-gui/pages/[projectType]/kanban/[viewId]/index.vue

@ -26,7 +26,7 @@ try {
</script>
<template>
<NuxtLayout id="content" class="flex" name="shared-view">
<NuxtLayout class="flex" name="shared-view">
<div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" />
</div>

2
packages/nc-gui/pages/[projectType]/view/[viewId].vue

@ -26,7 +26,7 @@ try {
</script>
<template>
<NuxtLayout id="content" class="flex" name="shared-view">
<NuxtLayout class="flex" name="shared-view">
<div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" />
</div>

5
packages/nc-gui/pages/forgot-password.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import type { RuleObject } from 'ant-design-vue/es/form'
import { definePageMeta, reactive, ref, useApi, useI18n, validateEmail } from '#imports'
definePageMeta({
@ -25,13 +26,13 @@ const formRules = {
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
if (validateEmail(v)) return resolve(true)
if (validateEmail(v)) return resolve()
reject(new Error(t('msg.error.signUpRules.emailInvalid')))
})
},
message: t('msg.error.signUpRules.emailInvalid'),
},
],
] as RuleObject[],
}
async function resetPassword() {

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

@ -7,6 +7,8 @@ definePageMeta({
allowedRoles: [Role.Super],
title: 'title.appStore',
})
useSidebar('nc-left-sidebar', { hasSidebar: false })
</script>
<template>

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

@ -2,6 +2,7 @@
import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import type { VNodeRef } from '@vue/runtime-core'
import type { RuleObject } from 'ant-design-vue/es/form'
import {
extractSdkResponseErrorMsg,
message,
@ -25,7 +26,7 @@ const nameValidationRules = [
message: 'Project name is required',
},
projectTitleValidator,
]
] as RuleObject[]
const form = ref<typeof Form>()

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

@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm } from '#imports'
import {
CertTypes,
ClientType,
Form,
Modal,
SSLUsage,
@ -25,8 +27,6 @@ import {
useSidebar,
watch,
} from '#imports'
import { ClientType } from '~/lib'
import type { DefaultConnection, ProjectCreateForm } from '~/utils'
const useForm = Form.useForm

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

Loading…
Cancel
Save