Browse Source

Merge pull request #4716 from nocodb/develop

pull/4717/head 0.101.0-beta.0
github-actions[bot] 2 years ago committed by GitHub
parent
commit
0ef5f829a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/ISSUE_TEMPLATE/--bug-report.yaml
  2. 52
      .github/uffizzi/docker-compose.uffizzi.yml
  3. 71
      .github/workflows/release-pr.yml
  4. 89
      .github/workflows/uffizzi-preview.yml
  5. 25
      README.md
  6. 15
      markdown/readme/languages/chinese.md
  7. 13
      markdown/readme/languages/dutch.md
  8. 12
      markdown/readme/languages/french.md
  9. 13
      markdown/readme/languages/german.md
  10. 13
      markdown/readme/languages/indonesian.md
  11. 14
      markdown/readme/languages/italian.md
  12. 13
      markdown/readme/languages/japanese.md
  13. 13
      markdown/readme/languages/korean.md
  14. 13
      markdown/readme/languages/portuguese.md
  15. 13
      markdown/readme/languages/russian.md
  16. 13
      markdown/readme/languages/spanish.md
  17. 410
      packages/nc-cli/package-lock.json
  18. 2
      packages/nc-cli/package.json
  19. 15
      packages/nc-gui/components.d.ts
  20. 4
      packages/nc-gui/components/account/License.vue
  21. 15
      packages/nc-gui/components/cell/DateTimePicker.vue
  22. 13
      packages/nc-gui/components/cell/Decimal.vue
  23. 13
      packages/nc-gui/components/cell/Float.vue
  24. 13
      packages/nc-gui/components/cell/Integer.vue
  25. 4
      packages/nc-gui/components/cell/Json.vue
  26. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  27. 2
      packages/nc-gui/components/cell/TextArea.vue
  28. 4
      packages/nc-gui/components/cell/TimePicker.vue
  29. 4
      packages/nc-gui/components/cell/attachment/Carousel.vue
  30. 657
      packages/nc-gui/components/dashboard/TreeView.vue
  31. 3
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  32. 430
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  33. 8
      packages/nc-gui/components/dashboard/settings/Erd.vue
  34. 25
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  35. 154
      packages/nc-gui/components/dashboard/settings/Modal.vue
  36. 30
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  37. 676
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  38. 652
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  39. 13
      packages/nc-gui/components/dlg/AirtableImport.vue
  40. 2
      packages/nc-gui/components/dlg/QuickImport.vue
  41. 9
      packages/nc-gui/components/dlg/TableCreate.vue
  42. 9
      packages/nc-gui/components/dlg/TableRename.vue
  43. 6
      packages/nc-gui/components/erd/TableNode.vue
  44. 19
      packages/nc-gui/components/erd/View.vue
  45. 22
      packages/nc-gui/components/general/AddBaseButton.vue
  46. 31
      packages/nc-gui/components/general/BaseLogo.vue
  47. 58
      packages/nc-gui/components/general/EmojiIcons.vue
  48. 5
      packages/nc-gui/components/general/ReleaseInfo.vue
  49. 13
      packages/nc-gui/components/general/ShareBaseButton.vue
  50. 22
      packages/nc-gui/components/general/TableIcon.vue
  51. 11
      packages/nc-gui/components/general/Tooltip.vue
  52. 4
      packages/nc-gui/components/general/TruncateText.vue
  53. 28
      packages/nc-gui/components/general/ViewIcon.vue
  54. 9
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  55. 5
      packages/nc-gui/components/smartsheet/Cell.vue
  56. 6
      packages/nc-gui/components/smartsheet/Form.vue
  57. 91
      packages/nc-gui/components/smartsheet/Grid.vue
  58. 4
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  59. 6
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  60. 120
      packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue
  61. 16
      packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue
  62. 38
      packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue
  63. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  64. 2
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  65. 74
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  66. 24
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  67. 39
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  68. 6
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  69. 30
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  70. 4
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  71. 9
      packages/nc-gui/components/smartsheet/column/utils.ts
  72. 2
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  73. 7
      packages/nc-gui/components/smartsheet/header/Cell.vue
  74. 5
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  75. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  76. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  77. 21
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  78. 52
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  79. 2
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  80. 12
      packages/nc-gui/components/smartsheet/toolbar/KanbanStackEditOrAdd.vue
  81. 4
      packages/nc-gui/components/smartsheet/toolbar/MoreActions.vue
  82. 12
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  83. 33
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  84. 5
      packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue
  85. 9
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  86. 25
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  87. 9
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  88. 10
      packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue
  89. 15
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  90. 2
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  91. 43
      packages/nc-gui/components/template/Editor.vue
  92. 2
      packages/nc-gui/components/virtual-cell/Formula.vue
  93. 27
      packages/nc-gui/components/virtual-cell/Lookup.vue
  94. 6
      packages/nc-gui/components/virtual-cell/QrCode.vue
  95. 17
      packages/nc-gui/components/virtual-cell/Rollup.vue
  96. 68
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  97. 39
      packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue
  98. 31
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  99. 2
      packages/nc-gui/components/webhook/List.vue
  100. 27
      packages/nc-gui/composables/useColumnCreateStore.ts
  101. Some files were not shown because too many files have changed in this diff Show More

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

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

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

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

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

