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. 4
      .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. 731
      packages/nc-gui/components/dashboard/TreeView.vue
  31. 169
      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. 180
      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. 15
      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. 38
      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. 270
      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. 45
      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

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

@ -35,7 +35,7 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Project Details 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: | placeholder: |
or provide the following info or provide the following info
``` ```
@ -58,4 +58,4 @@ body:
placeholder: | placeholder: |
> Drag & drop relevant image or videos > Drag & drop relevant image or videos
validations: validations:
required: false required: false

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 # reopened: closed pull request is reopened
# synchronize: commit(s) pushed to the pull request # synchronize: commit(s) pushed to the pull request
# ready_for_review: non PR release # 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: paths:
- "packages/nocodb-sdk/**" - "packages/nocodb-sdk/**"
- "packages/nc-gui/**" - "packages/nc-gui/**"
@ -20,7 +21,7 @@ concurrency:
jobs: jobs:
# enrich tag for pr release # enrich tag for pr release
set-tag: 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' runs-on: 'ubuntu-latest'
steps: steps:
- name: set-tag - name: set-tag
@ -47,7 +48,7 @@ jobs:
# Build, install, publish frontend and backend to npm # Build, install, publish frontend and backend to npm
release-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] needs: [set-tag]
uses: ./.github/workflows/release-npm.yml uses: ./.github/workflows/release-npm.yml
with: with:
@ -58,7 +59,7 @@ jobs:
# Build docker image and push to docker hub # Build docker image and push to docker hub
release-docker: 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] needs: [release-npm, set-tag]
uses: ./.github/workflows/release-docker.yml uses: ./.github/workflows/release-docker.yml
with: with:
@ -72,7 +73,7 @@ jobs:
# Build executables and publish to GitHub # Build executables and publish to GitHub
release-executables: 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] needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml uses: ./.github/workflows/release-timely-executables.yml
with: with:
@ -82,7 +83,7 @@ jobs:
# Add a comment for PR docker build # Add a comment for PR docker build
leave-comment: 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' runs-on: 'ubuntu-latest'
needs: [release-docker, set-tag] needs: [release-docker, set-tag]
steps: 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 }} 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 # Add a comment for PR executable build
leave-executable-comment: 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' runs-on: 'ubuntu-latest'
needs: [release-executables, set-tag] needs: [release-executables, set-tag]
steps: 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 }}) 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) ![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"> <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) [<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 # 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 ## NPX
You can run below command if you need an interactive configuration. 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 # Table of Contents
- [Quick try](#quick-try) - [Quick try](#quick-try)
* [1-Click Deploy to Heroku](#1-click-deploy-to-heroku)
* [NPX](#npx) * [NPX](#npx)
* [Node Application](#node-application) * [Node Application](#node-application)
* [Docker](#docker) * [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 ## 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 # 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 ### Docker gebruiken
```bash ```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 # 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 ### Utilisez Docker
```bash ```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 # 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 ### Verwenden von Docker
```bash ```bash

13
markdown/readme/languages/indonesian.md

@ -34,19 +34,6 @@ Mengubah MySQL, PostgreSQL, SQL Server, SQLite & MariaDB apapun menjadi spreadsh
# Mulai Cepat # 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 ### Menggunakan Docker
```bash ```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"> <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> <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> </p>
# Prova veloce
### 1-Click Deploy
#### Heroku # Prova veloce
<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>
### Con Docker ### 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 を使う ### Docker を使う
```bash ```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 사용 ### Docker 사용
```bash ```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 # 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. ### Usando o Docker.
```bash ```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 ### Используя Docker
```bash ```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 # 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 ### Usando docker
```bash ```bash

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

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

2
packages/nc-cli/package.json

@ -73,7 +73,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"download": "^8.0.0", "download": "^8.0.0",
"download-git-repo": "^3.0.2", "download-git-repo": "^3.0.2",
"express": "^4.17.1", "express": "^4.18.2",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"glob": "^7.1.6", "glob": "^7.1.6",
"inquirer": "^7.3.3", "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'] AAlert: typeof import('ant-design-vue/es')['Alert']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete'] AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon'] 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'] AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACardMeta: typeof import('ant-design-vue/es')['CardMeta'] 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'] IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default']
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default'] IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['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'] LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSnowflakeIcon: typeof import('~icons/logos/snowflake-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['default'] LogosSwagger: typeof import('~icons/logos/swagger')['default']
MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default'] MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-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'] MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default'] MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default'] MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiCog: typeof import('~icons/mdi/cog')['default'] MdiCog: typeof import('~icons/mdi/cog')['default']
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default'] MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default'] MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default'] MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['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'] 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'] MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default'] MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['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'] MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default'] MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default'] MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGraphOutline: typeof import('~icons/mdi/graph-outline')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default'] MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default'] MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default'] MdiInformation: typeof import('~icons/mdi/information')['default']
MdiJson: typeof import('~icons/mdi/json')['default'] MdiJson: typeof import('~icons/mdi/json')['default']
MdiKey: typeof import('~icons/mdi/key')['default']
MdiKeyboard: typeof import('~icons/mdi/keyboard')['default'] MdiKeyboard: typeof import('~icons/mdi/keyboard')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyChange: typeof import('~icons/mdi/key-change')['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'] MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default'] MdiStorefrontOutline: typeof import('~icons/mdi/storefront-outline')['default']
MdiTable: typeof import('~icons/mdi/table')['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'] MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default']
MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default'] MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['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'] MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
PhCloudLightningDuotone: typeof import('~icons/ph/cloud-lightning-duotone')['default'] PhCloudLightningDuotone: typeof import('~icons/ph/cloud-lightning-duotone')['default']
PhFileCsv: typeof import('~icons/ph/file-csv')['default'] PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RiTeamFill: typeof import('~icons/ri/team-fill')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] 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> <template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull"> <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="text-xl mt-4 mb-8 text-center font-weight-bold">License</div>
<div class="mx-auto w-150">
<div> <div>
<a-textarea v-model:value="key" placeholder="License key" class="!mt-2 !max-w-[600px]"></a-textarea> <a-textarea v-model:value="key" placeholder="License key" class="!mt-2 !max-w-[600px]"></a-textarea>
</div> </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> </div>
</template> </template>

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

@ -2,10 +2,13 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
ActiveCellInj, ActiveCellInj,
ColumnInj,
ReadonlyInj, ReadonlyInj,
dateFormats,
inject, inject,
isDrawerOrModalExist, isDrawerOrModalExist,
ref, ref,
timeFormats,
useProject, useProject,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
@ -28,9 +31,15 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
let isDateInvalid = $ref(false) 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({ let localState = $computed({
get() { get() {
@ -52,7 +61,7 @@ let localState = $computed({
} }
if (val.isValid()) { 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" :show-time="true"
:bordered="false" :bordered="false"
class="!w-full !px-0 !border-none" class="!w-full !px-0 !border-none"
format="YYYY-MM-DD HH:mm" :format="dateTimeFormat"
:placeholder="isDateInvalid ? 'Invalid date' : ''" :placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :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 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() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>

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

@ -16,7 +16,18 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj) 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() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>

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

@ -16,7 +16,18 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj) 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() 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="text" size="small" :onclick="clear"><div class="text-xs">Cancel</div></a-button>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel"> <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> </a-button>
</div> </div>
</div> </div>
<LazyMonacoEditor <LazyMonacoEditor
:model-value="localValue" :model-value="localValue || ''"
class="min-w-full w-80" class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }" :class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
:hide-minimap="true" :hide-minimap="true"

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

@ -105,7 +105,7 @@ const vModel = computed({
const selectedTitles = computed(() => const selectedTitles = computed(() =>
modelValue modelValue
? typeof modelValue === 'string' ? typeof modelValue === 'string'
? isMysql ? isMysql(column.value.base_id)
? modelValue.split(',').sort((a, b) => { ? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el) => el.title === a) const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b) 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' import { EditModeInj, inject, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
modelValue?: string | null modelValue?: string | number
}>() }>()
const emits = defineEmits(['update:modelValue']) 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 editable = inject(EditModeInj, ref(false))
const column = inject(ColumnInj)!
let isTimeInvalid = $ref(false) 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({ const localState = $computed({
get() { get() {

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

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

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

@ -1,19 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import type { Input } from 'ant-design-vue' import type { Input } from 'ant-design-vue'
import { Dropdown, Tooltip, message } from 'ant-design-vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button' import GithubButton from 'vue-github-button'
import { Icon } from '@iconify/vue'
import type { VNodeRef } from '#imports' import type { VNodeRef } from '#imports'
import { import {
ClientType,
Empty, Empty,
TabType, TabType,
computed, computed,
extractSdkResponseErrorMsg,
isDrawerOrModalExist, isDrawerOrModalExist,
isMac, isMac,
reactive, reactive,
ref, ref,
resolveComponent, resolveComponent,
useDialog, useDialog,
useGlobal,
useNuxtApp, useNuxtApp,
useProject, useProject,
useRoute, useRoute,
@ -26,11 +31,11 @@ import {
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large' import MdiTableLarge from '~icons/mdi/table-large'
const { addTab } = useTabs() const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { tables, loadTables, isSharedBase } = useProject() const { bases, tables, loadTables, isSharedBase } = useProject()
const { activeTab } = useTabs() const { activeTab } = useTabs()
@ -42,13 +47,19 @@ const route = useRoute()
const [searchActive, toggleSearchActive] = useToggle() 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('') 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(() => const tablesById = $computed(() =>
tables.value?.reduce<Record<string, TableType>>((acc, table) => { tables.value?.reduce<Record<string, TableType>>((acc, table) => {
@ -59,17 +70,22 @@ const tablesById = $computed(() =>
) )
const filteredTables = $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 // todo: replace with vuedraggable
const initSortable = (el: Element) => { const initSortable = (el: Element) => {
if (sortable) sortable.destroy() const base_id = el.getAttribute('nc-base')
sortable = Sortable.create(el as HTMLLIElement, { if (!base_id) return
handle: '.nc-drag-icon', if (sortables[base_id]) sortables[base_id].destroy()
Sortable.create(el as HTMLLIElement, {
onEnd: async (evt) => { onEnd: async (evt) => {
const offset = tables.value.findIndex((table) => table.base_id === base_id)
const { newIndex = 0, oldIndex = 0 } = evt const { newIndex = 0, oldIndex = 0 } = evt
const itemEl = evt.item as HTMLLIElement const itemEl = evt.item as HTMLLIElement
@ -99,10 +115,14 @@ const initSortable = (el: Element) => {
} }
// update the order of the moved item // 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 // 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 // update the item order
await $api.dbTable.reorder(item.id as string, { await $api.dbTable.reorder(item.id as string, {
@ -114,8 +134,12 @@ const initSortable = (el: Element) => {
} }
watchEffect(() => { watchEffect(() => {
if (menuRef) { if (menuRefs) {
initSortable(menuRef) 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 }) 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') $e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
const isOpen = ref(true) const isOpen = ref(true)
@ -153,6 +177,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const { close } = useDialog(resolveComponent('DlgTableRename'), { const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen, 'modelValue': isOpen,
'tableMeta': table, 'tableMeta': table,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog, '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}`) $e(`a:actions:import-${type}`)
const isOpen = ref(true) const isOpen = ref(true)
@ -171,6 +196,7 @@ function openQuickImportDialog(type: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), { const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'importType': type, 'importType': type,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -181,13 +207,14 @@ function openQuickImportDialog(type: string) {
} }
} }
function openAirtableImportDialog() { function openAirtableImportDialog(baseId?: string) {
$e('a:actions:import-airtable') $e('a:actions:import-airtable')
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgAirtableImport'), { const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -198,13 +225,14 @@ function openAirtableImportDialog() {
} }
} }
function openTableCreateDialog() { function openTableCreateDialog(baseId?: string) {
$e('c:table:create:navdraw') $e('c:table:create:navdraw')
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgTableCreate'), { const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen, 'modelValue': isOpen,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -217,9 +245,18 @@ function openTableCreateDialog() {
const searchInputRef: VNodeRef = (vnode: typeof Input) => vnode?.$el?.focus() const searchInputRef: VNodeRef = (vnode: typeof Input) => vnode?.$el?.focus()
const beforeSearch = ref<string[]>([])
const onSearchCloseIconClick = () => { const onSearchCloseIconClick = () => {
filterQuery = '' filterQuery = ''
toggleSearchActive(false) 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( 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> </script>
<template> <template>
<div class="nc-treeview-container flex flex-col"> <div class="nc-treeview-container flex flex-col">
<a-dropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu"> <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="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"> <Transition name="slide-left" mode="out-in">
<a-input <a-input
v-if="searchActive" 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"> <span v-else class="flex-1 text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
{{ $t('objects.tables') }} {{ $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> </span>
</Transition> </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)" /> <IcRoundSearch v-else class="text-lg text-primary mx-1 mt-0.5" @click="toggleSearchActive(true)" />
</Transition> </Transition>
</div> </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>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-accent" />
<!-- Request a data source you need? -->
{{ $t('labels.requestDataSource') }}
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="flex-1"> <div v-if="bases[0] && bases[0].enabled && !bases.slice(1).filter((el) => el.enabled)?.length" class="flex-1">
<div <div
v-if="isUIAllowed('table-create')" 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" 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" @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> <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> <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> <template #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
@ -298,7 +468,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-item <a-menu-item
v-if="isUIAllowed('airtableImport')" v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable" key="quick-import-airtable"
@click="openAirtableImportDialog" @click="openAirtableImportDialog(bases[0].id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-accent" /> <MdiTableLarge class="group-hover:text-accent" />
@ -306,14 +476,22 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div> </div>
</a-menu-item> </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"> <div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-accent" /> <MdiFileDocumentOutline class="group-hover:text-accent" />
CSV file CSV file
</div> </div>
</a-menu-item> </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"> <div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-accent" /> <MdiCodeJson class="group-hover:text-accent" />
JSON file JSON file
@ -323,7 +501,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-item <a-menu-item
v-if="isUIAllowed('excelImport')" v-if="isUIAllowed('excelImport')"
key="quick-import-excel" key="quick-import-excel"
@click="openQuickImportDialog('excel')" @click="openQuickImportDialog('excel', bases[0].id)"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-accent" /> <MdiFileExcel class="group-hover:text-accent" />
@ -334,6 +512,45 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-divider class="my-0" /> <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-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a <a
v-e="['e:datasource:import-request']" v-e="['e:datasource:import-request']"
@ -351,88 +568,420 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</a-dropdown> </a-dropdown>
</div> </div>
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden"> <div class="transition-height duration-200">
<div :key="key" ref="menuRef" class="border-none sortable-list"> <div class="border-none sortable-list">
<div <div v-if="bases[0]" :key="`base-${bases[0].id}`">
v-for="table of tables" <div
:key="table.id" v-if="bases[0] && bases[0].enabled"
v-e="['a:table:open']" ref="menuRefs"
:class="[ :key="`sortable-${bases[0].id}-${bases[0].id && bases[0].id in keys ? keys[bases[0].id] : '0'}`"
{ hidden: !filteredTables?.includes(table), active: activeTable === table.title }, :nc-base="bases[0].id"
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`, >
]" <div
class="nc-tree-item text-sm cursor-pointer group" v-for="table of tables.filter((table) => table.base_id === bases[0].id)"
:data-order="table.order" :key="table.id"
:data-id="table.id" v-e="['a:table:open']"
:data-testid="`tree-view-table-${table.title}`" :class="[
@click="addTableTab(table)" { 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-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}`">
<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 :key="table.title" :length="activeTable === table.id ? 18 : 20">{{
table.title
}}</GeneralTruncateText>
</div>
<a-dropdown
v-if="!isSharedBase && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']"
@click.stop
>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 outline-0" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table, bases[0].id)">
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`">
{{ $t('general.rename') }}
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-testid="`sidebar-table-delete-${table.title}`"
@click="deleteTable(table)"
>
<div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</GeneralTooltip>
</div>
</div>
</div>
</div>
</div>
<div
v-if="!tables.filter((table) => table.base_id === bases[0].id)?.length"
class="mt-0.5 pt-16 mx-3 flex flex-col items-center border-t-1 border-gray-50"
>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</div>
<div v-else class="transition-height duration-200">
<div class="border-none sortable-list">
<div v-for="[index, base] of Object.entries(bases)" :key="`base-${base.id}`">
<a-collapse
v-if="base && base.enabled"
v-model:activeKey="activeKey"
:class="[{ hidden: searchActive && !!filterQuery && !filteredTables?.find((el) => el.base_id === base.id) }]"
expand-icon-position="right"
:bordered="false"
:accordion="!searchActive"
ghost
> >
<GeneralTooltip class="pl-5 pr-3 py-2" modifier-key="Alt"> <a-collapse-panel :key="`collapse-${base.id}`">
<template #title>{{ table.table_name }}</template> <template #header>
<div v-if="index === '0'" class="flex items-center gap-2 text-gray-500 font-bold">
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)"> <GeneralBaseLogo :base-type="base.type" />
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`"> Default ({{ tables.filter((table) => table.base_id === base.id).length || '0' }})
<MdiDragVertical
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
</div> </div>
<div v-else class="flex items-center gap-2 text-gray-500 font-bold">
<div class="nc-tbl-title flex-1"> <GeneralBaseLogo :base-type="base.type" />
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText> {{ base.alias || '' }}
({{ tables.filter((table) => table.base_id === base.id).length || '0' }})
</div> </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 <a-dropdown
v-if="!isSharedBase && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))" v-if="!isSharedBase"
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop @click.stop
> >
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu outline-0" />
<template #overlay> <template #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)"> <!-- Quick Import From -->
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`"> <a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0">
{{ $t('general.rename') }} <a-menu-item
</div> 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-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div
v-else-if="isUIAllowed('table-create')"
class="group flex items-center gap-2 pl-8 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog(base.id)"
>
<MdiPlus />
<a-menu-item <span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{
v-if="isUIAllowed('table-delete')" $t('tooltip.addTable')
:data-testid="`sidebar-table-delete-${table.title}`" }}</span>
@click="deleteTable(table)"
> <a-dropdown
<div class="nc-project-menu-item"> v-if="!isSharedBase"
{{ $t('general.delete') }} :trigger="['click']"
</div> 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-item>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
</div> </div>
</GeneralTooltip> <div
</div> ref="menuRefs"
:key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`"
:nc-base="base.id"
>
<div
v-for="table of tables.filter((table) => table.base_id === base.id)"
:key="table.id"
v-e="['a:table:open']"
:class="[
{ hidden: !filteredTables?.includes(table), active: activeTable === table.id },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
]"
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
<GeneralTooltip class="pl-8 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<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>
<div v-else class="mt-0.5 pt-16 mx-3 flex flex-col items-center border-t-1 border-gray-50">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</div> </div>
</div> </div>
<template v-if="!isSharedBase" #overlay> <template v-if="!isSharedBase" #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'table'"> <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"> <div class="nc-project-menu-item">
{{ $t('general.rename') }} {{ $t('general.rename') }}
</div> </div>
@ -459,7 +1008,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-divider class="!my-0" /> <a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2"> <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" class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/> />
@ -527,7 +1076,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
} }
.nc-tree-item { .nc-tree-item {
@apply relative cursor-pointer after:(pointer-events-none content-[''] absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0); @apply relative cursor-pointer after:(pointer-events-none content-[''] absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0);
} }
.nc-tree-item svg { .nc-tree-item svg {
@ -576,4 +1125,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
:deep(.ant-dropdown-menu-title-content) { :deep(.ant-dropdown-menu-title-content) {
@apply !p-0; @apply !p-0;
} }
:deep(.ant-collapse-content-box) {
@apply !p-0;
}
:deep(.ant-collapse-header) {
@apply !border-0;
}
</style> </style>

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

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

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> <template>
<div class="w-full h-full !p-0 h-70vh"> <div class="w-full h-full !p-0 h-70vh">
<ErdView /> <ErdView :base-id="props.baseId" />
</div> </div>
</template> </template>

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

@ -1,6 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports' import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports'
const props = defineProps<{
baseId: string
}>()
const emit = defineEmits(['baseSynced'])
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project, loadTables } = useProject() const { project, loadTables } = useProject()
@ -19,7 +25,7 @@ async function loadMetaDiff() {
isLoading = true isLoading = true
isDifferent = false 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) { for (const model of metadiff) {
if (model.detectedChanges?.length > 0) { if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ') model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
@ -38,11 +44,12 @@ async function syncMetaDiff() {
if (!project.value?.id || !isDifferent) return if (!project.value?.id || !isDifferent) return
isLoading = true isLoading = true
await $api.project.metaDiffSync(project.value.id) await $api.base.metaDiffSync(project.value.id, props.baseId)
// Table metadata recreated successfully // Table metadata recreated successfully
message.info(t('msg.info.metaDataRecreated')) message.info(t('msg.info.metaDataRecreated'))
await loadTables() await loadTables()
await loadMetaDiff() await loadMetaDiff()
emit('baseSynced')
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} finally { } finally {
@ -63,8 +70,6 @@ const columns = [
// Models // Models
title: tableHeaderRenderer(t('labels.models')), title: tableHeaderRenderer(t('labels.models')),
key: 'table_name', key: 'table_name',
customRender: ({ record }: { record: { table_name: string; title?: string } }) =>
h('div', {}, record.title || record.table_name),
}, },
{ {
// Sync state // Sync state
@ -90,7 +95,6 @@ const columns = [
</div> </div>
</a-button> </a-button>
</div> </div>
<div class="max-h-600px overflow-y-auto"> <div class="max-h-600px overflow-y-auto">
<a-table <a-table
class="w-full" class="w-full"
@ -109,6 +113,17 @@ const columns = [
<template #emptyText> <template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template> </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> </a-table>
</div> </div>
</div> </div>

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

@ -1,14 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue' 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 StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill' import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple' import MultipleTableIcon from '~icons/mdi/table-multiple'
import NotebookOutline from '~icons/mdi/notebook-outline' import NotebookOutline from '~icons/mdi/notebook-outline'
import FolderCog from '~icons/mdi/folder-cog'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
openKey?: string openKey: string
dataSourcesState: string
} }
interface SubTabGroup { interface SubTabGroup {
@ -30,16 +35,24 @@ interface TabGroup {
const props = defineProps<Props>() 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 vModel = useVModel(props, 'modelValue', emits)
const vOpenKey = useVModel(props, 'openKey', emits)
const vDataState = useVModel(props, 'dataSourcesState', emits)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { t } = useI18n() const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const dataSourcesReload = ref(false)
const dataSourcesAwakened = ref(false)
const tabsInfo: TabGroup = { const tabsInfo: TabGroup = {
teamAndAuth: { teamAndAuth: {
title: t('title.teamAndAuth'), title: t('title.teamAndAuth'),
@ -68,56 +81,33 @@ const tabsInfo: TabGroup = {
$e('c:settings:team-auth') $e('c:settings:team-auth')
}, },
}, },
...(isUIAllowed('appStore') appStore: {
? { // App Store
appStore: { title: t('title.appStore'),
// App Store icon: StoreFrontOutline,
title: t('title.appStore'),
icon: StoreFrontOutline,
subTabs: {
new: {
title: 'Apps',
body: resolveComponent('DashboardSettingsAppStore'),
},
},
onClick: () => {
$e('c:settings:appstore')
},
},
}
: {}),
projMetaData: {
// Project Metadata
title: t('title.projMeta'),
icon: MultipleTableIcon,
subTabs: { subTabs: {
metaData: { new: {
// Metadata title: 'Apps',
title: t('title.metadata'), body: AppStore,
body: resolveComponent('DashboardSettingsMetadata'),
},
acl: {
// UI Access Control
title: t('title.uiACL'),
body: resolveComponent('DashboardSettingsUIAcl'),
onClick: () => {
$e('c:table:ui-acl')
},
},
erd: {
title: t('title.erdView'),
body: resolveComponent('DashboardSettingsErd'),
onClick: () => {
$e('c:settings:erd')
},
}, },
misc: { },
title: t('general.misc'), onClick: () => {
body: resolveComponent('DashboardSettingsMisc'), $e('c:settings:appstore')
},
},
dataSources: {
// Data Sources
title: 'Data Sources',
icon: MultipleTableIcon,
subTabs: {
dataSources: {
title: 'Data Sources',
body: DataSources,
}, },
}, },
onClick: () => { onClick: () => {
$e('c:settings:proj-metadata') vDataState.value = ''
$e('c:settings:data-sources')
}, },
}, },
audit: { audit: {
@ -135,29 +125,47 @@ const tabsInfo: TabGroup = {
$e('c:settings:audit') $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] const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
// Array of keys of tabs which are selected. In our case will be only one. // 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]]) const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]])
let selectedSubTabKeys = $ref<string[]>([firstKeyOfObject(selectedTab.subTabs)]) let selectedSubTabKeys = $ref<string[]>([firstKeyOfObject(selectedTab.subTabs)])
const selectedSubTab = $computed(() => selectedTab.subTabs[selectedSubTabKeys[0]]) const selectedSubTab = $computed(() => selectedTab.subTabs[selectedSubTabKeys[0]])
const handleAwaken = (val: boolean) => {
dataSourcesAwakened.value = val
}
watch( watch(
() => selectedTabKeys[0], () => selectedTabKeys[0],
(newTabKey) => { (newTabKey) => {
selectedSubTabKeys = [firstKeyOfObject(tabsInfo[newTabKey].subTabs)] selectedSubTabKeys = [firstKeyOfObject(tabsInfo[newTabKey].subTabs)]
}, },
) )
watch(
() => props.openKey,
(nextOpenKey) => {
selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === nextOpenKey) || firstKeyOfObject(tabsInfo)]
},
)
</script> </script>
<template> <template>
@ -210,7 +218,12 @@ watch(
<!-- Sub Tabs --> <!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500"> <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 <a-menu-item
v-for="(tab, key) of selectedTab.subTabs" v-for="(tab, key) of selectedTab.subTabs"
:key="key" :key="key"
@ -220,8 +233,59 @@ watch(
{{ tab.title }} {{ tab.title }}
</a-menu-item> </a-menu-item>
</a-menu> </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-content>
</a-layout> </a-layout>
</a-modal> </a-modal>

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

@ -10,9 +10,12 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
viewIcons,
} from '#imports' } from '#imports'
const props = defineProps<{
baseId: string
}>()
const { t } = useI18n() const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -32,8 +35,9 @@ const searchInput = $ref('')
const filteredTables = computed(() => const filteredTables = computed(() =>
tables.filter( tables.filter(
(el) => (el) =>
(typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) || el?.base_id === props.baseId &&
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase())), ((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>
<template #bodyCell="{ record, column }"> <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 v-if="column.name === 'view_name'">
<div class="flex items-center"> <div class="flex items-center gap-1">
<component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" /> <div class="min-w-5 flex items-center justify-center">
{{ record.title }} <GeneralViewIcon :meta="record" class="text-gray-500"></GeneralViewIcon>
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title }}</span>
</div> </div>
</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, watch,
} from '#imports' } from '#imports'
const { modelValue } = defineProps<{ const { modelValue, baseId } = defineProps<{
modelValue: boolean modelValue: boolean
baseId: string
}>() }>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -44,8 +45,6 @@ const enableAbort = ref(false)
let socket: Socket | null let socket: Socket | null
let socketInterval: NodeJS.Timer
const syncSource = ref({ const syncSource = ref({
id: '', id: '',
type: 'Airtable', type: 'Airtable',
@ -100,7 +99,7 @@ async function createOrUpdate() {
body: payload, body: payload,
}) })
} else { } 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, baseURL,
method: 'POST', method: 'POST',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
@ -113,7 +112,7 @@ async function createOrUpdate() {
} }
async function loadSyncSrc() { 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, baseURL,
method: 'GET', method: 'GET',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
@ -274,10 +273,10 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (socket) { if (socket) {
socket.removeAllListeners() socket.off('disconnect')
socket.disconnect() socket.disconnect()
socket.removeAllListeners()
} }
clearInterval(socketInterval)
}) })
</script> </script>

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

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

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

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

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

@ -21,9 +21,10 @@ import {
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
tableMeta: TableType tableMeta: TableType
baseId: string
} }
const { tableMeta, ...props } = defineProps<Props>() const { tableMeta, baseId, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated']) const emit = defineEmits(['update:modelValue', 'updated'])
@ -57,11 +58,11 @@ const validators = computed(() => {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255 let tableNameLengthLimit = 255
if (isMysql) { if (isMysql(baseId)) {
tableNameLengthLimit = 64 tableNameLengthLimit = 64
} else if (isPg) { } else if (isPg(baseId)) {
tableNameLengthLimit = 63 tableNameLengthLimit = 63
} else if (isMssql) { } else if (isMssql(baseId)) {
tableNameLengthLimit = 128 tableNameLengthLimit = 128
} }
const projectPrefix = project?.value?.prefix || '' 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="[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" 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' : ''" /> <GeneralTableIcon class="text-primary" :class="{ '!text-6xl !w-auto mr-2': showSkeleton }" :meta="table" />
<MdiEyeCircleOutline v-else class="text-primary" :class="showSkeleton ? 'text-6xl !px-2' : ''" /> <div :class="showSkeleton ? 'text-6xl' : ''" class="flex pr-2 pl-1">
<div :class="showSkeleton ? 'text-6xl' : ''" class="flex px-2">
{{ table.title }} {{ table.title }}
</div> </div>
</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 type { ERDConfig } from './utils'
import { reactive, ref, useMetas, useProject, watch } from '#imports' import { reactive, ref, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>() const props = defineProps<{ table?: TableType; baseId?: string }>()
const { tables: projectTables } = useProject() const { tables: projectTables } = useProject()
@ -18,7 +18,7 @@ const config = reactive<ERDConfig>({
showPkAndFk: true, showPkAndFk: true,
showViews: false, showViews: false,
showAllColumns: true, showAllColumns: true,
singleTableMode: !!table, singleTableMode: !!props.table,
showMMTables: false, showMMTables: false,
showJunctionTableNames: false, showJunctionTableNames: false,
}) })
@ -34,14 +34,13 @@ const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
} }
const populateTables = async () => { const populateTables = async () => {
let localTables: TableType[] let localTables: TableType[] = []
if (props.table) {
if (table) {
// if table is provided only get the table and its related tables // if table is provided only get the table and its related tables
localTables = projectTables.value.filter( localTables = projectTables.value.filter(
(t) => (t) =>
t.id === table.id || t.id === props.table.id ||
table.columns?.find( props.table.columns?.find(
(column) => (column) =>
column.uidt === UITypes.LinkToAnotherRecord && column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id, (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,
@ -59,7 +58,7 @@ const populateTables = async () => {
config.showMMTables || config.showMMTables ||
(!config.showMMTables && !t.mm) || (!config.showMMTables && !t.mm) ||
// Show mm table if it's the selected table // 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) .filter((t) => config.singleTableMode || (!config.showViews && t.type !== 'view') || config.showViews)
@ -76,6 +75,8 @@ watch(config, populateTables, {
deep: true, deep: true,
}) })
const filteredTables = computed(() => tables.value.filter((t) => !props.baseId || t.base_id === props.baseId))
watch( watch(
() => config.showAllColumns, () => config.showAllColumns,
() => { () => {
@ -87,7 +88,7 @@ watch(
<template> <template>
<div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]"> <div class="w-full" style="height: inherit" :class="[`nc-erd-vue-flow${config.singleTableMode ? '-single-table' : ''}`]">
<div class="relative h-full"> <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"> <GeneralOverlay v-model="isLoading" inline class="bg-gray-300/50">
<div class="h-full w-full flex flex-col justify-center items-center"> <div class="h-full w-full flex flex-col justify-center items-center">
<a-spin size="large" /> <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({ const releaseAlert = computed({
get() { get() {
if (currentVersion.value?.includes('-beta.') || latestRelease.value?.includes('-beta.')) {
return false
}
return ( return (
currentVersion.value && currentVersion.value &&
latestRelease.value && latestRelease.value &&
@ -22,7 +25,7 @@ const releaseAlert = computed({
async function fetchReleaseInfo() { async function fetchReleaseInfo() {
try { try {
const versionInfo = await $api.utils.appVersion() 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 currentVersion.value = versionInfo.currentVersion
latestRelease.value = versionInfo.releaseVersion latestRelease.value = versionInfo.releaseVersion
} else { } else {

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

@ -37,13 +37,16 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</script> </script>
<template> <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 v-if="isShareBaseAllowed">
<div class="flex items-center space-x-1"> <a-tooltip placement="left">
<MdiAccountPlusOutline class="mr-1 nc-share-base" /> <template #title>
<span class="text-xs">{{ $t('activity.inviteTeam') }}</span>
<div>{{ $t('activity.inviteTeam') }}</div> </template>
</div> <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> </div>
<LazyTabsAuthUserManagementUsersModal :show="showUserModal" @closed="showUserModal = false" /> <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> <script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core' import { onKeyStroke } from '@vueuse/core'
import type { CSSProperties } from '@vue/runtime-dom' import type { CSSProperties } from '@vue/runtime-dom'
import { controlledRef, ref, useElementHover, watch } from '#imports' import { controlledRef, ref, useAttrs, useElementHover, watch } from '#imports'
interface Props { interface Props {
// Key to be pressed on hover to trigger the tooltip // Key to be pressed on hover to trigger the tooltip
@ -23,6 +23,8 @@ const showTooltip = controlledRef(false, {
const isHovering = useElementHover(() => el.value) const isHovering = useElementHover(() => el.value)
const attrs = useAttrs()
const isKeyPressed = ref(false) const isKeyPressed = ref(false)
onKeyStroke( onKeyStroke(
@ -73,6 +75,11 @@ watch([isHovering, () => modifierKey, () => disabled], ([hovering, key, isDisabl
showTooltip.value = true showTooltip.value = true
} }
}) })
const divStyles = $computed(() => ({
style: attrs.style as CSSProperties,
class: attrs.class as string,
}))
</script> </script>
<template> <template>
@ -81,7 +88,7 @@ watch([isHovering, () => modifierKey, () => disabled], ([hovering, key, isDisabl
<slot name="title" /> <slot name="title" />
</template> </template>
<div ref="el" class="w-full" :class="$attrs.class" :style="$attrs.style"> <div ref="el" class="w-full" v-bind="divStyles">
<slot /> <slot />
</div> </div>
</a-tooltip> </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"> <div v-else class="w-full" data-testid="truncate-label">
<slot /> <slot />
</div> </div>
<div ref="text" class="hidden"> <div ref="text" class="hidden"><slot /></div>
<slot />
</div>
</template> </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 hide-minimap
/> />
<div v-if="activeLang.clients" class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase"> <div v-if="activeLang?.clients" class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select <a-select v-model:value="selectedClient" style="width: 6rem" dropdown-class-name="nc-dropdown-snippet-active-lang">
v-if="activeLang"
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"> <a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }} {{ client }}
</a-select-option> </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 { 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)) 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)" v-if="(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column)"
class="nc-locked-overlay" class="nc-locked-overlay"
@click.stop.prevent @click.stop.prevent
@dblclick.stop.prevent
/> />
</template> </template>
</div> </div>

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

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

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { computed, useColumnCreateStoreOrThrow, useProject, useVModel } from '#imports' import { computed, useColumnCreateStoreOrThrow, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any value: any
@ -10,9 +10,7 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { sqlUi } = useProject() const { onAlter, onDataTypeChange, validateInfos, sqlUi } = useColumnCreateStoreOrThrow()
const { onAlter, onDataTypeChange, validateInfos } = useColumnCreateStoreOrThrow()
// todo: 2nd argument of `getDataTypeListForUiType` is missing! // todo: 2nd argument of `getDataTypeListForUiType` is missing!
const dataTypes = computed(() => sqlUi.value.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any)) 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"> <script setup lang="ts">
import { import { computed, currencyCodes, currencyLocales, useVModel, validateCurrencyCode, validateCurrencyLocale } from '#imports'
computed,
currencyCodes,
currencyLocales,
useProject,
useVModel,
validateCurrencyCode,
validateCurrencyLocale,
} from '#imports'
interface Option { interface Option {
label: string label: string
@ -49,14 +41,12 @@ const validators = {
], ],
} }
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, isPg } = useColumnCreateStoreOrThrow()
setAdditionalValidations({ setAdditionalValidations({
...validators, ...validators,
}) })
const { isPg } = useProject()
const currencyList = currencyCodes || [] const currencyList = currencyCodes || []
const currencyLocaleList = currencyLocales() || [] const currencyLocaleList = currencyLocales() || []
@ -64,7 +54,7 @@ const currencyLocaleList = currencyLocales() || []
const isMoney = computed(() => vModel.value.dt === 'money') const isMoney = computed(() => vModel.value.dt === 'money')
const message = computed(() => { 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 '' 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" /> <LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="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" /> <LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" 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" /> <LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" 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" /> <LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" 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" /> <LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions <LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord" 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' import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#imports'
interface Props { interface Props {
column?: ColumnType & { meta: any } column?: ColumnType
columnPosition?: Pick<ColumnReqType, 'column_order'> 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 { Ref } from 'vue'
import type { ListItem as AntListItem } from 'ant-design-vue' import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep' import jsep from 'jsep'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType, FormulaType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk' import { UITypes, jsepCurlyHook, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import { import {
MetaInj, MetaInj,
NcAutocompleteTree, NcAutocompleteTree,
@ -26,7 +26,7 @@ const props = defineProps<{
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const uiTypesNotSupportedInFormulas = [UITypes.QrCode] const uiTypesNotSupportedInFormulas = [UITypes.QrCode, UITypes.Barcode]
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
@ -235,6 +235,63 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
}, },
typeErrors, 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 // 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 // set additional validations
setAdditionalValidations({ 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 meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi } = useColumnCreateStoreOrThrow()
const { tables, sqlUi } = $(useProject()) const { tables } = $(useProject())
setAdditionalValidations({ setAdditionalValidations({
childId: [{ required: true, message: 'Required' }], childId: [{ required: true, message: 'Required' }],
@ -44,8 +44,10 @@ const refTables = $computed(() => {
return [] 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> </script>
<template> <template>
@ -66,12 +68,18 @@ const refTables = $computed(() => {
<a-select <a-select
v-model:value="vModel.childId" v-model:value="vModel.childId"
show-search show-search
:filter-option="(value, option) => option.key.toLowerCase().includes(value.toLowerCase())" :filter-option="filterOption"
dropdown-class-name="nc-dropdown-ltar-child-table" dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange" @change="onDataTypeChange"
> >
<a-select-option v-for="table in refTables" :key="table.title" :value="table.id"> <a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
{{ table.title }} <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-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -96,7 +104,7 @@ const refTables = $computed(() => {
dropdown-class-name="nc-dropdown-on-update" dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange" @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 }} {{ option }}
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -110,7 +118,7 @@ const refTables = $computed(() => {
dropdown-class-name="nc-dropdown-on-delete" dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange" @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 }} {{ option }}
</a-select-option> </a-select-option>
</a-select> </a-select>

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

@ -1,6 +1,7 @@
<script setup lang="ts"> <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 { UITypes, isSystemColumn } from 'nocodb-sdk'
import { getRelationName } from './utils'
import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
const props = defineProps<{ 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_relation_column_id) vModel.value.fk_relation_column_id = null
if (!vModel.value.fk_lookup_column_id) vModel.value.fk_lookup_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(() => { const refTables = $computed(() => {
if (!tables || !tables.length) { if (!tables || !tables.length || !meta || !meta.columns) {
return [] return []
} }
return meta?.columns const _refTables = meta.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && !c.system) .filter((column) => column.uidt === UITypes.LinkToAnotherRecord && !column.system && column.base_id === meta?.base_id)
.map((c: ColumnType) => ({ .map((column) => ({
col: c.colOptions, col: column.colOptions,
column: c, column,
...tables.find((t) => t.id === (c.colOptions as LinkToAnotherRecordType).fk_related_model_id), ...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 columns = $computed<ColumnType[]>(() => {
const selectedTable = refTables?.find((t) => t.column.id === vModel.value.fk_relation_column_id) const selectedTable = refTables.find((t) => t.column.id === vModel.value.fk_relation_column_id)
if (!selectedTable?.id) { if (!selectedTable?.id) {
return [] return []
} }
return metas[selectedTable.id].columns.filter((c: ColumnType) => !isSystemColumn(c))
return metas[selectedTable.id].columns.filter((c: any) => {
return !(isSystemColumn(c) || c.uidt === UITypes.QrCode)
})
}) })
</script> </script>
@ -69,11 +62,11 @@ const columns = $computed(() => {
dropdown-class-name="!w-64 nc-dropdown-relation-table" dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange" @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="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="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600"> <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>
</div> </div>
</a-select-option> </a-select-option>

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { UITypes } from 'nocodb-sdk' import type { UITypes } from 'nocodb-sdk'
import { AllowedColumnTypesForQrCode } from 'nocodb-sdk' import { AllowedColumnTypesForQrAndBarcodes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import { useVModel } from '#imports' import { useVModel } from '#imports'
@ -26,9 +26,7 @@ const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
return fields.value return fields.value
?.filter( ?.filter(
(el) => (el) =>
el.fk_column_id && el.fk_column_id && AllowedColumnTypesForQrAndBarcodes.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
// AllowedColumnTypesForQrCode.map((el) => el.toString()).includes(metaColumnById.value[el.fk_column_id].uidt),
AllowedColumnTypesForQrCode.includes(metaColumnById.value[el.fk_column_id].uidt as UITypes),
) )
.map((field) => { .map((field) => {
return { return {

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

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { getRelationName } from './utils'
import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports' import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
@ -24,11 +26,6 @@ setAdditionalValidations({
rollup_function: [{ required: true, message: 'Required' }], rollup_function: [{ required: true, message: 'Required' }],
}) })
const relationNames = {
mm: 'Many To Many',
hm: 'Has Many',
}
const aggrFunctionsList = [ const aggrFunctionsList = [
{ text: 'count', value: 'count' }, { text: 'count', value: 'count' },
{ text: 'min', value: 'min' }, { 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 if (!vModel.value.rollup_function) vModel.value.rollup_function = null
const refTables = $computed(() => { const refTables = $computed(() => {
if (!tables || !tables.length) { if (!tables || !tables.length || !meta || !meta.columns) {
return [] return []
} }
return ( const _refTables = meta.columns
meta?.columns .filter(
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system) (c) =>
.map((c) => ({ c.uidt === UITypes.LinkToAnotherRecord &&
col: c.colOptions, (c.colOptions as LinkToAnotherRecordType).type !== 'bt' &&
column: c, !c.system &&
...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id), 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(() => { const columns = $computed(() => {
@ -67,7 +69,7 @@ const columns = $computed(() => {
return [] 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> </script>
@ -80,11 +82,11 @@ const columns = $computed(() => {
dropdown-class-name="!w-64 nc-dropdown-relation-table" dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange" @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="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="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600"> <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>
</div> </div>
</a-select-option> </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 vModel = useVModel(props, 'value', emit)
const { isPg, isMysql } = useProject() const { setAdditionalValidations, validateInfos, isPg, isMysql } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([]) 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> <template>
<div class="flex p-2 items-center gap-2 p-4 nc-expanded-form-header"> <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"> <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"> <template v-if="meta">
{{ meta.title }} {{ meta.title }}

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

@ -2,7 +2,12 @@
import type { ColumnReqType, ColumnType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { ColumnInj, IsFormInj, IsKanbanInj, inject, provide, ref, toRef, useUIPermission } from '#imports' 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') const hideMenu = toRef(props, 'hideMenu')

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

@ -19,6 +19,7 @@ import {
isJSON, isJSON,
isPercent, isPercent,
isPhoneNumber, isPhoneNumber,
isPrimaryKey,
isRating, isRating,
isSet, isSet,
isSingleSelect, isSingleSelect,
@ -118,7 +119,9 @@ export default defineComponent({
const column = inject(ColumnInj, columnMeta) 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)) 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> <span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span>
</a-tooltip> </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"> <template v-if="!hideMenu">
<div class="flex-1" /> <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 MMIcon from '~icons/mdi/table-network'
import FormulaIcon from '~icons/mdi/math-integral' import FormulaIcon from '~icons/mdi/math-integral'
import QrCodeScan from '~icons/mdi/qrcode-scan' import QrCodeScan from '~icons/mdi/qrcode-scan'
import BarcodeScan from '~icons/mdi/barcode-scan'
import RollupIcon from '~icons/mdi/movie-roll' import RollupIcon from '~icons/mdi/movie-roll'
import CountIcon from '~icons/mdi/counter' import CountIcon from '~icons/mdi/counter'
import SpecificDBTypeIcon from '~icons/mdi/database-settings' import SpecificDBTypeIcon from '~icons/mdi/database-settings'
@ -32,6 +33,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: FormulaIcon, color: 'text-grey' } return { icon: FormulaIcon, color: 'text-grey' }
case UITypes.QrCode: case UITypes.QrCode:
return { icon: QrCodeScan, color: 'text-grey' } return { icon: QrCodeScan, color: 'text-grey' }
case UITypes.Barcode:
return { icon: BarcodeScan, color: 'text-grey' }
case UITypes.Lookup: case UITypes.Lookup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) { switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: 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() if (sortable) sortable.destroy()
sortable = new Sortable(el, { sortable = new Sortable(el, {
handle: '.nc-drag-icon', // handle: '.nc-drag-icon',
ghostClass: 'ghost', ghostClass: 'ghost',
onStart: onSortStart, onStart: onSortStart,
onEnd: onSortEnd, onEnd: onSortEnd,
@ -213,6 +213,24 @@ function openDeleteDialog(view: ViewType) {
close(1000) 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> </script>
<template> <template>
@ -234,6 +252,7 @@ function openDeleteDialog(view: ViewType) {
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog" @delete="openDeleteDialog"
@rename="onRename" @rename="onRename"
@select-icon="setIcon($event, view)"
/> />
</a-menu> </a-menu>
</template> </template>

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

@ -1,18 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk' import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity' import type { WritableComputedRef } from '@vue/reactivity'
import { import { Tooltip } from 'ant-design-vue'
IsLockedInj, import { IsLockedInj, inject, message, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports'
computed,
inject,
message,
onKeyStroke,
useDebounceFn,
useNuxtApp,
useUIPermission,
useVModel,
viewIcons,
} from '#imports'
interface Props { interface Props {
view: ViewType view: ViewType
@ -21,9 +11,15 @@ interface Props {
interface Emits { interface Emits {
(event: 'update:view', data: Record<string, any>): void (event: 'update:view', data: Record<string, any>): void
(event: 'selectIcon', icon: string): void
(event: 'changeView', view: Record<string, any>): void (event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: ViewType): void (event: 'rename', view: ViewType): void
(event: 'delete', view: ViewType): void (event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): 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 */ /** Original view title when editing the view name */
let originalTitle = $ref<string | undefined>() 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} */ /** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */
const onClick = useDebounceFn(() => { const onClick = useDebounceFn(() => {
if (isEditing || isStopped) return if (isEditing || isStopped) return
@ -172,20 +166,26 @@ function onStopEdit() {
@click.stop="onClick" @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 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}`"> <div class="flex w-auto min-w-5" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<MdiDrag <a-dropdown :trigger="['click']" @click.stop>
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move" <component :is="isUIAllowed('viewIconCustomisation') ? Tooltip : 'div'">
@click.stop.prevent <GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
/> <template v-if="isUIAllowed('viewIconCustomisation')" #title>Change icon</template>
</component>
<component
:is="viewIcons[viewType].icon" <template v-if="isUIAllowed('viewIconCustomisation')" #overlay>
class="nc-view-icon group-hover:hidden" <GeneralEmojiIcons class="shadow bg-white p-2" @select-icon="emits('selectIcon', $event)" />
:style="{ color: viewIcons[viewType].color }" </template>
/> </a-dropdown>
</div> </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> <div v-else>
<LazyGeneralTruncateText>{{ vModel.alias || vModel.title }}</LazyGeneralTruncateText> <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']>(() => const options = computed<SelectProps['options']>(() =>
meta.value?.columns meta.value?.columns
?.filter((c: ColumnType) => { ?.filter((c: ColumnType) => {
if (c.uidt === UITypes.QrCode) { if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode) {
return false return false
} else if (isSort) { } else if (isSort) {
/** ignore hasmany and manytomany relations if it's using within sort menu */ /** 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"> <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() const { isUIAllowed } = useUIPermission()

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

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

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

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TableType } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
@ -16,7 +17,7 @@ const { meta } = useSmartsheetStoreOrThrow()
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const { search, loadFieldQuery } = useFieldQuery(activeView) const { search, loadFieldQuery } = useFieldQuery()
const isDropdownOpen = ref(false) const isDropdownOpen = ref(false)
@ -25,9 +26,9 @@ const searchDropdown = ref(null)
onClickOutside(searchDropdown, () => (isDropdownOpen.value = false)) onClickOutside(searchDropdown, () => (isDropdownOpen.value = false))
const columns = computed(() => const columns = computed(() =>
meta.value?.columns?.map((c) => ({ (meta.value as TableType)?.columns?.map((column) => ({
value: c.id, value: column.id,
label: c.title, label: column.title,
})), })),
) )
@ -35,7 +36,7 @@ watch(
() => activeView.value?.id, () => activeView.value?.id,
(n, o) => { (n, o) => {
if (n !== o) { if (n !== o) {
loadFieldQuery(activeView) loadFieldQuery(activeView.value?.id)
} }
}, },
{ immediate: true }, { immediate: true },
@ -75,6 +76,7 @@ function onPressEnter() {
class="max-w-[200px]" class="max-w-[200px]"
:placeholder="$t('placeholder.filterQuery')" :placeholder="$t('placeholder.filterQuery')"
:bordered="false" :bordered="false"
data-testid="search-data-input"
@press-enter="onPressEnter" @press-enter="onPressEnter"
> >
<template #addonBefore> </template> <template #addonBefore> </template>

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

@ -196,6 +196,26 @@ watch(passwordProtected, (value) => {
const { locale } = useI18n() const { locale } = useI18n()
const isRtl = computed(() => isRtlLang(locale.value as any)) 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> </script>
<template> <template>
@ -228,15 +248,22 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
data-testid="nc-modal-share-view__link" data-testid="nc-modal-share-view__link"
class="share-link-box !bg-primary !bg-opacity-5 ring-1 ring-accent ring-opacity-100" 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"> <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> </a>
<MdiContentCopy v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" /> <MdiContentCopy v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" />
</div> </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"> <div class="px-1 mt-2 flex flex-col gap-3">
<!-- todo: i18n --> <!-- todo: i18n -->
<div class="text-gray-500 border-b-1">Options</div> <div class="text-gray-500 border-b-1">Options</div>
@ -352,7 +379,7 @@ const isRtl = computed(() => isRtlLang(locale.value as any))
<style scoped> <style scoped>
.share-link-box { .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) { :deep(.ant-collapse-header) {

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

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

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

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

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

@ -13,7 +13,6 @@ import {
useProject, useProject,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission, useUIPermission,
viewIcons,
} from '#imports' } from '#imports'
import { LockType } from '~/lib' import { LockType } from '~/lib'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
@ -30,9 +29,11 @@ const isView = false
const { $api, $e } = useNuxtApp() 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) const showWebhookDrawer = ref(false)
@ -47,7 +48,7 @@ const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject() const { isSharedBase } = useProject()
const Icon = computed(() => { const Icon = computed(() => {
switch (selectedView?.value.lock_type) { switch (selectedView.value?.lock_type) {
case LockType.Personal: case LockType.Personal:
return MdiAccountIcon return MdiAccountIcon
case LockType.Locked: 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) { async function changeLockType(type: LockType) {
$e('a:grid:lockmenu', { lockType: type }) $e('a:grid:lockmenu', { lockType: type })
if (!selectedView?.value) return if (!selectedView.value) return
if (type === 'personal') { if (type === 'personal') {
// Coming soon // Coming soon
@ -79,8 +82,6 @@ async function changeLockType(type: LockType) {
} }
} }
const { isSqlView } = useSmartsheetStoreOrThrow()
const open = ref(false) const open = ref(false)
useMenuCloseOnEsc(open) useMenuCloseOnEsc(open)
@ -91,14 +92,10 @@ useMenuCloseOnEsc(open)
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu"> <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"> <a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<component <GeneralViewIcon :meta="selectedView"></GeneralViewIcon>
:is="viewIcons[selectedView?.type].icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
/>
<span class="!text-sm font-weight-normal"> <span class="!text-sm font-weight-normal">
<GeneralTruncateText>{{ selectedView?.title }}</GeneralTruncateText> <GeneralTruncateText :key="selectedView?.title">{{ selectedView?.title }}</GeneralTruncateText>
</span> </span>
<component :is="Icon" class="text-gray-500" :class="`nc-icon-${selectedView?.lock_type}`" /> <component :is="Icon" class="text-gray-500" :class="`nc-icon-${selectedView?.lock_type}`" />
@ -117,7 +114,7 @@ useMenuCloseOnEsc(open)
> >
<template #title> <template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0"> <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 <MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" 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"> <script setup lang="ts">
import { ActiveViewInj, inject, viewIcons } from '#imports' import { ActiveViewInj, inject } from '#imports'
const selectedView = inject(ActiveViewInj) const selectedView = inject(ActiveViewInj)
</script> </script>
<template> <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"> <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 <GeneralViewIcon class="nc-view-icon" :meta="selectedView" />
:is="viewIcons[selectedView?.type].icon"
v-if="selectedView?.type"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
/>
<span class="!text-sm font-medium max-w-36 overflow-ellipsis overflow-hidden whitespace-nowrap"> <span class="!text-sm font-medium max-w-36 overflow-ellipsis overflow-hidden whitespace-nowrap">
{{ selectedView?.title }} {{ selectedView?.title }}

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

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

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

@ -44,7 +44,7 @@ let isLoading = $ref(false)
let totalRows = $ref(0) let totalRows = $ref(0)
const currentPage = $ref(1) let currentPage = $ref(1)
const currentLimit = $ref(10) 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, { const response: any = await api.auth.projectUserList(project.value?.id, {
query: { query: {
limit, limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0, offset: (page - 1) * limit,
query: searchText.value, query: searchText.value,
}, },
} as RequestParams) } 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 }) => { const isSuperAdmin = (user: { main_roles?: string }) => {
return user.main_roles?.split(',').includes(OrgUserRoles.SUPER_ADMIN) 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"> <div v-else class="flex flex-col w-full px-6">
<LazyTabsAuthUserManagementUsersModal <LazyTabsAuthUserManagementUsersModal
:key="showUserModal" :key="`${showUserModal}`"
:show="showUserModal" :show="showUserModal"
:selected-user="selectedUser" :selected-user="selectedUser"
@closed="showUserModal = false" @closed="showUserModal = false"

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

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

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

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

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

@ -10,7 +10,7 @@ const cellValue = inject(CellValueInj)
const { isPg } = useProject() 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)) const urls = computed(() => replaceUrlsWithLink(result.value))

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

@ -12,8 +12,8 @@ import {
isAttachment, isAttachment,
provide, provide,
ref, ref,
refAutoReset,
useMetas, useMetas,
useShowNotEditableWarning,
watch, watch,
} from '#imports' } from '#imports'
@ -75,26 +75,13 @@ provide(MetaInj, lookupTableMeta)
provide(CellUrlDisableOverlayInj, ref(true)) provide(CellUrlDisableOverlayInj, ref(true))
const timeout = 3000 // in ms const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =
useShowNotEditableWarning()
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
}
})
</script> </script>
<template> <template>
<div class="h-full"> <div class="h-full" @dblclick="activateShowEditNonEditableFieldWarning">
<div class="h-full flex gap-1 overflow-x-auto p-1" @dblclick="showEditWarning = true"> <div class="h-full flex gap-1 overflow-x-auto p-1">
<template v-if="lookupColumn"> <template v-if="lookupColumn">
<!-- Render virtual cell --> <!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)"> <div v-if="isVirtualCol(lookupColumn)">
@ -133,10 +120,10 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template> </template>
</div> </div>
<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') }} {{ $t('msg.info.computedFieldEditWarning') }}
</div> </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') }} {{ $t('msg.info.computedFieldDeleteWarning') }}
</div> </div>
</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 tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOfAllowedCharsForQrValue)
const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsForQrCode.value)
const qrCode = useQRCode(qrValue, { const qrCode = useQRCode(qrValue, {
width: 150, width: 150,
}) })
@ -40,12 +42,12 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
<template #footer> <template #footer>
<div class="mr-4" data-testid="nc-qr-code-large-value-label">{{ qrValue }}</div> <div class="mr-4" data-testid="nc-qr-code-large-value-label">{{ qrValue }}</div>
</template> </template>
<img v-if="qrValue && !tooManyCharsForQrCode" :src="qrCodeLarge" alt="QR Code" /> <img v-if="showQrCode" :src="qrCodeLarge" alt="QR Code" />
</a-modal> </a-modal>
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs"> <div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.qrCodeValueTooLong') }} {{ $t('labels.qrCodeValueTooLong') }}
</div> </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"> <div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.computedFieldUnableToClear') }} {{ $t('msg.warning.nonEditableFields.computedFieldUnableToClear') }}
</div> </div>

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

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

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

@ -52,7 +52,7 @@ onMounted(() => {
<template> <template>
<div class=""> <div class="">
<div class="mb-2"> <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 <a-button
v-e="['c:webhook:add']" 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 type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form'
import { import {
Form, Form,
computed, computed,
@ -20,10 +21,13 @@ const useForm = Form.useForm
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber] const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
interface ValidationsObj {
[key: string]: RuleObject[]
}
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => { (meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUi } = useProject() const { sqlUis, isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
@ -32,14 +36,22 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { $e } = useNuxtApp() 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 idType = null
const additionalValidations = ref<Record<string, any>>({}) const additionalValidations = ref<ValidationsObj>({})
const setAdditionalValidations = (validations: Record<string, any>) => { const setAdditionalValidations = (validations: ValidationsObj) => {
additionalValidations.value = validations additionalValidations.value = { ...additionalValidations.value, ...validations }
} }
const formState = ref<Record<string, any>>({ const formState = ref<Record<string, any>>({
@ -267,6 +279,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isEdit, isEdit,
column, column,
sqlUi, sqlUi,
isMssql,
isPg,
isMysql,
} }
}, },
) )

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

Loading…
Cancel
Save