89
.github/workflows/uffizzi-preview.yml

@ -0,0 +1,89 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "PR Release"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
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",

15
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,7 +87,10 @@ 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']
LogosSnowflakeIcon: typeof import('~icons/logos/snowflake-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['default']
MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
@ -138,13 +143,17 @@ declare module '@vue/runtime-core' {
MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiCog: typeof import('~icons/mdi/cog')['default']
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
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']
@ -176,10 +185,12 @@ 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']
MdiJson: typeof import('~icons/mdi/json')['default']
MdiKey: typeof import('~icons/mdi/key')['default']
MdiKeyboard: typeof import('~icons/mdi/keyboard')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyChange: typeof import('~icons/mdi/key-change')['default']
@ -218,7 +229,6 @@ declare module '@vue/runtime-core' {
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default']
MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default']
MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default']
@ -235,7 +245,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/account/License.vue

@ -35,10 +35,12 @@ loadLicense()
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<div class="text-xl mt-4 mb-8 text-center font-weight-bold">License</div>
<div class="mx-auto w-150">
<div>
<a-textarea v-model:value="key" placeholder="License key" class="!mt-2 !max-w-[600px]"></a-textarea>
</div>
<a-button class="mt-4" @click="setLicense" type="primary">Save license key</a-button>
<div class="text-center"> <a-button class="mt-4" @click="setLicense" type="primary">Save license key</a-button></div>
</div>
</div>
</template>

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

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

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

@ -16,7 +16,18 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)
const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({
get: () => _vModel.value,
set: (value: string) => {
if (value === '') {
_vModel.value = null
} else {
_vModel.value = value
}
},
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>

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

@ -16,7 +16,18 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)
const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({
get: () => _vModel.value,
set: (value: string) => {
if (value === '') {
_vModel.value = null
} else {
_vModel.value = value
}
},
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>

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

@ -16,7 +16,18 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)
const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({
get: () => _vModel.value,
set: (value: string) => {
if (value === '') {
_vModel.value = null
} else {
_vModel.value = value
}
},
})
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()

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>

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

@ -1,19 +1,24 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import type { Input } from 'ant-design-vue'
import { Dropdown, Tooltip, message } from 'ant-design-vue'
import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button'
import { Icon } from '@iconify/vue'
import type { VNodeRef } from '#imports'
import {
ClientType,
Empty,
TabType,
computed,
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isMac,
reactive,
ref,
resolveComponent,
useDialog,
useGlobal,
useNuxtApp,
useProject,
useRoute,
@ -26,11 +31,11 @@ import {
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
const { addTab } = useTabs()
const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp()
const { tables, loadTables, isSharedBase } = useProject()
const { bases, tables, loadTables, isSharedBase } = useProject()
const { activeTab } = useTabs()
@ -42,13 +47,19 @@ const route = useRoute()
const [searchActive, toggleSearchActive] = useToggle()
let key = $ref(0)
const { appInfo } = useGlobal()
const menuRef = $ref<HTMLLIElement>()
const toggleDialog = inject(ToggleDialogInj, () => {})
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 +70,22 @@ 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, {
handle: '.nc-drag-icon',
const base_id = el.getAttribute('nc-base')
if (!base_id) return
if (sortables[base_id]) sortables[base_id].destroy()
Sortable.create(el as HTMLLIElement, {
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 +115,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 +134,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 +169,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 +177,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 +188,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 +196,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 +207,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 +225,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 +245,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 +286,55 @@ 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 },
)
const setIcon = async (icon: string, table: TableType) => {
try {
table.meta = {
...(table.meta || {}),
icon,
}
tables.value.splice(tables.value.indexOf(table), 1, { ...table })
updateTab({ id: table.id }, { meta: table.meta })
$api.dbTable.update(table.id as string, {
meta: table.meta,
})
$e('a:table:icon:navdraw', { icon })
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<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 +347,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 +358,108 @@ 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
v-if="appInfo.ee"
key="connect-new-source"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)"
>
<div class="color-transition nc-project-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake
</div>
</a-menu-item>
</a-menu-item-group>
<div class="flex-1">
<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-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-2 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog(bases[0].id)"
>
<MdiPlus />
<MdiPlus class="w-5" />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{ $t('tooltip.addTable') }}</span>
<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 +468,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 +476,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 +501,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 +512,45 @@ 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
v-if="appInfo.ee"
key="connect-new-source"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE)"
>
<div class="color-transition nc-project-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-e="['e:datasource:import-request']"
@ -351,14 +568,21 @@ 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 class="transition-height duration-200">
<div class="border-none sortable-list">
<div v-if="bases[0]" :key="`base-${bases[0].id}`">
<div
v-for="table of tables"
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.title },
{ 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"
@ -367,27 +591,47 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
<GeneralTooltip class="pl-5 pr-3 py-2" modifier-key="Alt">
<GeneralTooltip class="pl-2 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<MdiDragVertical
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="isUIAllowed('tableIconCustomisation') ? Dropdown : 'div'"
trigger="click"
destroy-popup-on-hide
class="flex items-center"
@click.stop
>
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
</span>
<component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
v-else
class="nc-table-icon nc-view-icon w-5"
:class="{ 'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
<template v-if="isUIAllowed('tableIconCustomisation')" #title>Change icon</template>
</component>
</div>
<template v-if="isUIAllowed('tableIconCustomisation')" #overlay>
<GeneralEmojiIcons class="shadow bg-white p-2" @select-icon="setIcon($event, table)" />
</template>
</component>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
<GeneralTruncateText :key="table.title" :length="activeTable === table.id ? 18 : 20">{{
table.title
}}</GeneralTruncateText>
</div>
<a-dropdown
@ -395,11 +639,11 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
:trigger="['click']"
@click.stop
>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<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)">
<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>
@ -422,17 +666,322 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div>
</div>
</div>
</div>
</div>
<div v-else class="mt-0.5 pt-16 mx-3 flex flex-col items-center border-t-1 border-gray-50">
<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
>
<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 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"
: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(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 />
<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>
<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}`">
<component
:is="isUIAllowed('tableIconCustomisation') ? Dropdown : 'div'"
trigger="click"
destroy-popup-on-hide
class="flex items-center"
@click.stop
>
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
</span>
<component
:is="icon(table)"
v-else
class="nc-table-icon nc-view-icon w-5"
:class="{ 'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
<template v-if="isUIAllowed('tableIconCustomisation')" #title>Change icon</template>
</component>
</div>
<template v-if="isUIAllowed('tableIconCustomisation')" #overlay>
<GeneralEmojiIcons class="shadow bg-white p-2" @select-icon="setIcon($event, table)" />
</template>
</component>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
</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>
</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 +1008,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"
/>
@ -576,4 +1125,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>

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

@ -70,6 +70,7 @@ onMounted(async () => {
</script>
<template>
<div>
<a-modal
v-model:visible="showPluginInstallModal"
:class="{ active: showPluginInstallModal }"
@ -90,7 +91,6 @@ onMounted(async () => {
<a-modal
v-model:visible="showPluginUninstallModal"
:class="{ active: showPluginUninstallModal }"
:closable="false"
width="24rem"
centered
@ -161,6 +161,7 @@ onMounted(async () => {
</div>
</a-card>
</div>
</div>
</template>
<style scoped lang="scss">

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

@ -0,0 +1,430 @@
<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 ClientType.SNOWFLAKE:
clientType = ClientType.SNOWFLAKE
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>

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

154
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,8 +81,6 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth')
},
},
...(isUIAllowed('appStore')
? {
appStore: {
// App Store
title: t('title.appStore'),
@ -77,47 +88,26 @@ const tabsInfo: TabGroup = {
subTabs: {
new: {
title: 'Apps',
body: resolveComponent('DashboardSettingsAppStore'),
body: AppStore,
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
}
: {}),
projMetaData: {
// Project Metadata
title: t('title.projMeta'),
dataSources: {
// Data Sources
title: 'Data Sources',
icon: MultipleTableIcon,
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')
dataSources: {
title: 'Data Sources',
body: DataSources,
},
},
erd: {
title: t('title.erdView'),
body: resolveComponent('DashboardSettingsErd'),
onClick: () => {
$e('c:settings:erd')
},
},
misc: {
title: t('general.misc'),
body: resolveComponent('DashboardSettingsMisc'),
},
},
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>

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

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

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

@ -0,0 +1,676 @@
<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 as _clientTypes,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
generateUniqueName,
getDefaultConnectionConfig,
getTestDatabaseName,
nextTick,
onMounted,
projectTitleValidator,
readFile,
ref,
useApi,
useGlobal,
useI18n,
useNuxtApp,
watch,
} from '#imports'
const { connectionType } = defineProps<{ connectionType: ClientType }>()
const emit = defineEmits(['baseCreated'])
const { appInfo } = useGlobal()
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 clientTypes = computed(() => {
return _clientTypes.filter((type) => {
return appInfo.value?.ee || type.value !== ClientType.SNOWFLAKE
})
})
const validators = computed(() => {
let clientValidations: Record<string, any[]> = {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
}
switch (formState.dataSource.client) {
case ClientType.SQLITE:
clientValidations = {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
break
case ClientType.SNOWFLAKE:
clientValidations = {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
break
case ClientType.PG:
case ClientType.MSSQL:
clientValidations['dataSource.searchPath.0'] = [fieldRequiredValidator()]
break
}
return {
'title': [
{
required: true,
message: 'Base name is required',
},
projectTitleValidator,
],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
...clientValidations,
}
})
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-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.username']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<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>

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

@ -0,0 +1,652 @@
<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': [projectTitleValidator],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
...(formState.value.dataSource.client === ClientType.SQLITE
? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()],
}
: formState.value.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.username': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: {
'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()],
'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-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.username']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<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>

13
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'])
@ -44,8 +45,6 @@ const enableAbort = ref(false)
let socket: Socket | null
let socketInterval: NodeJS.Timer
const syncSource = ref({
id: '',
type: 'Airtable',
@ -100,7 +99,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 +112,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 },
@ -274,10 +273,10 @@ onMounted(async () => {
onBeforeUnmount(() => {
if (socket) {
socket.removeAllListeners()
socket.off('disconnect')
socket.disconnect()
socket.removeAllListeners()
}
clearInterval(socketInterval)
})
</script>

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 || ''

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

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

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>

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

@ -0,0 +1,31 @@
<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 LogosSnowflakeIcon from '~icons/logos/snowflake-icon'
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
case ClientType.SNOWFLAKE:
return LogosSnowflakeIcon
default:
return MdiDatabaseOutline
}
})
</script>
<template>
<component :is="baseIcon" />
</template>

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

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

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

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

13
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>
<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" />

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

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

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>

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

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

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

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

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>

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

6
packages/nc-gui/components/smartsheet/Form.vue

@ -33,7 +33,7 @@ provide(IsGalleryInj, ref(false))
// todo: generate hideCols based on default values
const hiddenCols = ['created_at', 'updated_at']
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.SpecificDBType]
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.Barcode, UITypes.SpecificDBType]
const state = useGlobal()
@ -229,7 +229,9 @@ async function addAllColumns() {
}
function shouldSkipColumn(col: Record<string, any>) {
return isDbRequired(col) || !!col.required || (!!col.rqd && !col.cdf) || col.uidt === UITypes.QrCode
return (
isDbRequired(col) || !!col.required || (!!col.rqd && !col.cdf) || col.uidt === UITypes.QrCode || col.uidt === UITypes.Barcode
)
}
async function removeAllColumns() {

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

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

@ -7,6 +7,7 @@ import {
IsFormInj,
RowInj,
inject,
isBarcode,
isBt,
isCount,
isFormula,
@ -24,7 +25,7 @@ import { NavigateDir } from '~/lib'
const props = defineProps<{
column: ColumnType
modelValue: any
row: Row
row?: Row
active?: boolean
}>()
@ -59,6 +60,7 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
<LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" />
<LazyVirtualCellBarcode v-else-if="isBarcode(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" />
</div>

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))

120
packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue

@ -0,0 +1,120 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { onMounted, useVModel, watch } from '#imports'
const props = defineProps<{
modelValue: any
}>()
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const vModel = useVModel(props, 'modelValue', emit)
const { setAdditionalValidations, validateInfos, column } = useColumnCreateStoreOrThrow()
const columnsAllowedAsBarcodeValue = computed<SelectProps['options']>(() => {
return fields.value
?.filter(
(el) =>
el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
})
})
const supportedBarcodeFormats = [
{ value: 'CODE128', label: 'CODE128' },
{ value: 'upc', label: 'UPC' },
{ value: 'EAN13', label: 'EAN-13' },
{ value: 'EAN8', label: 'EAN-8' },
{ value: 'EAN5', label: 'EAN-5' },
{ value: 'EAN2', label: 'EAN-2' },
{ value: 'CODE39', label: 'CODE39' },
{ value: 'ITF14', label: 'ITF-14' },
{ value: 'MSI', label: 'MSI' },
{ value: 'PHARMACODE', label: 'pharmacode' },
{ value: 'CODABAR', label: 'codabar' },
]
onMounted(() => {
// set default value
vModel.value.meta = {
barcodeFormat: supportedBarcodeFormats[0].value,
...vModel.value.meta,
}
vModel.value.fk_barcode_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id || columnsAllowedAsBarcodeValue.value?.[0]
})
watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => {
if (vModel.value.fk_barcode_value_column_id == null) {
vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.value
}
})
setAdditionalValidations({
fk_barcode_value_column_id: [{ required: true, message: 'Required' }],
barcode_format: [{ required: true, message: 'Required' }],
})
const showBarcodeValueColumnInfoIcon = computed(() => !columnsAllowedAsBarcodeValue.value?.length)
</script>
<template>
<a-row>
<a-col :span="24">
<a-form-item
class="flex pb-2 nc-barcode-value-column-select flex-row"
:label="$t('labels.barcodeValueColumn')"
v-bind="validateInfos.fk_barcode_value_column_id"
>
<div class="flex w-1/2 flex-row items-center">
<a-select
v-model:value="vModel.fk_barcode_value_column_id"
:options="columnsAllowedAsBarcodeValue"
placeholder="Select a column for the Barcode value"
not-found-content="No valid Column Type can be found."
@click.stop
/>
<div v-if="showBarcodeValueColumnInfoIcon" class="pl-2">
<a-tooltip placement="bottom">
<template #title>
<span>
The valid Column Types for a Barcode Column are: Number, Single Line Text, Long Text, Phone Number, URL, Email,
Decimal. Please create one first.
</span>
</template>
<mdi-information class="cursor-pointer" />
</a-tooltip>
</div>
</div>
</a-form-item>
<a-form-item
class="flex w-1/2 pb-2 nc-barcode-format-select"
:label="$t('labels.barcodeFormat')"
v-bind="validateInfos.barcode_format"
>
<a-select
v-model:value="vModel.meta.barcodeFormat"
:options="supportedBarcodeFormats"
placeholder="Select a Barcode format"
@click.stop
/>
</a-form-item>
</a-col>
</a-row>
</template>

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

@ -1,13 +1,5 @@
<script setup lang="ts">
import {
computed,
currencyCodes,
currencyLocales,
useProject,
useVModel,
validateCurrencyCode,
validateCurrencyLocale,
} from '#imports'
import { computed, currencyCodes, currencyLocales, useVModel, validateCurrencyCode, validateCurrencyLocale } from '#imports'
interface Option {
label: string
@ -49,14 +41,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 +54,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 ''
})

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

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

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

@ -171,12 +171,14 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"

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'>
}

74
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,
@ -26,7 +26,7 @@ const props = defineProps<{
const emit = defineEmits(['update:value'])
const uiTypesNotSupportedInFormulas = [UITypes.QrCode]
const uiTypesNotSupportedInFormulas = [UITypes.QrCode, UITypes.Barcode]
const vModel = useVModel(props, 'value', emit)
@ -235,6 +235,63 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
},
typeErrors,
)
} else if (parsedTree.callee.name === 'DATETIME_DIFF') {
// parsedTree.arguments[0] = date
validateAgainstType(
parsedTree.arguments[0],
formulaTypes.DATE,
(v: any) => {
if (!validateDateWithUnknownFormat(v)) {
typeErrors.add('The first parameter of DATETIME_DIFF() should have date value')
}
},
typeErrors,
)
// parsedTree.arguments[1] = date
validateAgainstType(
parsedTree.arguments[1],
formulaTypes.DATE,
(v: any) => {
if (!validateDateWithUnknownFormat(v)) {
typeErrors.add('The second parameter of DATETIME_DIFF() should have date value')
}
},
typeErrors,
)
// parsedTree.arguments[2] = ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"]
validateAgainstType(
parsedTree.arguments[2],
formulaTypes.STRING,
(v: any) => {
if (
![
'milliseconds',
'ms',
'seconds',
's',
'minutes',
'm',
'hours',
'h',
'days',
'd',
'weeks',
'w',
'months',
'M',
'quarters',
'Q',
'years',
'y',
].includes(v)
) {
typeErrors.add(
'The third parameter of DATETIME_DIFF() should have value either "milliseconds", "ms", "seconds", "s", "minutes", "m", "hours", "h", "days", "d", "weeks", "w", "months", "M", "quarters", "Q", "years", or "y"',
)
}
},
typeErrors,
)
}
}
}
@ -604,7 +661,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({

24
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,12 +68,18 @@ 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">
{{ table.title }}
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
<div class="flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500"></GeneralTableIcon>
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ table.title }}</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
@ -96,7 +104,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 +118,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>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrCode } from 'nocodb-sdk'
import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { useVModel } from '#imports'
@ -26,9 +26,7 @@ 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.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
)
.map((field) => {
return {

30
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)
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]
}

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

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

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" />

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

@ -9,6 +9,7 @@ import BTIcon from '~icons/mdi/table-arrow-left'
import MMIcon from '~icons/mdi/table-network'
import FormulaIcon from '~icons/mdi/math-integral'
import QrCodeScan from '~icons/mdi/qrcode-scan'
import BarcodeScan from '~icons/mdi/barcode-scan'
import RollupIcon from '~icons/mdi/movie-roll'
import CountIcon from '~icons/mdi/counter'
import SpecificDBTypeIcon from '~icons/mdi/database-settings'
@ -32,6 +33,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: FormulaIcon, color: 'text-grey' }
case UITypes.QrCode:
return { icon: QrCodeScan, color: 'text-grey' }
case UITypes.Barcode:
return { icon: BarcodeScan, color: 'text-grey' }
case UITypes.Lookup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:

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

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

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

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

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

@ -21,7 +21,7 @@ const localValue = computed({
const options = computed<SelectProps['options']>(() =>
meta.value?.columns
?.filter((c: ColumnType) => {
if (c.uidt === UITypes.QrCode) {
if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode) {
return false
} else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */

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(

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

33
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -196,6 +196,26 @@ watch(passwordProtected, (value) => {
const { locale } = useI18n()
const isRtl = computed(() => isRtlLang(locale.value as any))
const iframeCode = computed(() => {
if (!sharedViewUrl.value) return
return `<iframe class="nc-embed"
src="${sharedViewUrl.value}?embed"
frameborder="0"
width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"/>`
})
const copyIframeCode = async () => {
if (iframeCode.value) {
await copy(iframeCode.value)
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
}
}
</script>
<template>
@ -228,15 +248,22 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
data-testid="nc-modal-share-view__link"
class="share-link-box !bg-primary !bg-opacity-5 ring-1 ring-accent ring-opacity-100"
>
<div class="flex-1 h-min text-xs">{{ sharedViewUrl }}</div>
<div class="flex-1 h-min text-xs text-gray-500">{{ sharedViewUrl }}</div>
<a v-e="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank">
<MdiOpenInNew class="text-sm text-gray-500 mt-2" />
<MdiOpenInNew class="text-sm text-gray-500" />
</a>
<MdiContentCopy v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" />
</div>
<div
class="flex gap-1 items-center pb-1 text-gray-500 cursor-pointer font-weight-medium mb-2 mt-4 pl-1"
@click="copyIframeCode"
>
<MdiCodeTags class="text-gray-500" /> Embed this view in your site
</div>
<div class="px-1 mt-2 flex flex-col gap-3">
<!-- todo: i18n -->
<div class="text-gray-500 border-b-1">Options</div>
@ -352,7 +379,7 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
<style scoped>
.share-link-box {
@apply flex p-2 w-full items-center items-center gap-1 bg-gray-100 rounded;
@apply flex p-2 w-full items-center items-center gap-2 bg-gray-100 rounded;
}
:deep(.ant-collapse-header) {

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

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

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"
>

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

@ -13,7 +13,6 @@ import {
useProject,
useSmartsheetStoreOrThrow,
useUIPermission,
viewIcons,
} from '#imports'
import { LockType } from '~/lib'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
@ -30,9 +29,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 +48,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 +59,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 +82,6 @@ async function changeLockType(type: LockType) {
}
}
const { isSqlView } = useSmartsheetStoreOrThrow()
const open = ref(false)
useMenuCloseOnEsc(open)
@ -91,14 +92,10 @@ useMenuCloseOnEsc(open)
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<component
:is="viewIcons[selectedView?.type].icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
/>
<GeneralViewIcon :meta="selectedView"></GeneralViewIcon>
<span class="!text-sm font-weight-normal">
<GeneralTruncateText>{{ selectedView?.title }}</GeneralTruncateText>
<GeneralTruncateText :key="selectedView?.title">{{ selectedView?.title }}</GeneralTruncateText>
</span>
<component :is="Icon" class="text-gray-500" :class="`nc-icon-${selectedView?.lock_type}`" />
@ -117,7 +114,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"

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

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

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

@ -88,6 +88,7 @@ onMounted(() => {
</script>
<template>
<div>
<a-modal
v-model:visible="showNewTokenModal"
:class="{ active: showNewTokenModal }"
@ -134,7 +135,6 @@ onMounted(() => {
<a-modal
v-model:visible="showDeleteTokenModal"
:class="{ active: showDeleteTokenModal }"
:closable="false"
width="28rem"
centered
@ -214,7 +214,12 @@ onMounted(() => {
</a-button>
</a-tooltip>
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-api-token-mgmt">
<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]">
@ -239,4 +244,5 @@ onMounted(() => {
</div>
</div>
</div>
</div>
</template>

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

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

43
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) {
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/Lookup.vue

@ -12,8 +12,8 @@ import {
isAttachment,
provide,
ref,
refAutoReset,
useMetas,
useShowNotEditableWarning,
watch,
} from '#imports'
@ -75,26 +75,13 @@ provide(MetaInj, lookupTableMeta)
provide(CellUrlDisableOverlayInj, ref(true))
const timeout = 3000 // in ms
const showEditWarning = refAutoReset(false, timeout)
const showClearWarning = refAutoReset(false, timeout)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
showEditWarning.value = true
break
default:
showClearWarning.value = true
}
})
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =
useShowNotEditableWarning()
</script>
<template>
<div class="h-full">
<div class="h-full flex gap-1 overflow-x-auto p-1" @dblclick="showEditWarning = true">
<div class="h-full" @dblclick="activateShowEditNonEditableFieldWarning">
<div class="h-full flex gap-1 overflow-x-auto p-1">
<template v-if="lookupColumn">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)">
@ -133,10 +120,10 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template>
</div>
<div>
<div v-if="showEditWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.info.computedFieldEditWarning') }}
</div>
<div v-if="showClearWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<div v-if="showClearNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.info.computedFieldDeleteWarning') }}
</div>
</div>

6
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -9,6 +9,8 @@ const qrValue = computed(() => String(cellValue?.value))
const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOfAllowedCharsForQrValue)
const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsForQrCode.value)
const qrCode = useQRCode(qrValue, {
width: 150,
})
@ -40,12 +42,12 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
<template #footer>
<div class="mr-4" data-testid="nc-qr-code-large-value-label">{{ qrValue }}</div>
</template>
<img v-if="qrValue && !tooManyCharsForQrCode" :src="qrCodeLarge" alt="QR Code" />
<img v-if="showQrCode" :src="qrCodeLarge" alt="QR Code" />
</a-modal>
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.qrCodeValueTooLong') }}
</div>
<img v-if="qrValue && !tooManyCharsForQrCode" :src="qrCode" alt="QR Code" @click="showQrModal" />
<img v-if="showQrCode" :src="qrCode" alt="QR Code" @click="showQrModal" />
<div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.computedFieldUnableToClear') }}
</div>

17
packages/nc-gui/components/virtual-cell/Rollup.vue

@ -1,28 +1,23 @@
<script setup lang="ts">
import { CellValueInj, inject, refAutoReset } from '#imports'
import { CellValueInj, inject, useShowNotEditableWarning } from '#imports'
const value = inject(CellValueInj)
const timeout = 3000 // in ms
const showEditWarning = refAutoReset(false, timeout)
const showClearWarning = refAutoReset(false, timeout)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), () => (showClearWarning.value = true))
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =
useShowNotEditableWarning()
</script>
<template>
<div>
<div @dblclick="activateShowEditNonEditableFieldWarning">
<span class="text-center pl-3">
{{ value }}
</span>
<div>
<div v-if="showEditWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.info.computedFieldEditWarning') }}
</div>
<div v-if="showClearWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
<div v-if="showClearNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.info.computedFieldDeleteWarning') }}
</div>
</div>

68
packages/nc-gui/components/virtual-cell/barcode/Barcode.vue

@ -0,0 +1,68 @@
<script setup lang="ts">
import JsBarcodeWrapper from './JsBarcodeWrapper.vue'
const maxNumberOfAllowedCharsForBarcodeValue = 100
const cellValue = inject(CellValueInj)
const column = inject(ColumnInj)
const barcodeValue: ComputedRef<string> = computed(() => String(cellValue?.value || ''))
const tooManyCharsForBarcode = computed(() => barcodeValue.value.length > maxNumberOfAllowedCharsForBarcodeValue)
const modalVisible = ref(false)
const showBarcodeModal = () => {
modalVisible.value = true
}
const barcodeMeta = $computed(() => {
return {
barcodeFormat: 'CODE128',
...(column?.value?.meta || {}),
}
})
const handleModalOkClick = () => (modalVisible.value = false)
const showBarcode = computed(() => barcodeValue?.value.length > 0 && !tooManyCharsForBarcode.value)
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = useShowNotEditableWarning()
</script>
<template>
<a-modal
v-model:visible="modalVisible"
:class="{ active: modalVisible }"
wrap-class-name="nc-barcode-large"
:body-style="{ padding: '0px' }"
:footer="null"
@ok="handleModalOkClick"
>
<JsBarcodeWrapper v-if="showBarcode" :barcode-value="barcodeValue" :barcode-format="barcodeMeta.barcodeFormat" />
</a-modal>
<JsBarcodeWrapper
v-if="showBarcode"
:barcode-value="barcodeValue"
:barcode-format="barcodeMeta.barcodeFormat"
class="nc-barcode-svg"
@on-click-barcode="showBarcodeModal"
>
<template #barcodeRenderError>
<div class="text-left text-wrap mt-2 text-[#e65100] text-xs" data-testid="barcode-invalid-input-message">
{{ $t('msg.warning.barcode.renderError') }}
</div>
</template>
</JsBarcodeWrapper>
<div v-if="tooManyCharsForBarcode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.barcodeValueTooLong') }}
</div>
<div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.computedFieldUnableToClear') }}
</div>
<div v-if="showClearNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.barcodeFieldsCannotBeDirectlyChanged') }}
</div>
</template>

39
packages/nc-gui/components/virtual-cell/barcode/JsBarcodeWrapper.vue

@ -0,0 +1,39 @@
<script lang="ts" setup>
import JsBarcode from 'jsbarcode'
import { onMounted } from '#imports'
const props = defineProps({
barcodeValue: { type: String, required: true },
barcodeFormat: { type: String, required: true },
})
const emit = defineEmits(['onClickBarcode'])
const barcodeSvgRef = ref(null)
const errorForCurrentInput = ref(false)
const generate = () => {
try {
JsBarcode(barcodeSvgRef.value, String(props.barcodeValue), {
format: props.barcodeFormat,
})
errorForCurrentInput.value = false
} catch (e) {
console.log('e', e)
errorForCurrentInput.value = true
}
}
const onBarcodeClick = (ev: MouseEvent) => {
ev.stopPropagation()
emit('onClickBarcode')
}
watch([() => props.barcodeValue, () => props.barcodeFormat], generate)
onMounted(generate)
</script>
<template>
<svg v-show="!errorForCurrentInput" ref="barcodeSvgRef" class="w-full" data-testid="barcode" @click="onBarcodeClick"></svg>
<slot v-if="errorForCurrentInput" name="barcodeRenderError" />
</template>

31
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,21 @@ watch(
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1">
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
Link to '{{ relatedTableMeta.title }}'
<MdiLinkVariant class="text-xs" type="primary" />
Link to '
<GeneralTableIcon :meta="relatedTableMeta" class="-mx-1 w-5" />
{{ 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 +174,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,
}
},
)

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

Loading…
Cancel
Save