Browse Source

Merge branch 'develop' into NCDBOSS-88

pull/6472/head
Raju Udava 11 months ago
parent
commit
0d0eaf6556
  1. 29
      .github/workflows/ci-cd.yml
  2. 36
      .github/workflows/playwright-test-workflow.yml
  3. 28
      .github/workflows/pre-build-for-playwright.yml
  4. 32
      .github/workflows/publish-api-docs.yml
  5. 6
      .github/workflows/release-docker.yml
  6. 4
      .github/workflows/release-npm.yml
  7. 24
      README.md
  8. 4
      SECURITY.md
  9. 4
      charts/nocodb/templates/deployment.yaml
  10. 2
      charts/nocodb/templates/pvc.yaml
  11. 4
      charts/nocodb/values.yaml
  12. BIN
      docker-compose/sqlite/nocodb/noco.db
  13. BIN
      packages/nc-gui/assets/img/brand/nocodb.png
  14. 196
      packages/nc-gui/assets/img/fieldPlaceholder.svg
  15. 6
      packages/nc-gui/assets/nc-icons/add-data-source.svg
  16. 16
      packages/nc-gui/assets/nc-icons/bt-solid.svg
  17. 3
      packages/nc-gui/assets/nc-icons/comment_here.svg
  18. 9
      packages/nc-gui/assets/nc-icons/commentor.svg
  19. 8
      packages/nc-gui/assets/nc-icons/creator.svg
  20. 2
      packages/nc-gui/assets/nc-icons/download.svg
  21. 9
      packages/nc-gui/assets/nc-icons/editor.svg
  22. 12
      packages/nc-gui/assets/nc-icons/fields.svg
  23. 2
      packages/nc-gui/assets/nc-icons/filter.svg
  24. 6
      packages/nc-gui/assets/nc-icons/group.svg
  25. 16
      packages/nc-gui/assets/nc-icons/hm-solid.svg
  26. 7
      packages/nc-gui/assets/nc-icons/lookup.svg
  27. 10
      packages/nc-gui/assets/nc-icons/mm-solid.svg
  28. 8
      packages/nc-gui/assets/nc-icons/no-access.svg
  29. 8
      packages/nc-gui/assets/nc-icons/owner.svg
  30. 9
      packages/nc-gui/assets/nc-icons/project.svg
  31. 12
      packages/nc-gui/assets/nc-icons/record.svg
  32. 18
      packages/nc-gui/assets/nc-icons/super-admin.svg
  33. 8
      packages/nc-gui/assets/nc-icons/users.svg
  34. 9
      packages/nc-gui/assets/nc-icons/viewer.svg
  35. 15
      packages/nc-gui/assets/style.scss
  36. 13
      packages/nc-gui/components.d.ts
  37. 22
      packages/nc-gui/components/account/License.vue
  38. 30
      packages/nc-gui/components/account/Profile.vue
  39. 28
      packages/nc-gui/components/account/ResetPassword.vue
  40. 15
      packages/nc-gui/components/account/SignupSettings.vue
  41. 173
      packages/nc-gui/components/account/Token.vue
  42. 323
      packages/nc-gui/components/account/UserList.vue
  43. 28
      packages/nc-gui/components/account/UsersModal.vue
  44. 14
      packages/nc-gui/components/api-client/Headers.vue
  45. 10
      packages/nc-gui/components/api-client/Params.vue
  46. 40
      packages/nc-gui/components/cell/Checkbox.vue
  47. 4
      packages/nc-gui/components/cell/Currency.vue
  48. 8
      packages/nc-gui/components/cell/DatePicker.vue
  49. 16
      packages/nc-gui/components/cell/DateTimePicker.vue
  50. 4
      packages/nc-gui/components/cell/Decimal.vue
  51. 11
      packages/nc-gui/components/cell/Duration.vue
  52. 4
      packages/nc-gui/components/cell/Email.vue
  53. 4
      packages/nc-gui/components/cell/Float.vue
  54. 7
      packages/nc-gui/components/cell/Integer.vue
  55. 36
      packages/nc-gui/components/cell/Json.vue
  56. 27
      packages/nc-gui/components/cell/MultiSelect.vue
  57. 4
      packages/nc-gui/components/cell/Percent.vue
  58. 8
      packages/nc-gui/components/cell/PhoneNumber.vue
  59. 23
      packages/nc-gui/components/cell/SingleSelect.vue
  60. 7
      packages/nc-gui/components/cell/Text.vue
  61. 21
      packages/nc-gui/components/cell/TextArea.vue
  62. 14
      packages/nc-gui/components/cell/TimePicker.vue
  63. 4
      packages/nc-gui/components/cell/Url.vue
  64. 8
      packages/nc-gui/components/cell/YearPicker.vue
  65. 2
      packages/nc-gui/components/cell/attachment/Carousel.vue
  66. 2
      packages/nc-gui/components/cell/attachment/Image.vue
  67. 20
      packages/nc-gui/components/cell/attachment/Modal.vue
  68. 2
      packages/nc-gui/components/cell/attachment/RenameFile.vue
  69. 90
      packages/nc-gui/components/cell/attachment/index.vue
  70. 21
      packages/nc-gui/components/cell/attachment/utils.ts
  71. 10
      packages/nc-gui/components/dashboard/Sidebar.vue
  72. 7
      packages/nc-gui/components/dashboard/Sidebar/Header.vue
  73. 9
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  74. 54
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  75. 97
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  76. 41
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  77. 44
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  78. 472
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  79. 14
      packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue
  80. 57
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  81. 89
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  82. 66
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  83. 58
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  84. 97
      packages/nc-gui/components/dashboard/TreeView/index.vue
  85. 23
      packages/nc-gui/components/dashboard/View.vue
  86. 14
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  87. 10
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  88. 16
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  89. 442
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  90. 4
      packages/nc-gui/components/dashboard/settings/Erd.vue
  91. 58
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  92. 30
      packages/nc-gui/components/dashboard/settings/Misc.vue
  93. 28
      packages/nc-gui/components/dashboard/settings/Modal.vue
  94. 51
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  95. 14
      packages/nc-gui/components/dashboard/settings/UIAclTabs.vue
  96. 620
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  97. 99
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  98. 95
      packages/nc-gui/components/dlg/AirtableImport.vue
  99. 25
      packages/nc-gui/components/dlg/ProjectDelete.vue
  100. 100
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  101. Some files were not shown because too many files have changed in this diff Show More

29
.github/workflows/ci-cd.yml

@ -119,11 +119,18 @@ jobs:
shard: 2
playwright-mysql-3:
needs: pre-build-for-playwright
if: ${{ always() && ( github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft )}}
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: mysql
shard: 3
playwright-mysql-4:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: mysql
shard: 4
playwright-sqlite-1:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
@ -144,7 +151,14 @@ jobs:
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: sqlite
shard: 3
shard: 3
playwright-sqlite-4:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: sqlite
shard: 4
playwright-pg-shard-1:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
@ -161,8 +175,15 @@ jobs:
shard: 2
playwright-pg-shard-3:
needs: pre-build-for-playwright
if: ${{ always() && ( github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft )}}
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: pg
shard: 3
playwright-pg-shard-4:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: pg
shard: 3
shard: 4

36
.github/workflows/playwright-test-workflow.yml

@ -17,32 +17,30 @@ jobs:
timeout-minutes: 100
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v3
- name: Check node,pnpm Installation and set Path
shell: bash
working-directory: scripts/self-hosted-gh-runner
timeout-minutes: 1
run: |
./node-pnpm-check.sh
echo "make sure below mentioned versions are expected versions"
echo "If you are expecting the node and pnpm versions to be updated. Please update the node-pnpm-check.sh script"
env
- name: Setup Node
if: ${{ env.SETUP_NODE != 'false' }}
uses: actions/setup-node@v3
with:
node-version: 18.14.0
node-version: ${{ env.NC_REQ_NODE_V }}
- name: Setup pnpm
if: ${{ env.SETUP_PNPM != 'false' }}
uses: pnpm/action-setup@v2
with:
version: 8
version: ${{ env.NC_REQ_PNPM_V }}
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: check if npm cache is needed
shell: bash
run: |
IS_NPM_CACHE_DOWNLOAD_REQUIRED="/cache-marker-v1.txt"
# update the above file name to force the cache ex: /cache-marker-v2.txt.
if [[ ! -f ${PRE_REQ_CHECK_FILE_PATH} ]];
then
echo "IS_NPM_CACHE_DOWNLOAD_REQUIRED is true"
IS_NPM_CACHE_DOWNLOAD_REQUIRED="true"
else
IS_NPM_CACHE_DOWNLOAD_REQUIRED="false"
fi
echo "IS_NPM_CACHE_DOWNLOAD_REQUIRED=${IS_NPM_CACHE_DOWNLOAD_REQUIRED}" >> $GITHUB_ENV
echo "STORE_PATH=/root/setup-pnpm/node_modules/.bin/store/v3" >> $GITHUB_ENV
- uses: actions/cache@v3
if: env.IS_NPM_CACHE_DOWNLOAD_REQUIRED == 'true'
name: Setup pnpm cache
@ -233,6 +231,6 @@ jobs:
cp -r ./tests/playwright/playwright-report ${target_dir}/ || echo "playwright reports directory does not exists" >> ${target_dir}/playwright-report/index.html
cp ./packages/nocodb/*_test_backend.log ${target_dir}/ || echo "backend logs file does not exists" >> ${target_dir}/index.html
# end: artifacts copy
SUMMARY='[Artifacts]('${REPORTS_HOST}/${path}')
[playwright-report]('${REPORTS_HOST}/${path}'/playwright-report)'
SUMMARY='[Artifacts]('${REPORTS_HOST:-http://135.181.48.96}/${path}')
[playwright-report]('${REPORTS_HOST:-http://135.181.48.96}/${path}'/playwright-report)'
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY

28
.github/workflows/pre-build-for-playwright.yml

@ -16,27 +16,25 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Check node,pnpm Installation and set Path
shell: bash
working-directory: scripts/self-hosted-gh-runner
timeout-minutes: 1
run: |
./node-pnpm-check.sh
echo "make sure below mentioned versions are expected versions"
echo "If you are expecting the node and pnpm versions to be updated. Please update the node-pnpm-check.sh script"
env
- name: Setup Node
if: ${{ env.SETUP_NODE != 'false' }}
uses: actions/setup-node@v3
with:
node-version: 18.14.0
node-version: ${{ env.NC_REQ_NODE_V }}
- name: Setup pnpm
if: ${{ env.SETUP_PNPM != 'false' }}
uses: pnpm/action-setup@v2
with:
version: 8
- name: check if npm cache is needed
shell: bash
run: |
PRE_REQ_CHECK_FILE_PATH="/cache-marker-v1.txt"
# update the above file name to force the cache ex: /cache-marker-v2.txt.
if [[ ! -f ${PRE_REQ_CHECK_FILE_PATH} ]];
then
echo "IS_NPM_CACHE_DOWNLOAD_REQUIRED is true"
IS_NPM_CACHE_DOWNLOAD_REQUIRED="true"
else
IS_NPM_CACHE_DOWNLOAD_REQUIRED="false"
fi
echo "IS_NPM_CACHE_DOWNLOAD_REQUIRED=${IS_NPM_CACHE_DOWNLOAD_REQUIRED}" >> $GITHUB_ENV
version: ${{ env.NC_REQ_PNPM_V }}
- name: Get pnpm store directory
shell: bash
run: |

32
.github/workflows/publish-api-docs.yml

@ -18,26 +18,50 @@ jobs:
with:
fetch-depth: 0
- name: Pushes swagger file to src
- name: Pushes swagger file to data-apis-v1
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'src'
destination_folder: 'data-apis-v1'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'
- name: Pushes swagger file to meta-src
- name: Pushes swagger file to data-apis-v2
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'meta-src'
destination_folder: 'data-apis-v2'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'
- name: Pushes swagger file to meta-apis-v1
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'meta-apis-v1'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'
- name: Pushes swagger file to meta-apis-v2
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger-v2.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'meta-apis-v2'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'

6
.github/workflows/release-docker.yml

@ -90,10 +90,7 @@ jobs:
if: ${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}
run: |
export NODE_OPTIONS="--max_old_space_size=16384"
NOCODB_SDK_PKG_NAME=nocodb-sdk
if [[ "${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}" ]]; then
NOCODB_SDK_PKG_NAME=nocodb-sdk-daily
fi
NOCODB_SDK_PKG_NAME=nocodb-sdk-daily
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/bumpNocodbSdkVersion.js &&
pnpm --filter=${NOCODB_SDK_PKG_NAME} install --ignore-scripts --no-frozen-lockfile && pnpm --filter=${NOCODB_SDK_PKG_NAME} run build &&
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} node scripts/upgradeNocodbSdk.js &&
@ -104,6 +101,7 @@ jobs:
- name: Build nocodb and docker files
run: |
pnpm install --ignore-scripts --no-frozen-lockfile
pnpm run docker:build
working-directory: ${{ env.working-directory }}

4
.github/workflows/release-npm.yml

@ -55,9 +55,11 @@ jobs:
- run: |
export NODE_OPTIONS="--max_old_space_size=16384"
NOCODB_SDK_PKG_NAME=nocodb-sdk
if [[ "${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}" ]]; then
# If targetEnv is DEV, then use nocodb-sdk-daily package
if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then
NOCODB_SDK_PKG_NAME=nocodb-sdk-daily
fi
echo $NOCODB_SDK_PKG_NAME
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/bumpNocodbSdkVersion.js &&
pnpm --filter=${NOCODB_SDK_PKG_NAME} install --ignore-scripts --no-frozen-lockfile && pnpm --filter=${NOCODB_SDK_PKG_NAME} run build && pnpm --filter=${NOCODB_SDK_PKG_NAME} publish --no-git-checks &&
sleep 90 &&

24
README.md

@ -14,7 +14,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
</p>
<div align="center">
[![Node version](https://img.shields.io/badge/node-%3E%3D%2018.14.0-brightgreen)](http://nodejs.org/download/)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org)
@ -210,22 +210,22 @@ Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/
# Table of Contents
- [Quick try](#quick-try)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [GUI](#gui)
- [Join Our Community](#join-our-community)
- [Screenshots](#screenshots)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Production Setup](#production-setup)
- [Environment variables](#environment-variables)
- [Environment variables](#environment-variables)
- [Development Setup](#development-setup)
- [Contributing](#contributing)
- [Why are we building this?](#why-are-we-building-this)
@ -305,4 +305,4 @@ Thank you for your contributions! We appreciate all the contributions from the c
<a href="https://github.com/nocodb/nocodb/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nocodb/nocodb" />
</a>
</a>

4
SECURITY.md

@ -3,5 +3,5 @@
### Reporting a Vulnerability
Please report (suspected) security vulnerabilities to security@nocodb.com
- You will receive a response from us within 3 working days.
- If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
- You will receive a response from us within 7 working days.
- If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.

4
charts/nocodb/templates/deployment.yaml

@ -33,9 +33,11 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.storage.enabled }}
volumeMounts:
- name: {{ include "nocodb.fullname" . }}
mountPath: /usr/app/data
{{- end }}
envFrom:
- configMapRef:
name: {{ include "nocodb.fullname" . }}
@ -67,7 +69,9 @@ spec:
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.storage.enabled }}
volumes:
- name: {{ include "nocodb.fullname" . }}
persistentVolumeClaim:
claimName: {{ include "nocodb.fullname" . }}
{{- end }}

2
charts/nocodb/templates/pvc.yaml

@ -1,3 +1,4 @@
{{ if .Values.storage.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@ -12,3 +13,4 @@ spec:
accessModes:
{{- default (toYaml .Values.storage.accessModes) "- ReadWriteMany" | nindent 4 }}
volumeMode: Filesystem
{{ end }}

4
charts/nocodb/values.yaml

@ -86,6 +86,10 @@ extraSecretEnvs:
NC_DB: "mysql2://mysql:3306?u=nocodb&p=secretPass&d=nocodb"
storage:
# If disabled, another persistent storage should be configured for attachments to work.
# We recommend setting NC_S3_BUCKET_NAME and other NC_S3* environment variables.
# Refer documentation for more details.
enabled: true
size: 3Gi
storageClassName: ""

BIN
docker-compose/sqlite/nocodb/noco.db

Binary file not shown.

BIN
packages/nc-gui/assets/img/brand/nocodb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

196
packages/nc-gui/assets/img/fieldPlaceholder.svg

@ -0,0 +1,196 @@
<svg width="166" height="80" viewBox="0 0 166 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_784_33028" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="166" height="80">
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" fill="white"/>
</mask>
<g mask="url(#mask0_784_33028)">
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" fill="white"/>
<g filter="url(#filter0_dd_784_33028)">
<path d="M7.08193 3.68799H4.81572C4.18993 3.68799 3.68262 4.19603 3.68262 4.82274V7.09224C3.68262 7.71895 4.18993 8.227 4.81572 8.227H7.08193C7.70773 8.227 8.21504 7.71895 8.21504 7.09224V4.82274C8.21504 4.19603 7.70773 3.68799 7.08193 3.68799Z" fill="#3366FF"/>
<path d="M7.0819 3.82983H4.81569C4.26811 3.82983 3.82422 4.27437 3.82422 4.82274V7.09225C3.82422 7.64061 4.26811 8.08515 4.81569 8.08515H7.0819C7.62947 8.08515 8.07337 7.64061 8.07337 7.09225V4.82274C8.07337 4.27437 7.62947 3.82983 7.0819 3.82983Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 5.10645L5.52399 6.66673L4.8158 5.95751" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 3.68799H11.6144C10.9886 3.68799 10.4813 4.19603 10.4813 4.82274V7.09224C10.4813 7.71895 10.9886 8.227 11.6144 8.227H13.8806C14.5064 8.227 15.0137 7.71895 15.0137 7.09224V4.82274C15.0137 4.19603 14.5064 3.68799 13.8806 3.68799Z" fill="#E7E7E9"/>
<path d="M50.14 3.89429H18.413C17.7872 3.89429 17.2799 4.40233 17.2799 5.02904V6.88592C17.2799 7.51262 17.7872 8.02067 18.413 8.02067H50.14C50.7658 8.02067 51.2731 7.51262 51.2731 6.88592V5.02904C51.2731 4.40233 50.7658 3.89429 50.14 3.89429Z" fill="#E7E7E9"/>
<path d="M165.717 11.3475H0.283325V11.9149H165.717V11.3475Z" fill="#E7E7E9"/>
<g filter="url(#filter1_dd_784_33028)">
<path d="M7.08193 15.0355H4.81572C4.18993 15.0355 3.68262 15.5436 3.68262 16.1703V18.4398C3.68262 19.0665 4.18993 19.5745 4.81572 19.5745H7.08193C7.70773 19.5745 8.21504 19.0665 8.21504 18.4398V16.1703C8.21504 15.5436 7.70773 15.0355 7.08193 15.0355Z" fill="#3366FF"/>
<path d="M7.0819 15.1774H4.81569C4.26811 15.1774 3.82422 15.6219 3.82422 16.1703V18.4398C3.82422 18.9881 4.26811 19.4327 4.81569 19.4327H7.0819C7.62947 19.4327 8.07337 18.9881 8.07337 18.4398V16.1703C8.07337 15.6219 7.62947 15.1774 7.0819 15.1774Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 16.4539L5.52399 18.0141L4.8158 17.3049" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 15.0355H11.6144C10.9886 15.0355 10.4813 15.5436 10.4813 16.1703V18.4398C10.4813 19.0665 10.9886 19.5745 11.6144 19.5745H13.8806C14.5064 19.5745 15.0137 19.0665 15.0137 18.4398V16.1703C15.0137 15.5436 14.5064 15.0355 13.8806 15.0355Z" fill="#E7E7E9"/>
<path d="M75.6349 15.0355H18.413C17.7872 15.0355 17.2799 15.5436 17.2799 16.1703V18.4398C17.2799 19.0665 17.7872 19.5745 18.413 19.5745H75.6349C76.2607 19.5745 76.768 19.0665 76.768 18.4398V16.1703C76.768 15.5436 76.2607 15.0355 75.6349 15.0355Z" fill="#E7E7E9"/>
<path d="M165.717 22.6951H0.283325V23.2624H165.717V22.6951Z" fill="#E7E7E9"/>
<path d="M165.575 23.1206H0.424927V34.1844H165.575V23.1206Z" fill="#EBF0FF"/>
<g filter="url(#filter2_dd_784_33028)">
<path d="M7.08193 26.3829H4.81572C4.18993 26.3829 3.68262 26.891 3.68262 27.5177V29.7872C3.68262 30.4139 4.18993 30.9219 4.81572 30.9219H7.08193C7.70773 30.9219 8.21504 30.4139 8.21504 29.7872V27.5177C8.21504 26.891 7.70773 26.3829 7.08193 26.3829Z" fill="#3366FF"/>
<path d="M7.0819 26.5248H4.81569C4.26811 26.5248 3.82422 26.9693 3.82422 27.5177V29.7872C3.82422 30.3356 4.26811 30.7801 4.81569 30.7801H7.0819C7.62947 30.7801 8.07337 30.3356 8.07337 29.7872V27.5177C8.07337 26.9693 7.62947 26.5248 7.0819 26.5248Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 27.8014L5.52399 29.3617L4.8158 28.6525" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 26.3829H11.6144C10.9886 26.3829 10.4813 26.891 10.4813 27.5177V29.7872C10.4813 30.4139 10.9886 30.9219 11.6144 30.9219H13.8806C14.5064 30.9219 15.0137 30.4139 15.0137 29.7872V27.5177C15.0137 26.891 14.5064 26.3829 13.8806 26.3829Z" fill="#E7E7E9"/>
<path d="M50.14 26.5894H18.413C17.7872 26.5894 17.2799 27.0974 17.2799 27.7241V29.581C17.2799 30.2077 17.7872 30.7157 18.413 30.7157H50.14C50.7658 30.7157 51.2731 30.2077 51.2731 29.581V27.7241C51.2731 27.0974 50.7658 26.5894 50.14 26.5894Z" fill="#E7E7E9"/>
<path d="M165 23H1V34H165V23Z" stroke="#3366FF"/>
<g filter="url(#filter3_dd_784_33028)">
<path d="M7.08193 37.7305H4.81572C4.18993 37.7305 3.68262 38.2385 3.68262 38.8652V41.1347C3.68262 41.7614 4.18993 42.2695 4.81572 42.2695H7.08193C7.70773 42.2695 8.21504 41.7614 8.21504 41.1347V38.8652C8.21504 38.2385 7.70773 37.7305 7.08193 37.7305Z" fill="#3366FF"/>
<path d="M7.0819 37.8723H4.81569C4.26811 37.8723 3.82422 38.3169 3.82422 38.8652V41.1347C3.82422 41.6831 4.26811 42.1276 4.81569 42.1276H7.0819C7.62947 42.1276 8.07337 41.6831 8.07337 41.1347V38.8652C8.07337 38.3169 7.62947 37.8723 7.0819 37.8723Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 39.1489L5.52399 40.7092L4.8158 40" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 37.7305H11.6144C10.9886 37.7305 10.4813 38.2385 10.4813 38.8652V41.1347C10.4813 41.7614 10.9886 42.2695 11.6144 42.2695H13.8806C14.5064 42.2695 15.0137 41.7614 15.0137 41.1347V38.8652C15.0137 38.2385 14.5064 37.7305 13.8806 37.7305Z" fill="#E7E7E9"/>
<path d="M75.6349 37.7305H18.413C17.7872 37.7305 17.2799 38.2385 17.2799 38.8652V41.1347C17.2799 41.7614 17.7872 42.2695 18.413 42.2695H75.6349C76.2607 42.2695 76.768 41.7614 76.768 41.1347V38.8652C76.768 38.2385 76.2607 37.7305 75.6349 37.7305Z" fill="#E7E7E9"/>
<path d="M165.717 45.3901H0.283325V45.9575H165.717V45.3901Z" fill="#E7E7E9"/>
<g filter="url(#filter4_dd_784_33028)">
<path d="M7.08193 49.078H4.81572C4.18993 49.078 3.68262 49.586 3.68262 50.2128V52.4823C3.68262 53.109 4.18993 53.617 4.81572 53.617H7.08193C7.70773 53.617 8.21504 53.109 8.21504 52.4823V50.2128C8.21504 49.586 7.70773 49.078 7.08193 49.078Z" fill="#3366FF"/>
<path d="M7.0819 49.2198H4.81569C4.26811 49.2198 3.82422 49.6644 3.82422 50.2128V52.4823C3.82422 53.0306 4.26811 53.4752 4.81569 53.4752H7.0819C7.62947 53.4752 8.07337 53.0306 8.07337 52.4823V50.2128C8.07337 49.6644 7.62947 49.2198 7.0819 49.2198Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 50.4965L5.52399 52.0567L4.8158 51.3475" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 49.078H11.6144C10.9886 49.078 10.4813 49.586 10.4813 50.2128V52.4823C10.4813 53.109 10.9886 53.617 11.6144 53.617H13.8806C14.5064 53.617 15.0137 53.109 15.0137 52.4823V50.2128C15.0137 49.586 14.5064 49.078 13.8806 49.078Z" fill="#E7E7E9"/>
<path d="M50.14 49.2843H18.413C17.7872 49.2843 17.2799 49.7923 17.2799 50.4191V52.2759C17.2799 52.9026 17.7872 53.4107 18.413 53.4107H50.14C50.7658 53.4107 51.2731 52.9026 51.2731 52.2759V50.4191C51.2731 49.7923 50.7658 49.2843 50.14 49.2843Z" fill="#E7E7E9"/>
<path d="M165.717 56.7375H0.283325V57.3049H165.717V56.7375Z" fill="#E7E7E9"/>
<g filter="url(#filter5_dd_784_33028)">
<path d="M7.08193 60.4255H4.81572C4.18993 60.4255 3.68262 60.9336 3.68262 61.5603V63.8298C3.68262 64.4565 4.18993 64.9645 4.81572 64.9645H7.08193C7.70773 64.9645 8.21504 64.4565 8.21504 63.8298V61.5603C8.21504 60.9336 7.70773 60.4255 7.08193 60.4255Z" fill="#3366FF"/>
<path d="M7.0819 60.5674H4.81569C4.26811 60.5674 3.82422 61.0119 3.82422 61.5603V63.8298C3.82422 64.3782 4.26811 64.8227 4.81569 64.8227H7.0819C7.62947 64.8227 8.07337 64.3782 8.07337 63.8298V61.5603C8.07337 61.0119 7.62947 60.5674 7.0819 60.5674Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 61.844L5.52399 63.4043L4.8158 62.6951" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 60.4255H11.6144C10.9886 60.4255 10.4813 60.9336 10.4813 61.5603V63.8298C10.4813 64.4565 10.9886 64.9645 11.6144 64.9645H13.8806C14.5064 64.9645 15.0137 64.4565 15.0137 63.8298V61.5603C15.0137 60.9336 14.5064 60.4255 13.8806 60.4255Z" fill="#E7E7E9"/>
<path d="M75.6349 60.4255H18.413C17.7872 60.4255 17.2799 60.9336 17.2799 61.5603V63.8298C17.2799 64.4565 17.7872 64.9645 18.413 64.9645H75.6349C76.2607 64.9645 76.768 64.4565 76.768 63.8298V61.5603C76.768 60.9336 76.2607 60.4255 75.6349 60.4255Z" fill="#E7E7E9"/>
<path d="M165.717 68.0851H0.283325V68.6525H165.717V68.0851Z" fill="#E7E7E9"/>
<g filter="url(#filter6_dd_784_33028)">
<path d="M7.08193 71.7731H4.81572C4.18993 71.7731 3.68262 72.2811 3.68262 72.9078V75.1773C3.68262 75.804 4.18993 76.3121 4.81572 76.3121H7.08193C7.70773 76.3121 8.21504 75.804 8.21504 75.1773V72.9078C8.21504 72.2811 7.70773 71.7731 7.08193 71.7731Z" fill="#3366FF"/>
<path d="M7.0819 71.9149H4.81569C4.26811 71.9149 3.82422 72.3595 3.82422 72.9078V75.1773C3.82422 75.7257 4.26811 76.1702 4.81569 76.1702H7.0819C7.62947 76.1702 8.07337 75.7257 8.07337 75.1773V72.9078C8.07337 72.3595 7.62947 71.9149 7.0819 71.9149Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 73.1915L5.52399 74.7518L4.8158 74.0426" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 71.7731H11.6144C10.9886 71.7731 10.4813 72.2811 10.4813 72.9078V75.1773C10.4813 75.804 10.9886 76.3121 11.6144 76.3121H13.8806C14.5064 76.3121 15.0137 75.804 15.0137 75.1773V72.9078C15.0137 72.2811 14.5064 71.7731 13.8806 71.7731Z" fill="#E7E7E9"/>
<path d="M50.14 71.9792H18.413C17.7872 71.9792 17.2799 72.4873 17.2799 73.114V74.9709C17.2799 75.5976 17.7872 76.1056 18.413 76.1056H50.14C50.7658 76.1056 51.2731 75.5976 51.2731 74.9709V73.114C51.2731 72.4873 50.7658 71.9792 50.14 71.9792Z" fill="#E7E7E9"/>
<path d="M165.717 79.4326H0.283325V80H165.717V79.4326Z" fill="#E7E7E9"/>
</g>
<g filter="url(#filter7_d_784_33028)">
<path d="M115.669 34.6749C115.494 34.4499 115.275 33.9937 114.894 33.4249C114.675 33.1124 114.138 32.5187 113.975 32.2124C113.858 32.0264 113.824 31.7997 113.881 31.5874C113.979 31.1838 114.362 30.9161 114.775 30.9624C115.094 31.0266 115.388 31.1831 115.619 31.4124C115.78 31.5644 115.929 31.7296 116.063 31.9062C116.163 32.0312 116.188 32.0812 116.3 32.2249C116.413 32.3687 116.488 32.5124 116.431 32.2999C116.388 31.9874 116.313 31.4624 116.206 30.9937C116.125 30.6374 116.106 30.5812 116.031 30.3124C115.956 30.0437 115.913 29.8187 115.831 29.5124C115.757 29.2116 115.699 28.907 115.656 28.5999C115.577 28.2073 115.635 27.7995 115.819 27.4437C116.037 27.2383 116.357 27.1841 116.631 27.3062C116.907 27.5095 117.112 27.7935 117.219 28.1187C117.383 28.5189 117.492 28.9394 117.544 29.3687C117.644 29.9937 117.838 30.9062 117.844 31.0937C117.844 30.8624 117.8 30.3749 117.844 30.1562C117.887 29.9282 118.046 29.7389 118.263 29.6562C118.449 29.5991 118.646 29.5862 118.838 29.6187C119.031 29.6592 119.203 29.7707 119.319 29.9312C119.464 30.2958 119.544 30.6828 119.556 31.0749C119.573 30.7316 119.632 30.3916 119.731 30.0624C119.836 29.9153 119.988 29.8092 120.163 29.7624C120.369 29.7247 120.581 29.7247 120.788 29.7624C120.957 29.8191 121.105 29.9259 121.213 30.0687C121.345 30.4002 121.425 30.7502 121.45 31.1062C121.45 31.1937 121.494 30.8624 121.631 30.6437C121.703 30.4316 121.882 30.2737 122.101 30.2295C122.321 30.1853 122.547 30.2616 122.695 30.4295C122.843 30.5974 122.89 30.8316 122.819 31.0437C122.819 31.4499 122.819 31.4312 122.819 31.7062C122.819 31.9812 122.819 32.2249 122.819 32.4562C122.796 32.8219 122.746 33.1854 122.669 33.5437C122.56 33.8606 122.409 34.1613 122.219 34.4374C121.915 34.7749 121.665 35.1563 121.475 35.5687C121.428 35.7736 121.407 35.9836 121.413 36.1937C121.412 36.3879 121.437 36.5812 121.488 36.7687C121.232 36.7957 120.974 36.7957 120.719 36.7687C120.475 36.7312 120.175 36.2437 120.094 36.0937C120.054 36.0132 119.971 35.9622 119.881 35.9622C119.791 35.9622 119.709 36.0132 119.669 36.0937C119.531 36.3312 119.225 36.7624 119.013 36.7874C118.594 36.8374 117.731 36.7874 117.05 36.7874C117.05 36.7874 117.163 36.1624 116.906 35.9374C116.65 35.7124 116.388 35.4499 116.194 35.2749L115.669 34.6749Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M115.669 34.6749C115.494 34.4499 115.275 33.9937 114.894 33.4249C114.675 33.1124 114.138 32.5187 113.975 32.2124C113.858 32.0264 113.824 31.7997 113.881 31.5874C113.979 31.1838 114.362 30.9161 114.775 30.9624C115.094 31.0266 115.388 31.1831 115.619 31.4124C115.78 31.5644 115.929 31.7296 116.063 31.9062C116.163 32.0312 116.188 32.0812 116.3 32.2249C116.413 32.3687 116.488 32.5124 116.431 32.2999C116.388 31.9874 116.313 31.4624 116.206 30.9937C116.125 30.6374 116.106 30.5812 116.031 30.3124C115.956 30.0437 115.913 29.8187 115.831 29.5124C115.757 29.2116 115.699 28.907 115.656 28.5999C115.577 28.2073 115.635 27.7995 115.819 27.4437C116.037 27.2383 116.357 27.1841 116.631 27.3062C116.907 27.5095 117.112 27.7935 117.219 28.1187C117.383 28.5189 117.492 28.9394 117.544 29.3687C117.644 29.9937 117.838 30.9062 117.844 31.0937C117.844 30.8624 117.8 30.3749 117.844 30.1562C117.887 29.9282 118.046 29.7389 118.263 29.6562C118.449 29.5991 118.646 29.5862 118.838 29.6187C119.031 29.6592 119.203 29.7707 119.319 29.9312C119.464 30.2958 119.544 30.6828 119.556 31.0749C119.573 30.7316 119.632 30.3916 119.731 30.0624C119.836 29.9153 119.988 29.8092 120.163 29.7624C120.369 29.7247 120.581 29.7247 120.788 29.7624C120.957 29.8191 121.105 29.9259 121.213 30.0687C121.345 30.4002 121.425 30.7502 121.45 31.1062C121.45 31.1937 121.494 30.8624 121.631 30.6437C121.703 30.4316 121.882 30.2737 122.101 30.2295C122.321 30.1853 122.547 30.2616 122.695 30.4295C122.843 30.5974 122.89 30.8316 122.819 31.0437C122.819 31.4499 122.819 31.4312 122.819 31.7062C122.819 31.9812 122.819 32.2249 122.819 32.4562C122.796 32.8219 122.746 33.1854 122.669 33.5437C122.56 33.8606 122.409 34.1613 122.219 34.4374C121.915 34.7749 121.665 35.1563 121.475 35.5687C121.428 35.7736 121.407 35.9836 121.413 36.1937C121.412 36.3879 121.437 36.5812 121.488 36.7687C121.232 36.7957 120.974 36.7957 120.719 36.7687C120.475 36.7312 120.175 36.2437 120.094 36.0937C120.054 36.0132 119.971 35.9622 119.881 35.9622C119.791 35.9622 119.709 36.0132 119.669 36.0937C119.531 36.3312 119.225 36.7624 119.013 36.7874C118.594 36.8374 117.731 36.7874 117.05 36.7874C117.05 36.7874 117.163 36.1624 116.906 35.9374C116.65 35.7124 116.388 35.4499 116.194 35.2749L115.669 34.6749Z" stroke="black" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M120.969 35.0162V32.8588C120.969 32.7297 120.864 32.625 120.734 32.625C120.605 32.625 120.5 32.7297 120.5 32.8588V35.0162C120.5 35.1453 120.605 35.25 120.734 35.25C120.864 35.25 120.969 35.1453 120.969 35.0162Z" fill="black"/>
<path d="M119.731 35.0154L119.719 32.8569C119.718 32.7281 119.612 32.6243 119.483 32.625C119.354 32.6258 119.249 32.7308 119.25 32.8596L119.263 35.0181C119.263 35.1469 119.369 35.2508 119.498 35.25C119.628 35.2493 119.732 35.1442 119.731 35.0154Z" fill="black"/>
<path d="M118 32.8619L118.013 35.0159C118.013 35.1459 118.119 35.2508 118.248 35.25C118.378 35.2493 118.482 35.1432 118.481 35.0131L118.469 32.8591C118.468 32.7291 118.362 32.6243 118.233 32.625C118.104 32.6258 117.999 32.7318 118 32.8619Z" fill="black"/>
</g>
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" stroke="#E7E7E9"/>
<defs>
<filter id="filter0_dd_784_33028" x="0.324219" y="3.32983" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter1_dd_784_33028" x="0.324219" y="14.6774" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter2_dd_784_33028" x="0.324219" y="26.0248" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter3_dd_784_33028" x="0.324219" y="37.3723" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter4_dd_784_33028" x="0.324219" y="48.7198" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter5_dd_784_33028" x="0.324219" y="60.0674" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter6_dd_784_33028" x="0.324219" y="71.4149" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter7_d_784_33028" x="112.679" y="26.8667" width="11.3476" height="12.118" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="0.4"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_784_33028" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

6
packages/nc-gui/assets/nc-icons/add-data-source.svg

@ -0,0 +1,6 @@
<svg width="100%" height="100%" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5.3335C11.3137 5.3335 14 4.43807 14 3.3335C14 2.22893 11.3137 1.3335 8 1.3335C4.68629 1.3335 2 2.22893 2 3.3335C2 4.43807 4.68629 5.3335 8 5.3335Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 3.3335V12.6668C2 13.7735 5 14.5 7.5 14.5M14 3.3335V7.5M2 8.16683C2 9.2735 5.5 10 8 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.3333 10.3334V14.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.3333 12.3334H10.3333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 828 B

16
packages/nc-gui/assets/nc-icons/bt-solid.svg

@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_27)">
<path d="M13 10C11.8954 10 11 9.10457 11 8C11 6.89543 11.8954 6 13 6C14.1046 6 15 6.89543 15 8C15 9.10457 14.1046 10 13 10Z" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10C1.89543 10 1 9.10457 1 8C1 6.89543 1.89543 6 3 6C4.10457 6 5 6.89543 5 8C5 9.10457 4.10457 10 3 10Z" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 15C5.89543 15 5 14.1046 5 13C5 11.8954 5.89543 11 7 11C8.10457 11 9 11.8954 9 13C9 14.1046 8.10457 15 7 15Z" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 5C5.89543 5 5 4.10457 5 3C5 1.89543 5.89543 1 7 1C8.10457 1 9 1.89543 9 3C9 4.10457 8.10457 5 7 5Z" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 8L11 8" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 4L11 6" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M9 12L11 10" stroke="#36BFFF" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1_27">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

3
packages/nc-gui/assets/nc-icons/comment_here.svg

@ -0,0 +1,3 @@
<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M38 19.5001C38.0069 22.1398 37.3901 24.7438 36.2 27.1001C34.7889 29.9235 32.6195 32.2984 29.9349 33.9586C27.2503 35.6188 24.1565 36.4988 21 36.5001C18.3603 36.5069 15.7562 35.8902 13.4 34.7001L2 38.5001L5.8 27.1001C4.60986 24.7438 3.99312 22.1398 4 19.5001C4.00122 16.3436 4.88122 13.2498 6.54144 10.5652C8.20165 7.88055 10.5765 5.71119 13.4 4.30006C15.7562 3.10992 18.3603 2.49317 21 2.50006H22C26.1687 2.73004 30.1061 4.48958 33.0583 7.44177C36.0105 10.394 37.77 14.3314 38 18.5001V19.5001Z" stroke="#6A7184" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

9
packages/nc-gui/assets/nc-icons/commentor.svg

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_22_1091" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_22_1091)">
<path d="M8 14.2V8.99998C8 8.81109 8.06389 8.65276 8.19167 8.52498C8.31944 8.3972 8.47778 8.33331 8.66667 8.33331H13.3333C13.5222 8.33331 13.6806 8.3972 13.8083 8.52498C13.9361 8.65276 14 8.81109 14 8.99998V12.3333C14 12.5222 13.9361 12.6805 13.8083 12.8083C13.6806 12.9361 13.5222 13 13.3333 13H10L8.56667 14.4333C8.45556 14.5444 8.33333 14.5722 8.2 14.5166C8.06667 14.4611 8 14.3555 8 14.2ZM9.33333 11.6666H12.6667V9.66665H9.33333V11.6666Z" fill="currentColor" stroke="none"/>
<path d="M7.33333 7.33333C6.6 7.33333 5.97222 7.07222 5.45 6.55C4.92778 6.02778 4.66667 5.4 4.66667 4.66667C4.66667 3.93333 4.92778 3.30556 5.45 2.78333C5.97222 2.26111 6.6 2 7.33333 2C8.06667 2 8.69444 2.26111 9.21667 2.78333C9.73889 3.30556 10 3.93333 10 4.66667C10 5.4 9.73889 6.02778 9.21667 6.55C8.69444 7.07222 8.06667 7.33333 7.33333 7.33333ZM7.33333 6C7.7 6 8.01389 5.86944 8.275 5.60833C8.53611 5.34722 8.66667 5.03333 8.66667 4.66667C8.66667 4.3 8.53611 3.98611 8.275 3.725C8.01389 3.46389 7.7 3.33333 7.33333 3.33333C6.96667 3.33333 6.65278 3.46389 6.39167 3.725C6.13056 3.98611 6 4.3 6 4.66667C6 5.03333 6.13056 5.34722 6.39167 5.60833C6.65278 5.86944 6.96667 6 7.33333 6ZM2 11.3333V10.8167C2 10.4389 2.09444 10.0889 2.28333 9.76667C2.47222 9.44445 2.73333 9.2 3.06667 9.03333C3.63333 8.74444 4.27222 8.5 4.98333 8.3C5.69444 8.1 6.13333 8 7 8C7 8.33331 7 8.83331 7 9.33331C6.33333 9.38887 5.52222 9.51944 5 9.69167C4.47778 9.86389 4.03889 10.0444 3.68333 10.2333C3.57222 10.2889 3.48611 10.3694 3.425 10.475C3.36389 10.5806 3.33333 10.6944 3.33333 10.8167V11.3333H6.78333V12.6132L3.33333 12.6667C2.96667 12.6667 2.65278 12.5361 2.39167 12.275C2.13056 12.0139 2 11.7 2 11.3333Z" fill="currentColor" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

8
packages/nc-gui/assets/nc-icons/creator.svg

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_18_1026" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_18_1026)">
<path d="M6.66665 8.00002C5.93331 8.00002 5.30554 7.73891 4.78331 7.21669C4.26109 6.69446 3.99998 6.06669 3.99998 5.33335C3.99998 4.60002 4.26109 3.97224 4.78331 3.45002C5.30554 2.9278 5.93331 2.66669 6.66665 2.66669C7.39998 2.66669 8.02776 2.9278 8.54998 3.45002C9.0722 3.97224 9.33331 4.60002 9.33331 5.33335C9.33331 6.06669 9.0722 6.69446 8.54998 7.21669C8.02776 7.73891 7.39998 8.00002 6.66665 8.00002ZM1.33331 12V11.4667C1.33331 11.1 1.42776 10.7556 1.61665 10.4334C1.80554 10.1111 2.06665 9.86669 2.39998 9.70002C2.96665 9.41113 3.60554 9.16669 4.31665 8.96669C5.02776 8.76669 5.81109 8.66669 6.66665 8.66669H6.89998C6.96665 8.66669 7.03331 8.6778 7.09998 8.70002C7.01109 8.90002 6.93609 9.10835 6.87498 9.32502C6.81387 9.54169 6.76665 9.76669 6.73331 10H6.66665C5.87776 10 5.16942 10.1 4.54165 10.3C3.91387 10.5 3.39998 10.7 2.99998 10.9C2.89998 10.9556 2.81942 11.0334 2.75831 11.1334C2.6972 11.2334 2.66665 11.3445 2.66665 11.4667V12H6.86665C6.93331 12.2334 7.0222 12.4639 7.13331 12.6917C7.24442 12.9195 7.36665 13.1334 7.49998 13.3334H2.66665C2.29998 13.3334 1.98609 13.2028 1.72498 12.9417C1.46387 12.6806 1.33331 12.3667 1.33331 12ZM10.5666 13.4667L10.4666 13C10.3333 12.9445 10.2083 12.8861 10.0916 12.825C9.97498 12.7639 9.85554 12.6889 9.73331 12.6L9.24998 12.75C9.10554 12.7945 8.96387 12.7889 8.82498 12.7334C8.68609 12.6778 8.57776 12.5889 8.49998 12.4667L8.36665 12.2334C8.28887 12.1 8.26109 11.9556 8.28331 11.8C8.30554 11.6445 8.37776 11.5167 8.49998 11.4167L8.86665 11.1C8.84442 10.9445 8.83331 10.8 8.83331 10.6667C8.83331 10.5334 8.84442 10.3889 8.86665 10.2334L8.49998 9.91669C8.37776 9.81669 8.30554 9.69169 8.28331 9.54169C8.26109 9.39169 8.28887 9.25002 8.36665 9.11669L8.51665 8.86669C8.59442 8.74446 8.69998 8.65558 8.83331 8.60002C8.96665 8.54446 9.10554 8.53891 9.24998 8.58335L9.73331 8.73335C9.85554 8.64446 9.97498 8.56946 10.0916 8.50835C10.2083 8.44724 10.3333 8.38891 10.4666 8.33335L10.5666 7.85002C10.6 7.69446 10.675 7.56946 10.7916 7.47502C10.9083 7.38058 11.0444 7.33335 11.2 7.33335H11.4666C11.6222 7.33335 11.7583 7.38335 11.875 7.48335C11.9916 7.58335 12.0666 7.71113 12.1 7.86669L12.2 8.33335C12.3333 8.38891 12.4583 8.45002 12.575 8.51669C12.6916 8.58335 12.8111 8.66669 12.9333 8.76669L13.3833 8.61669C13.5389 8.56113 13.6889 8.56113 13.8333 8.61669C13.9778 8.67224 14.0889 8.76669 14.1666 8.90002L14.3 9.13335C14.3778 9.26669 14.4055 9.41113 14.3833 9.56669C14.3611 9.72224 14.2889 9.85002 14.1666 9.95002L13.8 10.2667C13.8222 10.4 13.8333 10.5389 13.8333 10.6834C13.8333 10.8278 13.8222 10.9667 13.8 11.1L14.1666 11.4167C14.2889 11.5167 14.3611 11.6417 14.3833 11.7917C14.4055 11.9417 14.3778 12.0834 14.3 12.2167L14.15 12.4667C14.0722 12.5889 13.9666 12.6778 13.8333 12.7334C13.7 12.7889 13.5611 12.7945 13.4166 12.75L12.9333 12.6C12.8111 12.6889 12.6916 12.7639 12.575 12.825C12.4583 12.8861 12.3333 12.9445 12.2 13L12.1 13.4834C12.0666 13.6389 11.9916 13.7639 11.875 13.8584C11.7583 13.9528 11.6222 14 11.4666 14H11.2C11.0444 14 10.9083 13.95 10.7916 13.85C10.675 13.75 10.6 13.6222 10.5666 13.4667ZM11.3333 12C11.7 12 12.0139 11.8695 12.275 11.6084C12.5361 11.3472 12.6666 11.0334 12.6666 10.6667C12.6666 10.3 12.5361 9.98613 12.275 9.72502C12.0139 9.46391 11.7 9.33335 11.3333 9.33335C10.9666 9.33335 10.6528 9.46391 10.3916 9.72502C10.1305 9.98613 9.99998 10.3 9.99998 10.6667C9.99998 11.0334 10.1305 11.3472 10.3916 11.6084C10.6528 11.8695 10.9666 12 11.3333 12ZM6.66665 6.66669C7.03331 6.66669 7.3472 6.53613 7.60831 6.27502C7.86942 6.01391 7.99998 5.70002 7.99998 5.33335C7.99998 4.96669 7.86942 4.6528 7.60831 4.39169C7.3472 4.13058 7.03331 4.00002 6.66665 4.00002C6.29998 4.00002 5.98609 4.13058 5.72498 4.39169C5.46387 4.6528 5.33331 4.96669 5.33331 5.33335C5.33331 5.70002 5.46387 6.01391 5.72498 6.27502C5.98609 6.53613 6.29998 6.66669 6.66665 6.66669Z" fill="currentColor" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

2
packages/nc-gui/assets/nc-icons/download.svg

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.66675 6.66663L8.00008 9.99996L11.3334 6.66663" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10V2" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10V2" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

9
packages/nc-gui/assets/nc-icons/editor.svg

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_21_1063" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_21_1063)">
<path d="M7.33333 8.00002C6.6 8.00002 5.97222 7.73891 5.45 7.21669C4.92778 6.69447 4.66667 6.06669 4.66667 5.33335C4.66667 4.60002 4.92778 3.97224 5.45 3.45002C5.97222 2.9278 6.6 2.66669 7.33333 2.66669C8.06667 2.66669 8.69444 2.9278 9.21667 3.45002C9.73889 3.97224 10 4.60002 10 5.33335C10 6.06669 9.73889 6.69447 9.21667 7.21669C8.69444 7.73891 8.06667 8.00002 7.33333 8.00002ZM2 11.4667C2 11.0889 2.09444 10.7417 2.28333 10.425C2.47222 10.1084 2.73333 9.86669 3.06667 9.70002C3.73333 9.36669 4.425 9.11113 5.14167 8.93335C5.85833 8.75558 6.58889 8.66669 7.33333 8.66669C7.72222 8.66669 8.11111 8.70002 8.5 8.76669C8.88889 8.83335 9.27778 8.91113 9.66667 9.00002L8.53333 10.1334C8.33333 10.0778 8.13333 10.0417 7.93333 10.025C7.73333 10.0084 7.53333 10 7.33333 10C6.68889 10 6.05833 10.0778 5.44167 10.2334C4.825 10.3889 4.23333 10.6111 3.66667 10.9C3.55556 10.9556 3.47222 11.0334 3.41667 11.1334C3.36111 11.2334 3.33333 11.3445 3.33333 11.4667V12H7.33333V12.65C7.33333 12.65 7.33333 13 7.33333 13.3334H3.33333C2.96667 13.3334 2.65278 13.2028 2.39167 12.9417C2.13056 12.6806 2 12.3667 2 12V11.4667ZM7.33333 6.66669C7.7 6.66669 8.01389 6.53613 8.275 6.27502C8.53611 6.01391 8.66667 5.70002 8.66667 5.33335C8.66667 4.96669 8.53611 4.6528 8.275 4.39169C8.01389 4.13058 7.7 4.00002 7.33333 4.00002C6.96667 4.00002 6.65278 4.13058 6.39167 4.39169C6.13056 4.6528 6 4.96669 6 5.33335C6 5.70002 6.13056 6.01391 6.39167 6.27502C6.65278 6.53613 6.96667 6.66669 7.33333 6.66669Z" fill="currentColor" stroke="none"/>
<path d="M7.93333 13.27V12.17C7.93333 12.0811 7.95 11.995 7.98333 11.9117C8.01667 11.8283 8.06667 11.7533 8.13333 11.6867L11.6167 8.22001C11.7167 8.12001 11.8278 8.04779 11.95 8.00334C12.0722 7.9589 12.1944 7.93668 12.3167 7.93668C12.45 7.93668 12.5778 7.96168 12.7 8.01168C12.8222 8.06168 12.9333 8.13668 13.0333 8.23668L13.65 8.85334C13.7389 8.95334 13.8083 9.06445 13.8583 9.18668C13.9083 9.3089 13.9333 9.43112 13.9333 9.55334C13.9333 9.67557 13.9111 9.80057 13.8667 9.92834C13.8222 10.0561 13.75 10.17 13.65 10.27L10.1833 13.7367C10.1167 13.8033 10.0417 13.8533 9.95833 13.8867C9.875 13.92 9.78889 13.9367 9.7 13.9367H8.6C8.41111 13.9367 8.25278 13.8728 8.125 13.745C7.99722 13.6172 7.93333 13.4589 7.93333 13.27ZM8.93333 12.9367H9.56667L11.5833 10.9033L11.2833 10.5867L10.9667 10.2867L8.93333 12.3033V12.9367ZM11.2833 10.5867L10.9667 10.2867L11.5833 10.9033L11.2833 10.5867Z" fill="currentColor" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

12
packages/nc-gui/assets/nc-icons/fields.svg

@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.3335 12H14.0002" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12H2.00667" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.3335 8H14.0002" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 8H2.00667" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.3335 4H14.0002" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 4H2.00667" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.3335 12H14.0002" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12H2.00667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.3335 8H14.0002" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 8H2.00667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.3335 4H14.0002" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 4H2.00667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 792 B

After

Width:  |  Height:  |  Size: 822 B

2
packages/nc-gui/assets/nc-icons/filter.svg

@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6668 2H1.3335L6.66683 8.30667V12.6667L9.3335 14V8.30667L14.6668 2Z" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.6668 2H1.3335L6.66683 8.30667V12.6667L9.3335 14V8.30667L14.6668 2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 273 B

After

Width:  |  Height:  |  Size: 278 B

6
packages/nc-gui/assets/nc-icons/group.svg

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 5.33331V14H2V5.33331" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.6665 8H9.33317" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.3332 2H0.666504V5.33333H15.3332V2Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 5.33331V14H2V5.33331" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.6665 8H9.33317" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.3332 2H0.666504V5.33333H15.3332V2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 497 B

16
packages/nc-gui/assets/nc-icons/hm-solid.svg

@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_19)">
<path d="M3 10C4.10457 10 5 9.10457 5 8C5 6.89543 4.10457 6 3 6C1.89543 6 1 6.89543 1 8C1 9.10457 1.89543 10 3 10Z" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 10C14.1046 10 15 9.10457 15 8C15 6.89543 14.1046 6 13 6C11.8954 6 11 6.89543 11 8C11 9.10457 11.8954 10 13 10Z" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 15C10.1046 15 11 14.1046 11 13C11 11.8954 10.1046 11 9 11C7.89543 11 7 11.8954 7 13C7 14.1046 7.89543 15 9 15Z" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5C10.1046 5 11 4.10457 11 3C11 1.89543 10.1046 1 9 1C7.89543 1 7 1.89543 7 3C7 4.10457 7.89543 5 9 5Z" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 8L5 8" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4L5 6" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M7 12L5 10" stroke="#FA8231" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1_19">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

7
packages/nc-gui/assets/nc-icons/lookup.svg

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 10L14 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 6L14 6" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 12.5V13C6 13.5523 6.44772 14 7 14H13C13.5523 14 14 13.5523 14 13V3C14 2.44772 13.5523 2 13 2H7C6.44772 2 6 2.44772 6 3V3.5" stroke="currentColor" stroke-width="1.33" stroke-linecap="round"/>
<path d="M5 11C3.34315 11 2 9.65685 2 8C2 6.34315 3.34315 5 5 5C6.65685 5 8 6.34315 8 8C8 9.65685 6.65685 11 5 11Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.4714 12.4714C1.21105 12.7318 0.788945 12.7318 0.528595 12.4714C0.268246 12.2111 0.268246 11.7889 0.528595 11.5286L1.4714 12.4714ZM2.5286 9.5286L3 9.05719L3.94281 10L3.4714 10.4714L2.5286 9.5286ZM0.528595 11.5286L2.5286 9.5286L3.4714 10.4714L1.4714 12.4714L0.528595 11.5286Z" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

10
packages/nc-gui/assets/nc-icons/mm-solid.svg

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6C10.8954 6 10 5.10457 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4C14 5.10457 13.1046 6 12 6Z" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14Z" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 14C2.89543 14 2 13.1046 2 12C2 10.8954 2.89543 10 4 10C5.10457 10 6 10.8954 6 12C6 13.1046 5.10457 14 4 14Z" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 6C2.89543 6 2 5.10457 2 4C2 2.89543 2.89543 2 4 2C5.10457 2 6 2.89543 6 4C6 5.10457 5.10457 6 4 6Z" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 10.5L10.5 5.5" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M5.5 5.5L10.5 10.5" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M6 4L10 4" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M6 12L10 12" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

8
packages/nc-gui/assets/nc-icons/no-access.svg

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_18_1051" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_18_1051)">
<path d="M10.1333 7.29998L7.03331 4.19998C7.18887 4.13331 7.3472 4.08331 7.50831 4.04998C7.66942 4.01665 7.83331 3.99998 7.99998 3.99998C8.65553 3.99998 9.20831 4.22498 9.65831 4.67498C10.1083 5.12498 10.3333 5.67776 10.3333 6.33331C10.3333 6.49998 10.3166 6.66387 10.2833 6.82498C10.25 6.98609 10.2 7.14442 10.1333 7.29998ZM3.89998 11.4C4.46665 10.9666 5.09998 10.625 5.79998 10.375C6.49998 10.125 7.23331 9.99998 7.99998 9.99998C8.19998 9.99998 8.39165 10.0083 8.57498 10.025C8.75831 10.0416 8.94998 10.0666 9.14998 10.1L7.68331 8.63331C7.16109 8.56665 6.71387 8.3472 6.34165 7.97498C5.96942 7.60276 5.74998 7.15554 5.68331 6.63331L3.78331 4.73331C3.42776 5.18887 3.15276 5.69165 2.95831 6.24165C2.76387 6.79165 2.66665 7.37776 2.66665 7.99998C2.66665 8.65553 2.77498 9.2722 2.99165 9.84998C3.20831 10.4278 3.51109 10.9444 3.89998 11.4ZM12.2 11.2666C12.5555 10.8111 12.8333 10.3083 13.0333 9.75831C13.2333 9.20831 13.3333 8.6222 13.3333 7.99998C13.3333 6.5222 12.8139 5.26387 11.775 4.22498C10.7361 3.18609 9.47776 2.66665 7.99998 2.66665C7.37776 2.66665 6.79165 2.76665 6.24165 2.96665C5.69165 3.16665 5.18887 3.44442 4.73331 3.79998L12.2 11.2666ZM7.99998 14.6666C7.08887 14.6666 6.22776 14.4916 5.41665 14.1416C4.60554 13.7916 3.8972 13.3139 3.29165 12.7083C2.68609 12.1028 2.20831 11.3944 1.85831 10.5833C1.50831 9.7722 1.33331 8.91109 1.33331 7.99998C1.33331 7.07776 1.50831 6.21387 1.85831 5.40831C2.20831 4.60276 2.68609 3.8972 3.29165 3.29165C3.8972 2.68609 4.60554 2.20831 5.41665 1.85831C6.22776 1.50831 7.08887 1.33331 7.99998 1.33331C8.9222 1.33331 9.78609 1.50831 10.5916 1.85831C11.3972 2.20831 12.1028 2.68609 12.7083 3.29165C13.3139 3.8972 13.7916 4.60276 14.1416 5.40831C14.4916 6.21387 14.6666 7.07776 14.6666 7.99998C14.6666 8.91109 14.4916 9.7722 14.1416 10.5833C13.7916 11.3944 13.3139 12.1028 12.7083 12.7083C12.1028 13.3139 11.3972 13.7916 10.5916 14.1416C9.78609 14.4916 8.9222 14.6666 7.99998 14.6666ZM7.99998 13.3333C8.58887 13.3333 9.14442 13.2472 9.66665 13.075C10.1889 12.9028 10.6666 12.6555 11.1 12.3333C10.6666 12.0111 10.1889 11.7639 9.66665 11.5916C9.14442 11.4194 8.58887 11.3333 7.99998 11.3333C7.41109 11.3333 6.85554 11.4194 6.33331 11.5916C5.81109 11.7639 5.33331 12.0111 4.89998 12.3333C5.33331 12.6555 5.81109 12.9028 6.33331 13.075C6.85554 13.2472 7.41109 13.3333 7.99998 13.3333Z" fill="currentColor" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

8
packages/nc-gui/assets/nc-icons/owner.svg

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_18_1022" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="currentColor"/>
</mask>
<g mask="url(#mask0_18_1022)">
<path d="M7.99999 9.20002C7.33332 9.20002 6.76665 8.96668 6.29999 8.50002C5.83332 8.03335 5.59999 7.46668 5.59999 6.80002C5.59999 6.13335 5.83332 5.56668 6.29999 5.10002C6.76665 4.63335 7.33332 4.40002 7.99999 4.40002C8.66665 4.40002 9.23332 4.63335 9.69999 5.10002C10.1667 5.56668 10.4 6.13335 10.4 6.80002C10.4 7.46668 10.1667 8.03335 9.69999 8.50002C9.23332 8.96668 8.66665 9.20002 7.99999 9.20002ZM7.99999 8.00002C8.33332 8.00002 8.61665 7.88335 8.84999 7.65002C9.08332 7.41668 9.19999 7.13335 9.19999 6.80002C9.19999 6.46668 9.08332 6.18335 8.84999 5.95002C8.61665 5.71668 8.33332 5.60002 7.99999 5.60002C7.66665 5.60002 7.38332 5.71668 7.14999 5.95002C6.91665 6.18335 6.79999 6.46668 6.79999 6.80002C6.79999 7.13335 6.91665 7.41668 7.14999 7.65002C7.38332 7.88335 7.66665 8.00002 7.99999 8.00002ZM7.99999 2.88335L3.99999 4.41668V7.41668C3.99999 8.00557 4.08332 8.57779 4.24999 9.13335C4.41665 9.6889 4.64443 10.2056 4.93332 10.6833C5.4111 10.4611 5.90832 10.2917 6.42499 10.175C6.94165 10.0583 7.46665 10 7.99999 10C8.53332 10 9.05832 10.0583 9.57499 10.175C10.0917 10.2917 10.5889 10.4611 11.0667 10.6833C11.3555 10.2056 11.5833 9.6889 11.75 9.13335C11.9167 8.57779 12 8.00557 12 7.41668V4.41668L7.99999 2.88335ZM7.99999 11.2C7.59999 11.2 7.20554 11.2389 6.81665 11.3167C6.42777 11.3945 6.04999 11.5111 5.68332 11.6667C6.00554 12.0111 6.3611 12.3111 6.74999 12.5667C7.13888 12.8222 7.55554 13.0167 7.99999 13.15C8.44443 13.0167 8.8611 12.8222 9.24999 12.5667C9.63888 12.3111 9.99443 12.0111 10.3167 11.6667C9.94999 11.5111 9.57221 11.3945 9.18332 11.3167C8.79443 11.2389 8.39999 11.2 7.99999 11.2ZM7.99999 14.3333C7.93332 14.3333 7.86665 14.3306 7.79999 14.325C7.73332 14.3195 7.67221 14.3056 7.61665 14.2833C6.1611 13.8056 4.99443 12.9222 4.11665 11.6333C3.23888 10.3445 2.79999 8.93891 2.79999 7.41668V4.41668C2.79999 4.16113 2.86943 3.93335 3.00832 3.73335C3.14721 3.53335 3.33332 3.38891 3.56665 3.30002L7.56665 1.76668C7.7111 1.71113 7.85554 1.68335 7.99999 1.68335C8.14443 1.68335 8.28888 1.71113 8.43332 1.76668L12.4333 3.30002C12.6667 3.38891 12.8528 3.53335 12.9917 3.73335C13.1305 3.93335 13.2 4.16113 13.2 4.41668V7.41668C13.2 8.93891 12.7611 10.3445 11.8833 11.6333C11.0055 12.9222 9.83888 13.8056 8.38332 14.2833C8.32777 14.3056 8.26665 14.3195 8.19999 14.325C8.13332 14.3306 8.06665 14.3333 7.99999 14.3333Z" fill="currentColor" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

9
packages/nc-gui/assets/nc-icons/project.svg

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 1073 1073" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1749_80944" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="94" y="40" width="885" height="993">
<path d="M978.723 40H94V1033H978.723V40Z" fill="white"/>
</mask>
<g mask="url(#mask0_1749_80944)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M638.951 291.265L936.342 462.949C966.129 480.145 980.256 502.958 978.723 525.482V774.266C980.256 796.789 966.129 819.602 936.342 836.798L638.951 1008.48C582.292 1041.19 490.431 1041.19 433.773 1008.48L136.381 836.798C106.595 819.602 92.4675 796.789 93.9999 774.266L93.9999 525.482C92.4675 502.957 106.595 480.145 136.381 462.949L433.773 291.265C490.431 258.556 582.292 258.556 638.951 291.265Z" fill="#142966"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M638.951 65.0055L936.342 236.69C966.129 253.886 980.256 276.699 978.723 299.222V548.006C980.256 570.529 966.129 593.343 936.342 610.538L638.951 782.223C582.292 814.931 490.431 814.931 433.773 782.223L136.381 610.538C106.595 593.343 92.4675 570.529 93.9999 548.006L93.9999 299.222C92.4675 276.699 106.595 253.886 136.381 236.69L433.773 65.0055C490.431 32.2968 582.292 32.2968 638.951 65.0055Z" fill="#36BFFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

12
packages/nc-gui/assets/nc-icons/record.svg

@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1613_80692)">
<path d="M11.8571 5.96903L4.14285 10.4225" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="1.15184" width="9.06208" height="9.06208" rx="1.335" transform="matrix(0.866044 -0.499967 0.866044 0.499967 -0.845705 8.77156)" stroke="#374151" stroke-width="1.33"/>
<path d="M3.5 6.34009L11.2143 10.7935" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1613_80692">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 686 B

18
packages/nc-gui/assets/nc-icons/super-admin.svg

@ -0,0 +1,18 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1882_74955" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1882_74955)">
<path d="M7.33333 7.99996C6.6 7.99996 5.97222 7.73885 5.45 7.21663C4.92778 6.6944 4.66667 6.06663 4.66667 5.33329C4.66667 4.59996 4.92778 3.97218 5.45 3.44996C5.97222 2.92774 6.6 2.66663 7.33333 2.66663C8.06667 2.66663 8.69444 2.92774 9.21667 3.44996C9.73889 3.97218 10 4.59996 10 5.33329C10 6.06663 9.73889 6.6944 9.21667 7.21663C8.69444 7.73885 8.06667 7.99996 7.33333 7.99996ZM2 12V11.4666C2 11.1 2.09444 10.7555 2.28333 10.4333C2.47222 10.1111 2.73333 9.86663 3.06667 9.69996C3.63333 9.41107 4.27222 9.16663 4.98333 8.96663C5.69444 8.76663 6.47778 8.66663 7.33333 8.66663H7.56667C7.63333 8.66663 7.7 8.67774 7.76667 8.69996C7.67778 8.89996 7.60278 9.10829 7.54167 9.32496C7.48056 9.54163 7.43333 9.76663 7.4 9.99996H7.33333C6.54444 9.99996 5.83611 10.1 5.20833 10.3C4.58056 10.5 4.06667 10.7 3.66667 10.9C3.56667 10.9555 3.48611 11.0333 3.425 11.1333C3.36389 11.2333 3.33333 11.3444 3.33333 11.4666V12H7.53333C7.6 12.2333 7.68889 12.4638 7.8 12.6916C7.91111 12.9194 8.03333 13.1333 8.16667 13.3333H3.33333C2.96667 13.3333 2.65278 13.2027 2.39167 12.9416C2.13056 12.6805 2 12.3666 2 12ZM7.33333 6.66663C7.7 6.66663 8.01389 6.53607 8.275 6.27496C8.53611 6.01385 8.66667 5.69996 8.66667 5.33329C8.66667 4.96663 8.53611 4.65274 8.275 4.39163C8.01389 4.13051 7.7 3.99996 7.33333 3.99996C6.96667 3.99996 6.65278 4.13051 6.39167 4.39163C6.13056 4.65274 6 4.96663 6 5.33329C6 5.69996 6.13056 6.01385 6.39167 6.27496C6.65278 6.53607 6.96667 6.66663 7.33333 6.66663Z" fill="currentColor" stroke="none"/>
<path d="M11 14C10.9117 14 10.826 13.9815 10.7429 13.9444C10.6597 13.9074 10.587 13.8543 10.5247 13.7852L8.14026 11.0741C8.09351 11.0198 8.05844 10.9605 8.03506 10.8963C8.01169 10.8321 8 10.7654 8 10.6963C8 10.6519 8.0039 10.6062 8.01169 10.5593C8.01948 10.5123 8.03636 10.4691 8.06234 10.4296L8.64675 9.32593C8.7039 9.22716 8.78052 9.14815 8.87662 9.08889C8.97273 9.02963 9.08052 9 9.2 9H12.8C12.9195 9 13.0273 9.02963 13.1234 9.08889C13.2195 9.14815 13.2961 9.22716 13.3532 9.32593L13.9377 10.4296C13.9636 10.4691 13.9805 10.5123 13.9883 10.5593C13.9961 10.6062 14 10.6519 14 10.6963C14 10.7654 13.9883 10.8321 13.9649 10.8963C13.9416 10.9605 13.9065 11.0198 13.8597 11.0741L11.4753 13.7852C11.413 13.8543 11.3403 13.9074 11.2571 13.9444C11.174 13.9815 11.0883 14 11 14ZM10.2597 10.4815H11.7403L11.2727 9.59259H10.7273L10.2597 10.4815ZM10.6883 13.0519V11.0741H8.95844L10.6883 13.0519ZM11.3117 13.0519L13.0416 11.0741H11.3117V13.0519ZM12.4338 10.4815H13.2597L12.7922 9.59259H11.9662L12.4338 10.4815ZM8.74026 10.4815H9.56623L10.0338 9.59259H9.20779L8.74026 10.4815Z" fill="currentColor" stroke="currentColor" stroke-width="0.3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

8
packages/nc-gui/assets/nc-icons/users.svg

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.3333 14V12.6667C15.3328 12.0758 15.1362 11.5019 14.7742 11.0349C14.4122 10.5679 13.9053 10.2344 13.3333 10.0867" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3334 14V12.6667C11.3334 11.9594 11.0525 11.2811 10.5524 10.781C10.0523 10.281 9.37399 10 8.66675 10H3.33341C2.62617 10 1.94789 10.281 1.4478 10.781C0.9477 11.2811 0.666748 11.9594 0.666748 12.6667V14" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 2.08667C11.2404 2.23354 11.7488 2.56714 12.1118 3.03488C12.4749 3.50262 12.672 4.07789 12.672 4.67C12.672 5.26212 12.4749 5.83739 12.1118 6.30513C11.7488 6.77287 11.2404 7.10647 10.6667 7.25334" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.99992 7.33333C7.47268 7.33333 8.66659 6.13943 8.66659 4.66667C8.66659 3.19391 7.47268 2 5.99992 2C4.52716 2 3.33325 3.19391 3.33325 4.66667C3.33325 6.13943 4.52716 7.33333 5.99992 7.33333Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.3333 14V12.6667C15.3328 12.0758 15.1362 11.5019 14.7742 11.0349C14.4122 10.5679 13.9053 10.2344 13.3333 10.0867" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3334 14V12.6667C11.3334 11.9594 11.0525 11.2811 10.5524 10.781C10.0523 10.281 9.37399 10 8.66675 10H3.33341C2.62617 10 1.94789 10.281 1.4478 10.781C0.9477 11.2811 0.666748 11.9594 0.666748 12.6667V14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 2.08667C11.2404 2.23354 11.7488 2.56714 12.1118 3.03488C12.4749 3.50262 12.672 4.07789 12.672 4.67C12.672 5.26212 12.4749 5.83739 12.1118 6.30513C11.7488 6.77287 11.2404 7.10647 10.6667 7.25334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.99992 7.33333C7.47268 7.33333 8.66659 6.13943 8.66659 4.66667C8.66659 3.19391 7.47268 2 5.99992 2C4.52716 2 3.33325 3.19391 3.33325 4.66667C3.33325 6.13943 4.52716 7.33333 5.99992 7.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

9
packages/nc-gui/assets/nc-icons/viewer.svg

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_18_1042" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_18_1042)">
<path d="M6.66665 8.00002C5.93331 8.00002 5.30554 7.73891 4.78331 7.21669C4.26109 6.69447 3.99998 6.06669 3.99998 5.33335C3.99998 4.60002 4.26109 3.97224 4.78331 3.45002C5.30554 2.9278 5.93331 2.66669 6.66665 2.66669C7.39998 2.66669 8.02776 2.9278 8.54998 3.45002C9.0722 3.97224 9.33331 4.60002 9.33331 5.33335C9.33331 6.06669 9.0722 6.69447 8.54998 7.21669C8.02776 7.73891 7.39998 8.00002 6.66665 8.00002ZM1.33331 12V11.4667C1.33331 11.1 1.42776 10.7556 1.61665 10.4334C1.80554 10.1111 2.06665 9.86669 2.39998 9.70002C2.96665 9.41113 3.60554 9.16669 4.31665 8.96669C4.71274 8.85529 4.93609 8.67502 5.79165 8.67502H6.02498C6.09165 8.67502 6.15831 8.68613 6.22498 8.70835C5.99998 9.00002 5.33331 9.66669 4.99998 10C4.99998 10 4.8376 10.2057 4.54165 10.3C3.91387 10.5 3.39998 10.7 2.99998 10.9C2.89998 10.9556 2.81942 11.0334 2.75831 11.1334C2.6972 11.2334 2.66665 11.3445 2.66665 11.4667V12H4.99998C5.33331 12.3334 5.33331 12.3334 5.66665 12.6917C5.90596 12.949 6.19998 13.1334 6.33331 13.3334H2.66665C2.29998 13.3334 1.98609 13.2028 1.72498 12.9417C1.46387 12.6806 1.33331 12.3667 1.33331 12ZM6.66665 6.66669C7.03331 6.66669 7.3472 6.53613 7.60831 6.27502C7.86942 6.01391 7.99998 5.70002 7.99998 5.33335C7.99998 4.96669 7.86942 4.6528 7.60831 4.39169C7.3472 4.13058 7.03331 4.00002 6.66665 4.00002C6.29998 4.00002 5.98609 4.13058 5.72498 4.39169C5.46387 4.6528 5.33331 4.96669 5.33331 5.33335C5.33331 5.70002 5.46387 6.01391 5.72498 6.27502C5.98609 6.53613 6.29998 6.66669 6.66665 6.66669Z" fill="currentColor" stroke="none"/>
<path d="M9.99998 12.6667C10.6555 12.6667 11.2639 12.5167 11.825 12.2167C12.3861 11.9167 12.8333 11.5111 13.1666 11C12.8333 10.4889 12.3861 10.0833 11.825 9.78333C11.2639 9.48333 10.6555 9.33333 9.99998 9.33333C9.34442 9.33333 8.73609 9.48333 8.17498 9.78333C7.61387 10.0833 7.16665 10.4889 6.83331 11C7.16665 11.5111 7.61387 11.9167 8.17498 12.2167C8.73609 12.5167 9.34442 12.6667 9.99998 12.6667ZM9.99998 14C8.93331 14 7.98054 13.7194 7.14165 13.1583C6.30276 12.5972 5.69998 11.8778 5.33331 11C5.69998 10.1222 6.30276 9.40278 7.14165 8.84167C7.98054 8.28056 8.93331 8 9.99998 8C11.0666 8 12.0194 8.28056 12.8583 8.84167C13.6972 9.40278 14.3 10.1222 14.6666 11C14.3 11.8778 13.6972 12.5972 12.8583 13.1583C12.0194 13.7194 11.0666 14 9.99998 14ZM9.99998 12C9.7222 12 9.48609 11.9028 9.29165 11.7083C9.0972 11.5139 8.99998 11.2778 8.99998 11C8.99998 10.7222 9.0972 10.4861 9.29165 10.2917C9.48609 10.0972 9.7222 10 9.99998 10C10.2778 10 10.5139 10.0972 10.7083 10.2917C10.9028 10.4861 11 10.7222 11 11C11 11.2778 10.9028 11.5139 10.7083 11.7083C10.5139 11.9028 10.2778 12 9.99998 12Z" fill="currentColor" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

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

@ -53,6 +53,14 @@ main {
@apply !rounded-lg !py-2 !px-3 mb-1;
}
.mobile {
.nc-scrollbar-md, nc-scrollbar-dark-md, nc-scrollbar-dark-md, nc-scrollbar-sm-dark, nc-scrollbar-x-md {
&::-webkit-scrollbar {
width: 0px;
}
}
}
.nc-scrollbar-md {
overflow-y: scroll;
overflow-x: hidden;
@ -185,7 +193,7 @@ a {
}
}
.nc-project-menu-item {
.nc-base-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opacity duration-100) hover:(after:(opacity-5));
// &:hover {
@ -434,9 +442,6 @@ a {
@apply !shadow-none rounded ring-1 ring-red-600;
}
.ant-modal {
@apply !top-[30px];
}
.ant-modal-content {
@apply !p-6;
border-radius: 1rem;
@ -598,7 +603,7 @@ input[type='number'] {
}
.nc-sidebar-node {
@apply !xs:(min-h-12 max-h-12 hover:bg-gray-50 ml-1.5);
@apply !xs:(min-h-12 max-h-12 hover:bg-gray-50 ml-1.5 w-[calc(100%-8px)]);
.nc-emoji {
@apply xs:(text-lg);

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

@ -52,7 +52,6 @@ declare module '@vue/runtime-core' {
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row']
@ -81,7 +80,6 @@ declare module '@vue/runtime-core' {
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
Icon: typeof import('~icons/ic/on')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
@ -93,6 +91,7 @@ declare module '@vue/runtime-core' {
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default']
MaterialSymbolsDeleteOutlineRounded: typeof import('~icons/material-symbols/delete-outline-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsKeyboardArrowDownRounded: typeof import('~icons/material-symbols/keyboard-arrow-down-rounded')['default']
MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default']
@ -105,12 +104,9 @@ declare module '@vue/runtime-core' {
MaterialSymbolsVisibility: typeof import('~icons/material-symbols/visibility')['default']
MaterialSymbolsVisibilityOff: typeof import('~icons/material-symbols/visibility-off')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccordionUp: typeof import('~icons/mdi/accordion-up')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircleOutline: typeof import('~icons/mdi/account-circle-outline')['default']
MdiAccountCircleOutlines: typeof import('~icons/mdi/account-circle-outlines')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAppleKeyboardShift: typeof import('~icons/mdi/apple-keyboard-shift')['default']
MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
@ -121,9 +117,7 @@ declare module '@vue/runtime-core' {
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
MdiChevronUp: typeof import('~icons/mdi/chevron-up')['default']
@ -134,6 +128,7 @@ declare module '@vue/runtime-core' {
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiEye: typeof import('~icons/mdi/eye')['default']
MdiFileDocumentMultipleOutline: typeof import('~icons/mdi/file-document-multiple-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
@ -151,12 +146,10 @@ declare module '@vue/runtime-core' {
MdiMessageOutline: typeof import('~icons/mdi/message-outline')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiMoreVert: typeof import('~icons/mdi/more-vert')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
@ -168,9 +161,7 @@ declare module '@vue/runtime-core' {
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
NcIconsInbox: typeof import('~icons/nc-icons/inbox')['default']
PhLink: typeof import('~icons/ph/link')['default']
PhMagnifyingGlassBold: typeof import('~icons/ph/magnifying-glass-bold')['default']
PhTriangleFill: typeof import('~icons/ph/triangle-fill')['default']
RiExternalLinkLine: typeof import('~icons/ri/external-link-line')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

22
packages/nc-gui/components/account/License.vue

@ -5,6 +5,8 @@ import { extractSdkResponseErrorMsg, useApi, useGlobal } from '#imports'
const { api, isLoading } = useApi()
const { t } = useI18n()
const { $e } = useNuxtApp()
const { loadAppInfo } = useGlobal()
@ -22,7 +24,7 @@ const loadLicense = async () => {
const setLicense = async () => {
try {
await api.orgLicense.set({ key: key.value })
message.success('License key updated')
message.success(t('success.licenseKeyUpdated'))
await loadAppInfo()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -35,14 +37,14 @@ loadLicense()
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<!-- <div class="text-xl mt-4 mb-8 text-center font-weight-bold">License</div>-->
<!-- <div class="mx-auto w-150">-->
<!-- <div>-->
<!-- <a-textarea v-model:value="key" placeholder="License key" class="!mt-2 !max-w-[600px]"></a-textarea>-->
<!-- </div>-->
<!-- <div class="text-center">-->
<!-- <a-button class="mt-4 !h-[2.2rem] !rounded-md" @click="setLicense" type="primary">Save license key</a-button>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="text-xl mt-4 mb-8 text-center font-weight-bold">License</div>-->
<!-- <div class="mx-auto w-150">-->
<!-- <div>-->
<!-- <a-textarea v-model:value="key" placeholder="License key" class="!mt-2 !max-w-[600px]"></a-textarea>-->
<!-- </div>-->
<!-- <div class="text-center">-->
<!-- <a-button class="mt-4 !h-[2.2rem] !rounded-md" @click="setLicense" type="primary">Save license key</a-button>-->
<!-- </div>-->
<!-- </div>-->
</div>
</template>

30
packages/nc-gui/components/account/Profile.vue

@ -1,6 +1,8 @@
<script lang="ts" setup>
const { user } = useGlobal()
const { t } = useI18n()
const isErrored = ref(false)
const isTitleUpdating = ref(false)
const form = ref({
@ -13,9 +15,9 @@ const formValidator = ref()
const formRules = {
title: [
{ required: true, message: 'Name required' },
{ min: 2, message: 'Name must be at least 2 characters long' },
{ max: 60, message: 'Name must be at most 60 characters long' },
{ required: true, message: t('msg.error.nameRequired') },
{ min: 2, message: t('msg.error.nameMinLength') },
{ max: 60, message: t('msg.error.nameMaxLength') },
],
}
@ -61,13 +63,13 @@ const onValidate = async (_: any, valid: boolean) => {
<template>
<div class="flex flex-col items-center">
<div class="flex flex-col w-150">
<div class="flex font-medium text-xl">Profile</div>
<div class="flex font-bold text-xl" data-rec="true">{{ $t('labels.profile') }}</div>
<div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2">
<div class="flex font-medium text-base">Account details</div>
<div class="flex text-gray-500">Control your appearance.</div>
<div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div>
<div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div>
<div class="flex flex-row mt-4">
<div class="flex h-20 mt-1.5">
<GeneralUserIcon size="xlarge" />
<GeneralUserIcon size="xlarge" :email="user?.email" />
</div>
<div class="flex w-10"></div>
<a-form
@ -79,24 +81,24 @@ const onValidate = async (_: any, valid: boolean) => {
@finish="onSubmit"
@validate="onValidate"
>
<div class="text-gray-800 mb-1.5">Name</div>
<div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('general.name') }}</div>
<a-form-item name="title" :rules="formRules.title">
<a-input
v-model:value="form.title"
class="w-full !rounded-md !py-1.5"
placeholder="Name"
:placeholder="$t('general.name')"
data-testid="nc-account-settings-rename-input"
/>
</a-form-item>
<div class="text-gray-800 mb-1.5">Account Email ID</div>
<div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('labels.accountEmailID') }}</div>
<a-input
v-model:value="email"
class="w-full !rounded-md !py-1.5"
placeholder="Email"
:placeholder="$t('general.email')"
disabled
data-testid="nc-account-settings-email-input"
/>
<div class="flex flex-row w-full justify-end mt-8">
<div class="flex flex-row w-full justify-end mt-8" data-rec="true">
<NcButton
type="primary"
html-type="submit"
@ -105,8 +107,8 @@ const onValidate = async (_: any, valid: boolean) => {
data-testid="nc-account-settings-save"
@click="onSubmit"
>
<template #loading> Saving </template>
Save
<template #loading> {{ $t('general.saving') }} </template>
{{ $t('general.save') }}
</NcButton>
</div>
</a-form>

28
packages/nc-gui/components/account/ResetPassword.vue

@ -16,19 +16,13 @@ const form = reactive({
})
const formRules = {
currentPassword: [
// Current password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
],
currentPassword: [{ required: true, message: t('msg.error.signUpRules.passwdRequired') }],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
],
passwordRepeat: [
// PasswordRepeat is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
// Passwords match
{
validator: (_: unknown, _v: string) => {
return new Promise((resolve, reject) => {
@ -78,14 +72,19 @@ const resetError = () => {
>
<Transition name="layout">
<div v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center">
<div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center" data-rec="true">
<MaterialSymbolsWarning />
{{ error }}
</div>
</div>
</Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-form-item
:label="$t('placeholder.password.current')"
data-rec="true"
name="currentPassword"
:rules="formRules.currentPassword"
>
<a-input-password
v-model:value="form.currentPassword"
data-testid="nc-user-settings-form__current-password"
@ -96,7 +95,7 @@ const resetError = () => {
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-form-item :label="$t('placeholder.password.new')" data-rec="true" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-testid="nc-user-settings-form__new-password"
@ -107,7 +106,12 @@ const resetError = () => {
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-form-item
:label="$t('placeholder.password.confirm')"
data-rec="true"
name="passwordRepeat"
:rules="formRules.passwordRepeat"
>
<a-input-password
v-model:value="form.passwordRepeat"
data-testid="nc-user-settings-form__new-password-repeat"
@ -126,7 +130,7 @@ const resetError = () => {
type="primary"
html-type="submit"
>
<div class="flex justify-center items-center gap-2">
<div class="flex justify-center items-center gap-2" data-rec="true">
<component :is="iconMap.passwordChange" />
{{ $t('activity.changePwd') }}
</div>

15
packages/nc-gui/components/account/SignupSettings.vue

@ -30,20 +30,21 @@ loadSettings()
</script>
<template>
<div data-testid="nc-app-settings">
<div class="text-xl mt-4 mb-8 text-center font-weight-bold">Settings</div>
<div class="flex justify-center">
<div class="p-7 flex flex-col items-center">
<h1 class="text-2xl mt-4 mb-5 pl-3.5 font-bold">{{ t('activity.settings') }}</h1>
<div class="flex items-center gap-2">
<a-form-item>
<a-checkbox
v-model:checked="settings.invite_only_signup"
v-e="['c:account:enable-signup']"
class="nc-checkbox nc-invite-only-signup-checkbox"
class="nc-checkbox nc-invite-only-signup-checkbox !mt-6"
name="virtual"
@change="saveSettings"
>
{{ $t('labels.inviteOnlySignup') }}
</a-checkbox>
/>
</a-form-item>
<span data-rec="true">
{{ $t('labels.inviteOnlySignup') }}
</span>
</div>
</div>
</template>

173
packages/nc-gui/components/account/Token.vue

@ -2,7 +2,7 @@
import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import type { ApiTokenType, RequestParams } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, ref, useApi, useCopy, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, isEeUI, ref, useApi, useCopy, useNuxtApp } from '#imports'
const { api, isLoading } = useApi()
@ -29,7 +29,7 @@ const showNewTokenModal = ref(false)
const currentLimit = ref(10)
const defaultTokenName = 'Untitled token'
const defaultTokenName = t('labels.untitledToken')
const selectedTokenData = ref<ApiTokenType>({
description: defaultTokenName,
@ -147,9 +147,9 @@ const selectInputOnMount: VNodeRef = (el) =>
const errorMessage = computed(() => {
const tokenLength = selectedTokenData.value.description?.length
if (!tokenLength) {
return 'Token name should not be empty'
return t('msg.info.tokenNameNotEmpty')
} else if (tokenLength > 255) {
return 'Token name should not be more than 255 characters'
return t('msg.info.tokenNameMaxLength')
}
})
@ -160,39 +160,54 @@ const handleCancel = () => {
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<div class="max-w-[810px] mx-auto p-4" data-testid="nc-token-list">
<div class="py-2 flex gap-4 items-center justify-between">
<h6 class="text-2xl my-4 text-left font-bold">API Tokens</h6>
<NcButton
:disabled="showNewTokenModal"
class="!rounded-md"
data-testid="nc-token-create"
size="middle"
type="primary"
@click="showNewTokenModal = true"
>
<span class="hidden md:block">
{{ $t('title.addNewToken') }}
</span>
<span class="flex items-center justify-center md:hidden">
<component :is="iconMap.plus" />
</span>
</NcButton>
<div class="h-full pt-2">
<div class="max-w-202 mx-auto px-4 h-full" data-testid="nc-token-list">
<div class="py-2 flex gap-4 items-baseline justify-between">
<h6 class="text-2xl text-left font-bold" data-rec="true">{{ $t('title.apiTokens') }}</h6>
<NcTooltip :disabled="!(isEeUI && tokens.length)">
<template #title>{{ $t('labels.tokenLimit') }}</template>
<NcButton
:disabled="showNewTokenModal || (isEeUI && tokens.length)"
class="!rounded-md"
data-testid="nc-token-create"
size="middle"
type="primary"
tooltip="bottom"
@click="showNewTokenModal = true"
>
<span class="hidden md:block" data-rec="true">
{{ $t('title.addNewToken') }}
</span>
<span class="flex items-center justify-center md:hidden" data-rec="true">
<component :is="iconMap.plus" />
</span>
</NcButton>
</NcTooltip>
</div>
<span>Create personal API tokens to use in automation or external apps.</span>
<div class="w-[780px] mt-5 border-1 rounded-md h-[530px] overflow-y-scroll">
<div>
<div class="flex w-full pl-5 bg-gray-50 border-b-1">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9">Token name</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start">Creator</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start">Token</span>
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start">Actions</span>
<span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div class="mt-5 h-[calc(100%-13rem)]">
<div class="h-full w-full !overflow-hidden rounded-md">
<div class="flex w-full pl-5 bg-gray-50 border-1 rounded-t-md">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9" data-rec="true">{{ $t('title.tokenName') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
$t('title.creator')
}}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start" data-rec="true">{{
$t('labels.token')
}}</span>
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
$t('labels.actions')
}}</span>
</div>
<main>
<div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-5rem)]">
<div v-if="showNewTokenModal">
<div class="flex gap-5 px-3 py-3.5 text-gray-500 font-medium text-3.5 w-full nc-token-generate">
<div class="flex flex-col w-full">
<div
class="flex gap-5 px-3 py-2.5 text-gray-500 font-medium text-3.5 w-full nc-token-generate border-b-1 border-l-1 border-r-1"
:class="{
'rounded-b-md': !tokens.length,
}"
>
<div class="flex w-full">
<a-input
:ref="selectInputOnMount"
v-model:value="selectedTokenData.description"
@ -203,7 +218,9 @@ const handleCancel = () => {
data-testid="nc-token-input"
@press-enter="generateToken"
/>
<span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1">{{ errorMessage }} </span>
<span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1" data-rec="true"
>{{ errorMessage }}
</span>
</div>
<div class="flex gap-2 justify-start">
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
@ -220,13 +237,20 @@ const handleCancel = () => {
</NcButton>
</div>
</div>
<NcDivider />
</div>
<div v-if="!tokens.length" class="h-118 justify-center flex items-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="`${$t('general.no')} ${$t('labels.token')}`" />
<div
v-if="!tokens.length && !showNewTokenModal"
class="border-l-1 border-r-1 border-b-1 rounded-b-md justify-center flex items-center"
>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noToken')" />
</div>
<div v-for="el of tokens" :key="el.id" data-testid="nc-token-list" class="flex border-b-1 pl-5 py-3 justify-between">
<div
v-for="el of tokens"
:key="el.id"
data-testid="nc-token-list"
class="flex pl-5 py-3 justify-between token items-center border-l-1 border-r-1 border-b-1"
>
<span class="text-black font-bold text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" length="20">
{{ el.description }}
@ -241,39 +265,42 @@ const handleCancel = () => {
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" length="29">
{{ el.token }}
</GeneralTruncateText>
<span v-else>**************************************</span>
<span v-else>************************************</span>
</span>
<!-- ACTIONS -->
<span class="text-gray-500 font-medium text-3.5 w-2/9">
<div class="flex justify-end items-center gap-3 pr-5">
<NcTooltip placement="top">
<template #title>show or hide</template>
<component
:is="iconMap.eye"
class="nc-toggle-token-visibility hover::cursor-pointer"
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top" class="h-4">
<template #title>copy</template>
<component :is="iconMap.copy" class="hover::cursor-pointer" @click="copyToken(el.token)" />
</NcTooltip>
<NcTooltip placement="top" class="mb-0.5">
<template #title>delete</template>
<component
:is="iconMap.delete"
data-testid="nc-token-row-action-icon"
class="nc-delete-icon hover::cursor-pointer"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div>
</span>
<div class="flex justify-end items-center gap-3 pr-5 text-gray-500 font-medium text-3.5 w-2/9">
<NcTooltip placement="top">
<template #title>{{ $t('labels.showOrHide') }}</template>
<component
:is="iconMap.eye"
class="nc-toggle-token-visibility hover::cursor-pointer w-h-4 mb-[1.8px]"
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top" class="h-4">
<template #title>{{ $t('general.copy') }}</template>
<component
:is="iconMap.copy"
class="hover::cursor-pointer w-4 h-4 text-gray-600 mt-0.25"
@click="copyToken(el.token)"
/>
</NcTooltip>
<NcTooltip placement="top" class="mb-0.5">
<template #title>{{ $t('general.delete') }}</template>
<component
:is="iconMap.delete"
data-testid="nc-token-row-action-icon"
class="nc-delete-icon hover::cursor-pointer w-4 h-4"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div>
</div>
</main>
</div>
</div>
</div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-15">
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-5">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
@ -283,7 +310,11 @@ const handleCancel = () => {
</div>
</div>
<GeneralDeleteModal v-model:visible="isModalOpen" entity-name="Token" :on-delete="() => deleteToken(tokenToCopy)">
<GeneralDeleteModal
v-model:visible="isModalOpen"
:entity-name="$t('labels.token')"
:on-delete="() => deleteToken(tokenToCopy)"
>
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
@ -300,3 +331,9 @@ const handleCancel = () => {
</GeneralDeleteModal>
</div>
</template>
<style>
.token:last-child {
@apply border-b-1 rounded-b-md;
}
</style>

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

@ -1,11 +1,14 @@
<script lang="ts" setup>
import { OrgUserRoles } from 'nocodb-sdk'
import type { OrgUserReqType, RequestParams, Roles, UserType } from 'nocodb-sdk'
import type { OrgUserReqType, RequestParams, UserType } from 'nocodb-sdk'
import type { User } from '#imports'
import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useDebounceFn, useNuxtApp } from '#imports'
const { api, isLoading } = useApi()
// for loading screen
isLoading.value = true
const { $e } = useNuxtApp()
const { t } = useI18n()
@ -34,7 +37,7 @@ const pagination = reactive({
position: ['bottomCenter'],
})
const loadUsers = async (page = currentPage.value, limit = currentLimit.value) => {
const loadUsers = useDebounceFn(async (page = currentPage.value, limit = currentLimit.value) => {
currentPage.value = page
try {
const response: any = await api.orgUsers.list({
@ -55,11 +58,13 @@ const loadUsers = async (page = currentPage.value, limit = currentLimit.value) =
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
}, 500)
loadUsers()
onMounted(() => {
loadUsers()
})
const updateRole = async (userId: string, roles: Roles) => {
const updateRole = async (userId: string, roles: string) => {
try {
await api.orgUsers.update(userId, {
roles,
@ -72,20 +77,30 @@ const updateRole = async (userId: string, roles: Roles) => {
}
}
const deleteUser = async (userId: string) => {
const deleteModalInfo = ref<UserType | null>(null)
const deleteUser = async () => {
try {
await api.orgUsers.delete(userId)
await api.orgUsers.delete(deleteModalInfo.value?.id as string)
message.success(t('msg.success.userDeleted'))
await loadUsers()
if (!users.value.length && currentPage.value !== 1) {
currentPage.value--
loadUsers(currentPage.value)
}
$e('a:org-user:user-deleted')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
// closing the modal
isOpen.value = false
deleteModalInfo.value = null
}
// closing the modal
isOpen.value = false
}
const resendInvite = async (user: User) => {
const resendInvite = async (user: UserType) => {
try {
await api.orgUsers.resendInvite(user.id)
@ -112,7 +127,7 @@ const copyInviteUrl = async (user: User) => {
$e('c:user:copy-url')
}
const copyPasswordResetUrl = async (user: User) => {
const copyPasswordResetUrl = async (user: UserType) => {
try {
const { reset_password_url } = await api.orgUsers.generatePasswordResetToken(user.id)
@ -125,82 +140,88 @@ const copyPasswordResetUrl = async (user: User) => {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const openInviteModal = () => {
showUserModal.value = true
userMadalKey.value++
}
const openDeleteModal = (user: UserType) => {
deleteModalInfo.value = user
isOpen.value = true
}
</script>
<template>
<div data-testid="nc-super-user-list">
<div class="max-w-[900px] mx-auto">
<div class="text-xl my-4 text-left font-weight-bold">User Management</div>
<div class="py-2 flex gap-4 items-center">
<a-input-search
v-model:value="searchText"
size="middle"
class="max-w-[300px]"
placeholder="Search Users"
@blur="loadUsers"
@keydown.enter="loadUsers"
>
</a-input-search>
<div class="flex-grow"></div>
<component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers" />
<a-button
data-testid="nc-super-user-invite"
size="middle"
class="!rounded-md"
type="primary"
@click="
() => {
showUserModal = true
userMadalKey++
}
"
>
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
Invite new user
</div>
</a-button>
<div data-testid="nc-super-user-list" class="h-full">
<div class="max-w-195 mx-auto h-full">
<div class="text-2xl text-left font-weight-bold mb-4" data-rec="true">{{ $t('title.userManagement') }}</div>
<div class="py-2 flex gap-4 items-center justify-between">
<a-input v-model:value="searchText" class="!max-w-90 !rounded-md" placeholder="Search members" @change="loadUsers()">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
</a-input>
<div class="flex gap-3 items-center justify-center">
<component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" />
<NcButton data-testid="nc-super-user-invite" size="small" type="primary" @click="openInviteModal">
<div class="flex items-center gap-1" data-rec="true">
<component :is="iconMap.plus" />
{{ $t('activity.inviteUser') }}
</div>
</NcButton>
</div>
</div>
<a-table
:row-key="(record) => record.id"
:data-source="users"
:pagination="pagination"
:loading="isLoading"
size="small"
@change="loadUsers($event.current)"
>
<template #emptyText>
<div class="w-full rounded-md max-w-250 h-[calc(100%-12rem)] rounded-md overflow-hidden mt-5">
<div class="flex w-full bg-gray-50 border-1 rounded-t-md">
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6" data-rec="true">
{{ $t('labels.email') }}
</div>
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start" data-rec="true">{{ $t('objects.role') }}</div>
<div class="flex py-3.5 text-gray-500 font-medium text-3.5 w-28 justify-end mr-4" data-rec="true">
{{ $t('labels.action') }}
</div>
</div>
<div v-if="isLoading" class="flex items-center justify-center text-center h-[513px]">
<GeneralLoader size="xlarge" />
</div>
<!-- if users are empty -->
<div v-else-if="!users.length" class="flex items-center justify-center text-center h-full">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!-- Email -->
<a-table-column key="email" :title="$t('labels.email')" data-index="email">
<template #default="{ text }">
<div>
{{ text }}
</div>
<section v-else class="tbody h-[calc(100%-4rem)] nc-scrollbar-md border-t-0 !overflow-auto">
<div
v-for="el of users"
:key="el.id"
data-testid="nc-token-list"
class="user flex py-3 justify-around px-1 border-b-1 border-l-1 border-r-1"
:class="{
'py-4': el.roles?.includes('super'),
}"
>
<div class="text-3.5 text-start w-2/3 pl-5 flex items-center">
<GeneralTruncateText length="29">
{{ el.email }}
</GeneralTruncateText>
</div>
</template>
</a-table-column>
<!-- Role -->
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<template #default="{ record }">
<div>
<div v-if="record.roles.includes('super')" class="font-weight-bold">Super Admin</div>
<a-select
<div class="text-3.5 text-start w-1/3">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold" data-rec="true">
{{ $t('labels.superAdmin') }}
</div>
<NcSelect
v-else
v-model:value="record.roles"
class="w-[220px] nc-user-roles"
v-model:value="el.roles"
class="w-55 nc-user-roles"
:dropdown-match-select-width="false"
@change="updateRole(record.id, record.roles)"
@change="updateRole(el.id, el.roles as string)"
>
<a-select-option
class="nc-users-list-role-option"
:value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)"
>
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</span>
</a-select-option>
@ -210,94 +231,88 @@ const copyPasswordResetUrl = async (user: User) => {
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</span>
</a-select-option>
</a-select>
</div>
</template>
</a-table-column>
<!-- &lt;!&ndash; Projects &ndash;&gt;
<a-table-column key="projectsCount" :title="$t('objects.projects')" data-index="projectsCount">
<template #default="{ text }">
<div>
{{ text }}
</NcSelect>
</div>
</template>
</a-table-column> -->
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div v-if="!record.roles.includes('super')" class="flex items-center gap-2">
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-user-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]">
<component :is="iconMap.threeDotHorizontal" class="nc-user-row-action" />
</div>
</a-button>
</div>
<template #overlay>
<a-menu>
<template v-if="record.invite_token">
<a-menu-item>
<span class="w-26 flex items-center justify-end mr-4">
<div
class="flex items-center gap-2"
:class="{
'opacity-0': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">
<NcButton size="xsmall" type="ghost">
<MdiDotsVertical
class="text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)"
/>
</NcButton>
<template #overlay>
<NcMenu>
<template v-if="!el.roles?.includes('super')">
<!-- Resend invite Email -->
<div class="flex flex-row items-center py-3" @click="resendInvite(record)">
<component :is="iconMap.email" class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.resendInvite') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(record)">
<component :is="iconMap.copy" class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
</template>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyPasswordResetUrl(record)">
<component :is="iconMap.copy" class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyPasswordResetURL') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="isOpen = true">
<!-- Delete user modal -->
<GeneralDeleteModal v-model:visible="isOpen" entity-name="User" :on-delete="() => deleteUser(text)">
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ record.email }}
</div>
</div>
</span>
</template>
</GeneralDeleteModal>
<component :is="iconMap.delete" data-testid="nc-super-user-delete" class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('general.delete') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.resendInvite') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyPasswordResetUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem>
</template>
<NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded />
{{ $t('general.remove') }} {{ $t('objects.user') }}
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</span>
</div>
</section>
</div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-4">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
show-less-items
@change="loadUsers(currentPage, currentLimit)"
/>
</div>
<GeneralDeleteModal v-model:visible="isOpen" entity-name="User" :on-delete="() => deleteUser()">
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon>
<div
class="text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ deleteModalInfo?.email }}
</div>
</div>
<span v-else></span>
</template>
</a-table-column>
</a-table>
</span>
</template>
</GeneralDeleteModal>
<LazyAccountUsersModal :key="userMadalKey" :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</div>
</template>
<style scoped>
.user:last-child {
@apply rounded-b-md;
}
</style>

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

@ -79,8 +79,8 @@ const copyUrl = async () => {
try {
await copy(inviteUrl.value)
// Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
// Copied shareable source url to clipboard!
message.success(t('msg.toast.inviteUrlCopy'))
} catch (e: any) {
message.error(e.message)
}
@ -110,7 +110,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
>
<div class="flex flex-col">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.inviteUser') }}</a-typography-title>
<a-typography-title class="select-none" :level="4" data-rec="true"> {{ $t('activity.inviteUser') }}</a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon>
@ -124,13 +124,13 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-col mt-1 pb-5">
<div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]">
<component :is="iconMap.account" />
<div class="text-xs ml-0.5 mt-0.5">Copy Invite Token</div>
<div class="text-xs ml-0.5 mt-0.5" data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</div>
<a-alert class="!mt-2" type="success" show-icon>
<template #message>
<div class="flex flex-row justify-between items-center py-1">
<div class="flex pl-2 text-green-700 text-xs">
<div class="flex pl-2 text-green-700 text-xs" data-rec="true">
{{ inviteUrl }}
</div>
@ -143,7 +143,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</template>
</a-alert>
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2">
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2" data-rec="true">
{{ $t('msg.info.userInviteNoSMTP') }}
{{ usersData.invitationToken && usersData.emails }}
</div>
@ -153,7 +153,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-row justify-center items-center space-x-0.5">
<MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" />
<div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
<div class="text-xs text-gray-600" data-rec="true">{{ $t('activity.inviteMore') }}</div>
</div>
</a-button>
</div>
@ -177,7 +177,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
name="emails"
:rules="[{ required: true, message: 'Please input email' }]"
>
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('datatype.Email') }}:</div>
<div class="ml-1 mb-1 text-xs text-gray-500" data-rec="true">{{ $t('datatype.Email') }}:</div>
<a-input
:ref="emailInput"
@ -191,7 +191,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-col w-2/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<div class="ml-1 mb-1 text-xs text-gray-500" data-rec="true">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-option
@ -199,8 +199,8 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)"
>
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</span>
</a-select-option>
@ -210,8 +210,8 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</span>
</a-select-option>
@ -224,7 +224,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<a-button type="primary" class="!rounded-md" html-type="submit">
<div class="flex flex-row justify-center items-center space-x-1.5">
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div>
<div data-rec="true">{{ $t('activity.invite') }}</div>
</div>
</a-button>
</div>

14
packages/nc-gui/components/api-client/Headers.vue

@ -66,10 +66,10 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<tr>
<th></th>
<th>
<div class="text-left font-normal ml-2">Header Name</div>
<div class="text-left font-normal ml-2" data-rec="true">{{ $t('labels.headerName') }}</div>
</th>
<th>
<div class="text-left font-normal ml-2">Value</div>
<div class="text-left font-normal ml-2" data-rec="true">{{ $t('placeholder.value') }}</div>
</th>
<th class="w-8"></th>
</tr>
@ -87,9 +87,9 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<a-form-item class="form-item">
<a-auto-complete
v-model:value="headerRow.name"
placeholder="Key"
class="nc-input-hook-header-key"
:options="headerList"
:placeholder="$t('placeholder.key')"
:filter-option="filterOption"
/>
</a-form-item>
@ -97,7 +97,11 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<td class="px-2">
<a-form-item class="form-item">
<a-input v-model:value="headerRow.value" placeholder="Value" class="!rounded-md nc-input-hook-header-value" />
<a-input
v-model:value="headerRow.value"
:placeholder="$t('placeholder.value')"
class="!rounded-md nc-input-hook-header-value"
/>
</a-form-item>
</td>
@ -120,7 +124,7 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<td :colspan="12" class="">
<NcButton size="small" type="secondary" @click="addHeaderRow">
<div class="flex flex-row items-center gap-x-1">
<div>Add Header</div>
<div data-rec="true">{{ $t('labels.addHeader') }}</div>
<component :is="iconMap.plus" class="flex mx-auto" />
</div>
</NcButton>

10
packages/nc-gui/components/api-client/Params.vue

@ -24,11 +24,11 @@ const deleteParamRow = (i: number) => {
<thead class="h-8">
<tr>
<th>
<div class="text-left font-normal ml-2">Parameter Name</div>
<div class="text-left font-normal ml-2" data-rec="true">{{ $t('title.parameterName') }}</div>
</th>
<th>
<div class="text-left font-normal ml-2">Value</div>
<div class="text-left font-normal ml-2" data-rec="true">{{ $t('placeholder.value') }}</div>
</th>
<th class="w-8">
@ -41,13 +41,13 @@ const deleteParamRow = (i: number) => {
<tr v-for="(paramRow, idx) in vModel" :key="idx" class="!h-2 overflow-hidden">
<td class="px-2">
<a-form-item class="form-item">
<a-input v-model:value="paramRow.name" placeholder="Key" class="!rounded-lg" />
<a-input v-model:value="paramRow.name" :placeholder="$t('placeholder.key')" class="!rounded-lg" />
</a-form-item>
</td>
<td class="px-2">
<a-form-item class="form-item">
<a-input v-model:value="paramRow.value" placeholder="Value" class="!rounded-lg" />
<a-input v-model:value="paramRow.value" :placeholder="$t('placeholder.value')" class="!rounded-lg" />
</a-form-item>
</td>
@ -69,7 +69,7 @@ const deleteParamRow = (i: number) => {
<td :colspan="12" class="">
<NcButton size="small" type="secondary" @click="addParamRow">
<div class="flex flex-row items-center gap-x-1">
<div>Add Parameter</div>
<div data-rec="true">{{ $t('activity.addParameter') }}</div>
<component :is="iconMap.plus" class="flex mx-auto" />
</div>
</NcButton>

40
packages/nc-gui/components/cell/Checkbox.vue

@ -8,7 +8,7 @@ import {
getMdiIcon,
inject,
parseProp,
useProject,
useBase,
useSelectedCellKeyupListener,
} from '#imports'
@ -28,7 +28,7 @@ const emits = defineEmits<Emits>()
const active = inject(ActiveCellInj, ref(false))
const { isMssql } = useProject()
const { isMssql } = useBase()
const column = inject(ColumnInj)
@ -40,6 +40,8 @@ const isGallery = inject(IsGalleryInj, ref(false))
const readOnly = inject(ReadonlyInj)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const checkboxMeta = computed(() => {
return {
icon: {
@ -53,7 +55,7 @@ const checkboxMeta = computed(() => {
const vModel = computed<boolean | number>({
get: () => !!props.modelValue && props.modelValue !== '0' && props.modelValue !== 0 && props.modelValue !== 'false',
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.base_id) ? +val : val),
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.source_id) ? +val : val),
})
function onClick(force?: boolean, event?: MouseEvent) {
@ -80,27 +82,29 @@ useSelectedCellKeyupListener(active, (e) => {
<template>
<div
class="flex cursor-pointer w-full h-full"
class="flex cursor-pointer w-full h-full items-center"
:class="{
'justify-center': !isForm || !isGallery,
'w-full flex-start': isForm || isGallery,
'w-full flex-start pl-2': isForm || isGallery || isExpandedFormOpen,
'w-full justify-center': !isForm && !isGallery && !isExpandedFormOpen,
'nc-cell-hover-show': !vModel && !readOnly,
'opacity-0': readOnly && !vModel,
}"
@click="onClick(false, $event)"
>
<div class="items-center" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery }" @click="onClick(true)">
<div :class="{ 'bg-gray-100 rounded-full ': !vModel }">
<Transition name="layout" mode="out-in" :duration="100">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
class="nc-checkbox"
:style="{
color: checkboxMeta.color,
}"
/>
</Transition>
</div>
<div
class="items-center"
:class="{ 'w-full justify-start': isEditColumnMenu || isGallery || isForm, 'py-2': isEditColumnMenu }"
@click="onClick(true)"
>
<Transition name="layout" mode="out-in" :duration="100">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
class="nc-checkbox"
:style="{
color: checkboxMeta.color,
}"
/>
</Transition>
</div>
</div>
</template>

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

@ -79,7 +79,7 @@ onMounted(() => {
v-model="vModel"
type="number"
class="w-full h-full text-sm border-none rounded-md outline-none"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="submitCurrency"
@keydown.down.stop
@keydown.left.stop
@ -93,7 +93,7 @@ onMounted(() => {
@contextmenu.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<!-- only show the numeric value as previously string value was accepted -->
<span v-else-if="!isNaN(vModel)">{{ currency }}</span>

8
packages/nc-gui/components/cell/DatePicker.vue

@ -25,6 +25,8 @@ const { modelValue, isPk } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const { showNull } = useGlobal()
const columnMeta = inject(ColumnInj, null)!
@ -84,11 +86,11 @@ watch(
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return 'NULL'
return t('general.null')
} else if (isDateInvalid.value) {
return 'Invalid date'
return t('msg.invalidDate')
} else {
return ''
}

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

@ -13,7 +13,7 @@ import {
parseProp,
ref,
timeFormats,
useProject,
useBase,
useSelectedCellKeyupListener,
watch,
} from '#imports'
@ -28,7 +28,7 @@ const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMssql, isXcdbBase } = useProject()
const { isMssql, isXcdbBase } = useBase()
const { showNull } = useGlobal()
@ -40,6 +40,8 @@ const editable = inject(EditModeInj, ref(false))
const isLockedMode = inject(IsLockedInj, ref(false))
const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false))
const column = inject(ColumnInj)!
@ -65,7 +67,7 @@ const localState = computed({
return undefined
}
const isXcDB = isXcdbBase(column.value.base_id)
const isXcDB = isXcdbBase(column.value.source_id)
// cater copy and paste
// when copying a datetime cell, the copied value would be local time
@ -81,7 +83,7 @@ const localState = computed({
return /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)
}
if (isMssql(column.value.base_id)) {
if (isMssql(column.value.source_id)) {
// e.g. 2023-04-29T11:41:53.000Z
return dayjs(modelValue)
}
@ -137,11 +139,11 @@ watch(
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return 'NULL'
return t('general.null')
} else if (isDateInvalid.value) {
return 'Invalid date'
return t('msg.invalidDate')
} else {
return ''
}

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

@ -97,7 +97,7 @@ watch(isExpandedFormOpen, () => {
class="outline-none !py-2 !px-1 border-none rounded-md w-full h-full !text-sm"
type="number"
:step="precision"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
style="letter-spacing: 0.06rem"
@blur="editEnabled = false"
@keydown.down.stop="onKeyDown"
@ -110,7 +110,7 @@ watch(isExpandedFormOpen, () => {
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else class="text-sm">{{ displayValue }}</span>
</template>

11
packages/nc-gui/components/cell/Duration.vue

@ -23,6 +23,8 @@ const { modelValue, showValidationError = true } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const { showNull } = useGlobal()
const column = inject(ColumnInj)
@ -39,7 +41,9 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const durationType = computed(() => parseProp(column?.value?.meta)?.duration || 0)
const durationPlaceholder = computed(() => (isEditColumn.value ? '(Optional)' : durationOptions[durationType.value].title))
const durationPlaceholder = computed(() =>
isEditColumn.value ? `(${t('labels.optional')})` : durationOptions[durationType.value].title,
)
const localState = computed({
get: () => convertMS2Duration(modelValue, durationType.value),
@ -105,13 +109,12 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
@mousedown.stop
/>
<span v-else-if="modelValue === null && showNull" class="nc-null">NULL</span>
<span v-else-if="modelValue === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else> {{ localState }}</span>
<div v-if="showWarningMessage && showValidationError" class="duration-warning">
<!-- TODO: i18n -->
Please enter a number
{{ $t('msg.plsEnterANumber') }}
</div>
</div>
</template>

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

@ -71,7 +71,7 @@ watch(
:ref="focus"
v-model="vModel"
class="w-full outline-none text-sm px-1 py-2"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -84,7 +84,7 @@ watch(
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<nuxt-link
v-else-if="validEmail"

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

@ -51,7 +51,7 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
class="outline-none px-1 border-none w-full h-full text-sm"
type="number"
step="0.1"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -63,7 +63,7 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else class="text-sm">{{ vModel }}</span>
</template>

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

@ -87,8 +87,11 @@ function onKeyDown(e: any) {
v-model="vModel"
class="outline-none py-2 px-1 border-none w-full h-full text-sm"
type="number"
:class="{
'pl-2': isExpandedFormOpen,
}"
style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown="onKeyDown"
@keydown.down.stop
@ -99,7 +102,7 @@ function onKeyDown(e: any) {
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else class="text-sm">{{ displayValue }}</span>
</template>

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

@ -1,10 +1,12 @@
<script setup lang="ts">
import NcModal from '../nc/Modal.vue'
import {
Modal as AModal,
ActiveCellInj,
EditModeInj,
IsFormInj,
JsonExpandInj,
ReadonlyInj,
RowHeightInj,
computed,
inject,
ref,
@ -41,8 +43,12 @@ const localValueState = ref<string | undefined>()
const error = ref<string | undefined>()
const _isExpanded = inject(JsonExpandInj, ref(false))
const isExpanded = ref(false)
const rowHeight = inject(RowHeightInj, ref(undefined))
const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value,
set: (val: undefined | string | Record<string, any>) => {
@ -136,28 +142,42 @@ useSelectedCellKeyupListener(active, (e) => {
break
}
})
const inputWrapperRef = ref<HTMLElement | null>(null)
onClickOutside(inputWrapperRef, (e) => {
if ((e.target as HTMLElement)?.closest('.nc-json-action')) return
editEnabled.value = false
})
watch(isExpanded, () => {
_isExpanded.value = isExpanded.value
})
</script>
<template>
<component :is="isExpanded ? AModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null">
<component :is="isExpanded ? NcModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null">
<div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
<div class="flex flex-row justify-between pt-1 pb-2" @mousedown.stop>
<div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
<a-button type="text" size="small" @click="isExpanded = !isExpanded">
<CilFullscreenExit v-if="isExpanded" class="h-2.5" />
<CilFullscreen v-else class="h-2.5" />
</a-button>
<div v-if="!isForm || isExpanded" class="flex flex-row">
<a-button type="text" size="small" :onclick="clear"><div class="text-xs">Cancel</div></a-button>
<div v-if="!isForm || isExpanded" class="flex flex-row my-1">
<a-button type="text" size="small" :onclick="clear"
><div class="text-xs">{{ $t('general.cancel') }}</div></a-button
>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel" @click="onSave">
<div class="text-xs">Save</div>
<div class="text-xs">{{ $t('general.save') }}</div>
</a-button>
</div>
</div>
<LazyMonacoEditor
ref="inputWrapperRef"
:model-value="localValue || ''"
class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
@ -171,9 +191,9 @@ useSelectedCellKeyupListener(active, (e) => {
</span>
</div>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else>{{ vModel }}</span>
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</component>
</template>

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

@ -23,9 +23,9 @@ import {
onMounted,
reactive,
ref,
useBase,
useEventListener,
useMetas,
useProject,
useRoles,
useSelectedCellKeyupListener,
watch,
@ -43,6 +43,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -81,7 +83,7 @@ const { getMeta } = useMetas()
const { isUIAllowed } = useRoles()
const { isPg, isMysql } = useProject()
const { isPg, isMysql } = useBase()
// a variable to keep newly created options value
// temporary until it's add the option to column meta
@ -133,7 +135,7 @@ const vModel = computed({
const selectedTitles = computed(() =>
modelValue
? typeof modelValue === 'string'
? isMysql(column.value.base_id)
? isMysql(column.value.source_id)
? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b)
@ -142,8 +144,8 @@ const selectedTitles = computed(() =>
}
return 0
})
: modelValue.split(',')
: modelValue
: modelValue.split(',').map((el) => el.trim())
: modelValue.map((el) => el.trim())
: [],
)
@ -247,7 +249,7 @@ async function addIfMissingAndSave() {
// todo: refactor and avoid repetition
if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg(column.value.base_id)) {
if (isPg(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`),
@ -255,7 +257,7 @@ async function addIfMissingAndSave() {
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql(column.value.base_id)) {
if (!isMysql(column.value.source_id) && !isPg(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
@ -321,6 +323,8 @@ const handleClose = (e: MouseEvent) => {
!aselect.value.$el.contains(e.target) &&
!document.querySelector('.nc-dropdown-multi-select-cell.active')?.contains(e.target as Node)
) {
// loose focus when clicked outside
isEditable.value = false
isOpen.value = false
}
}
@ -378,10 +382,10 @@ const selectedOpts = computed(() => {
v-model:value="vModel"
mode="multiple"
class="w-full overflow-hidden"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:bordered="false"
clear-icon
show-search
:show-search="!isMobileMode"
:show-arrow="editAllowed && !(readOnly || isLockedMode)"
:open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode"
@ -390,6 +394,9 @@ const selectedOpts = computed(() => {
@search="search"
@keydown.stop
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700 nc-select-expand-btn" />
</template>
<a-select-option
v-for="op of options"
:key="op.id || op.title"
@ -421,7 +428,7 @@ const selectedOpts = computed(() => {
<div class="flex gap-2 text-gray-500 items-center h-full">
<component :is="iconMap.plusThick" class="min-w-4" />
<div class="text-xs whitespace-normal">
Create new option named <strong>{{ searchVal }}</strong>
{{ $t('msg.selectOption.createNewOptionNamed') }} <strong>{{ searchVal }}</strong>
</div>
</div>
</a-select-option>

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

@ -42,7 +42,7 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1"
:class="{ '!px-2': editEnabled }"
type="number"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -54,6 +54,6 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else>{{ vModel }}</span>
</template>

8
packages/nc-gui/components/cell/PhoneNumber.vue

@ -15,6 +15,8 @@ const rowHeight = inject(RowHeightInj, ref(undefined))
const { showNull } = useGlobal()
const { t } = useI18n()
const editEnabled = inject(EditModeInj)!
const isEditColumn = inject(EditColumnInj, ref(false))
@ -46,7 +48,7 @@ watch(
() => editEnabled.value,
() => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !isMobilePhone(localState.value)) {
message.error('Invalid Phone Number')
message.error(t('msg.invalidPhoneNumber'))
localState.value = undefined
return
}
@ -61,7 +63,7 @@ watch(
:ref="focus"
v-model="vModel"
class="w-full outline-none text-sm px-1 py-2"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -74,7 +76,7 @@ watch(
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<a v-else-if="validEmail" class="text-sm underline hover:opacity-75" :href="`tel:${vModel}`" target="_blank">
<LazyCellClampedText :value="vModel" :lines="rowHeight" />

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

@ -20,8 +20,8 @@ import {
inject,
isDrawerOrModalExist,
ref,
useBase,
useEventListener,
useProject,
useRoles,
useSelectedCellKeyupListener,
watch,
@ -37,6 +37,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -71,7 +73,7 @@ const { getMeta } = useMetas()
const { isUIAllowed } = useRoles()
const { isPg, isMysql } = useProject()
const { isPg, isMysql } = useBase()
// a variable to keep newly created option value
// temporary until it's add the option to column meta
@ -102,7 +104,7 @@ const hasEditRoles = computed(() => isUIAllowed('dataEdit'))
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value)
const vModel = computed({
get: () => tempSelectedOptState.value ?? modelValue,
get: () => tempSelectedOptState.value ?? modelValue?.trim(),
set: (val) => {
if (val && isNewOptionCreateEnabled.value && (options.value ?? []).every((op) => op.title !== val)) {
tempSelectedOptState.value = val
@ -175,7 +177,7 @@ async function addIfMissingAndSave() {
// todo: refactor and avoid repetition
if (updatedColMeta.cdf) {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg(column.value.base_id)) {
if (isPg(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.substring(
updatedColMeta.cdf.indexOf(`'`) + 1,
updatedColMeta.cdf.lastIndexOf(`'`),
@ -183,7 +185,7 @@ async function addIfMissingAndSave() {
}
// Mysql escapes single quotes with backslash so we keep quotes but others have to unescaped
if (!isMysql(column.value.base_id)) {
if (!isMysql(column.value.source_id) && !isPg(column.value.source_id)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
@ -202,6 +204,8 @@ async function addIfMissingAndSave() {
}
const search = () => {
if (isMobileMode.value) return
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value
}
@ -217,6 +221,7 @@ const onKeydown = (e: KeyboardEvent) => {
const onSelect = () => {
isOpen.value = false
isEditable.value = false
}
const cellClickHook = inject(CellClickHookInj, null)
@ -284,16 +289,16 @@ const selectedOpt = computed(() => {
v-else
ref="aselect"
v-model:value="vModel"
class="w-full overflow-hidden"
class="w-full overflow-hidden xs:min-h-12"
:class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed"
:bordered="false"
:open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode"
:show-arrow="hasEditRoles && !(readOnly || isLockedMode) && active && vModel === null"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:show-search="isOpen && active"
:show-search="!isMobileMode && isOpen && active"
@select="onSelect"
@keydown="onKeydown($event)"
@search="search"
@ -324,7 +329,7 @@ const selectedOpt = computed(() => {
<div class="flex gap-2 text-gray-500 items-center h-full">
<component :is="iconMap.plusThick" class="min-w-4" />
<div class="text-xs whitespace-normal">
Create new option named <strong>{{ searchVal }}</strong>
{{ $t('msg.selectOption.createNewOptionNamed') }} <strong>{{ searchVal }}</strong>
</div>
</div>
</a-select-option>

7
packages/nc-gui/components/cell/Text.vue

@ -33,7 +33,10 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
:ref="focus"
v-model="vModel"
class="h-full w-full outline-none p-2 bg-transparent"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:class="{
'px-1': isExpandedFormOpen,
}"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -46,7 +49,7 @@ const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</template>

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

@ -28,6 +28,8 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(1 as const))
const isForm = inject(IsFormInj, ref(false))
const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
@ -68,10 +70,11 @@ onClickOutside(inputWrapperRef, (e) => {
<template>
<NcDropdown v-model:visible="isVisible" class="overflow-visible" :trigger="[]" placement="bottomLeft">
<div
class="flex flex-row pt-0.5"
class="flex flex-row pt-0.5 w-full"
:class="{
'min-h-10': rowHeight !== 1,
'min-h-6.5': rowHeight === 1,
'h-full': isForm,
}"
>
<textarea
@ -80,11 +83,15 @@ onClickOutside(inputWrapperRef, (e) => {
v-model="vModel"
rows="4"
class="h-full w-full outline-none border-none"
:class="`${editEnabled ? 'p-2' : ''}`"
:class="{
'p-2': editEnabled,
'py-1 h-full': isForm,
'px-1': isExpandedFormOpen,
}"
:style="{
minHeight: `${height}px`,
}"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
@ -99,21 +106,21 @@ onClickOutside(inputWrapperRef, (e) => {
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<LazyCellClampedText v-else-if="rowHeight" :value="vModel" :lines="rowHeight" class="mr-7" />
<span v-else>{{ vModel }}</span>
<div
v-if="active"
v-if="active && !isExpandedFormOpen"
class="!absolute right-0 bottom-0 h-6 w-5 group cursor-pointer flex justify-end gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
:class="{ 'right-2 bottom-2': editEnabled }"
data-testid="attachment-cell-file-picker-button"
@click.stop="isVisible = !isVisible"
>
<NcTooltip placement="bottom">
<template #title>Expand</template>
<template #title>{{ $t('title.expand') }}</template>
<component
:is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-xs"
@ -135,8 +142,8 @@ onClickOutside(inputWrapperRef, (e) => {
<a-textarea
ref="inputRef"
v-model:value="vModel"
placeholder="Enter text"
class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black"
:placeholder="$t('activity.enterText')"
:bordered="false"
:auto-size="{ minRows: 20, maxRows: 20 }"
:disabled="readOnly"

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

@ -6,7 +6,7 @@ import {
ReadonlyInj,
inject,
onClickOutside,
useProject,
useBase,
useSelectedCellKeyupListener,
watch,
} from '#imports'
@ -20,7 +20,7 @@ const { modelValue, isPk } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const { isMysql } = useBase()
const { showNull } = useGlobal()
@ -36,7 +36,9 @@ const column = inject(ColumnInj)!
const isTimeInvalid = ref(false)
const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const dateFormat = isMysql(column.value.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const { t } = useI18n()
const localState = computed({
get() {
@ -89,11 +91,11 @@ watch(
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return 'NULL'
return t('general.null')
} else if (isTimeInvalid.value) {
return 'Invalid time'
return t('msg.invalidTime')
} else {
return ''
}

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

@ -91,7 +91,7 @@ watch(
v-if="editEnabled"
:ref="focus"
v-model="vModel"
:placeholder="isEditColumn ? 'Enter default URL (Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.enterDefaultUrlOptional') : ''"
class="outline-none text-sm w-full px-2 py-2 bg-transparent h-full"
@blur="editEnabled = false"
@keydown.down.stop
@ -105,7 +105,7 @@ watch(
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null">NULL</span>
<span v-else-if="vModel === null && showNull" class="nc-null uppercase"> $t('general.null')</span>
<nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay"

8
packages/nc-gui/components/cell/YearPicker.vue

@ -33,6 +33,8 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const isYearInvalid = ref(false)
const { t } = useI18n()
const localState = computed({
get() {
if (!modelValue) {
@ -76,11 +78,11 @@ watch(
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return '(Optional)'
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return 'NULL'
return t('general.null')
} else if (isYearInvalid.value) {
return 'Invalid year'
return t('msg.invalidTime')
} else {
return ''
}

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

@ -55,7 +55,7 @@ useEventListener(container, 'click', (e) => {
<template>
<GeneralOverlay v-model="selectedImage" :z-index="1001" class="bg-gray-500 bg-opacity-50">
<template v-if="selectedImage">
<div ref="container" class="overflow-hidden p-12 text-center relative">
<div ref="container" class="overflow-hidden p-12 text-center relative xs:h-screen">
<div class="text-white group absolute top-5 right-5">
<component
:is="iconMap.closeCircle"

2
packages/nc-gui/components/cell/attachment/Image.vue

@ -16,7 +16,7 @@ const onError = () => index.value++
<template>
<LazyNuxtImg
v-if="index < props.srcs.length"
class="m-auto"
class="m-auto object-cover"
:src="props.srcs[index]"
:alt="props?.alt || ''"
placeholder

20
packages/nc-gui/components/cell/attachment/Modal.vue

@ -102,17 +102,19 @@ const handleFileDelete = (i: number) => {
@click="open"
>
<MaterialSymbolsAttachFile class="transform group-hover:(text-accent scale-120)" />
Attach File
{{ $t('activity.attachFile') }}
</div>
<div class="flex items-center gap-2">
<div v-if="readOnly" class="text-gray-400">[Readonly]</div>
Viewing Attachments of
<div v-if="readOnly" class="text-gray-400">[{{ $t('labels.readOnly') }}]</div>
{{ $t('labels.viewingAttachmentsOf') }}
<div class="font-semibold underline">{{ column?.title }}</div>
</div>
<div v-if="selectedVisibleItems.includes(true)" class="flex flex-1 items-center gap-3 justify-end mr-[30px]">
<NcButton type="primary" class="nc-attachment-download-all" @click="bulkDownloadFiles"> Bulk Download </NcButton>
<NcButton type="primary" class="nc-attachment-download-all" @click="bulkDownloadFiles">
{{ $t('activity.bulkDownload') }}
</NcButton>
</div>
</div>
</template>
@ -124,7 +126,7 @@ const handleFileDelete = (i: number) => {
class="text-white ring ring-accent ring-opacity-100 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
>
<MaterialSymbolsFileCopyOutline class="text-accent" height="35" width="35" />
<div class="text-white text-3xl">Drop here</div>
<div class="text-white text-3xl">{{ $t('labels.dropHere') }}</div>
</general-overlay>
</template>
@ -138,7 +140,7 @@ const handleFileDelete = (i: number) => {
/>
<a-tooltip v-if="!readOnly">
<template #title> Remove File </template>
<template #title> {{ $t('title.removeFile') }} </template>
<component
:is="iconMap.closeCircle"
v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic && !isLocked)"
@ -148,7 +150,7 @@ const handleFileDelete = (i: number) => {
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> Download File </template>
<template #title> {{ $t('title.downloadFile') }} </template>
<div class="nc-attachment-download group-hover:(opacity-100)">
<component :is="iconMap.download" @click.stop="downloadFile(item)" />
@ -156,7 +158,7 @@ const handleFileDelete = (i: number) => {
</a-tooltip>
<a-tooltip v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic && !isLocked)" placement="bottom">
<template #title> Rename File </template>
<template #title> {{ $t('title.renameFile') }} </template>
<div class="nc-attachment-download group-hover:(opacity-100) mr-[35px]">
<component :is="iconMap.edit" @click.stop="renameFile(item, i)" />
@ -170,7 +172,7 @@ const handleFileDelete = (i: number) => {
<LazyCellAttachmentImage
v-if="isImage(item.title, item.mimetype)"
:srcs="getPossibleAttachmentSrc(item)"
class="max-w-full max-h-full m-auto justify-center"
class="object-cover h-64 m-auto justify-center"
@click.stop="onClick(item)"
/>

2
packages/nc-gui/components/cell/attachment/RenameFile.vue

@ -48,7 +48,7 @@ onMounted(() => {
<template>
<GeneralModal v-model:visible="visible" class="nc-attachment-rename-modal !w-[30rem]">
<div class="flex flex-col items-center justify-center h-full p-8">
<div class="text-lg font-semibold self-start mb-4">Rename File</div>
<div class="text-lg font-semibold self-start mb-4">{{ $t('title.renameFile') }}</div>
<a-form class="w-full h-full" no-style :model="form" @finish="renameFile(form.title)">
<a-form-item class="w-full" name="title" :rules="rules.title">

90
packages/nc-gui/components/cell/attachment/index.vue

@ -8,6 +8,7 @@ import {
DropZoneRef,
IsExpandedFormOpenInj,
IsGalleryInj,
IsKanbanInj,
RowHeightInj,
iconMap,
inject,
@ -46,11 +47,15 @@ const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()!
const { getPossibleAttachmentSrc, openAttachment } = useAttachment()
const { isMobileMode } = useGlobal()
const { getPossibleAttachmentSrc, openAttachment: _openAttachment } = useAttachment()
const {
isPublic,
@ -61,7 +66,7 @@ const {
visibleItems,
onDrop,
isLoading,
open,
open: _open,
FileIcon,
selectedImage,
isReadonly: _isReadonly,
@ -136,7 +141,7 @@ watch(
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
if (e.key === 'Enter' && !isReadonly.value) {
e.stopPropagation()
if (!modalVisible.value) {
if (!modalVisible.value && !isMobileMode.value) {
modalVisible.value = true
} else {
// click Attach File button
@ -146,6 +151,36 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
})
const rowHeight = inject(RowHeightInj, ref())
const open = (e: Event) => {
e.stopPropagation()
_open()
}
const openAttachment = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) {
isExpandedForm.value = true
return
}
_openAttachment(item)
}
const onExpand = () => {
if (isMobileMode.value) return
modalVisible.value = true
}
const onImageClick = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) return
if (!isMobileMode.value && (isGallery.value || (isKanban.value && !isExpandedForm.value))) return
selectedImage.value = item
}
</script>
<template>
@ -153,9 +188,9 @@ const rowHeight = inject(RowHeightInj, ref())
ref="attachmentCellRef"
tabindex="0"
:style="{
height: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
height: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
class="nc-attachment-cell relative flex color-transition flex items-center"
class="nc-attachment-cell relative flex color-transition flex items-center w-full xs:(min-h-12 max-h-32)"
:class="{ 'justify-center': !active, 'justify-between': active }"
>
<LazyCellAttachmentCarousel />
@ -165,34 +200,41 @@ const rowHeight = inject(RowHeightInj, ref())
v-model="isOverDropZone"
inline
:target="currentCellRef"
data-rec="true"
class="nc-attachment-cell-dropzone text-white text-lg ring ring-accent ring-opacity-100 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
>
<MaterialSymbolsFileCopyOutline class="text-accent" />
Drop here
{{ $t('labels.dropHere') }}
</general-overlay>
</template>
<div
v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }"
:class="{ 'sm:(mx-auto px-4) xs:(w-full min-w-8)': !visibleItems.length }"
class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-testid="attachment-cell-file-picker-button"
@click.stop="open"
@click="open"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<NcTooltip placement="bottom">
<template #title> Click or drop a file into cell</template>
<NcTooltip placement="bottom" class="xs:w-full">
<template #title
><span data-rec="true">{{ $t('activity.attachmentDrop') }} </span></template
>
<div v-if="active || !visibleItems.length" class="flex items-center gap-1">
<div
v-if="active || !visibleItems.length || (isForm && visibleItems.length)"
class="flex items-center gap-1 xs:(w-full min-w-12 h-8 justify-center)"
>
<MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/>
<div
v-if="!visibleItems.length"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"
data-rec="true"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs xs:(justify-center rounded-lg text-sm)"
>
Add file(s)
{{ $t('activity.addFiles') }}
</div>
</div>
</NcTooltip>
@ -203,10 +245,10 @@ const rowHeight = inject(RowHeightInj, ref())
<template v-if="visibleItems.length">
<div
ref="sortableRef"
:class="{ 'justify-center': !isExpandedForm && !isGallery }"
:class="{ 'justify-center': !isExpandedForm && !isGallery && !isKanban }"
class="flex cursor-pointer w-full items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start"
:style="{
maxHeight: isForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
>
<template v-for="(item, i) of visibleItems" :key="item.url || item.title">
@ -218,20 +260,16 @@ const rowHeight = inject(RowHeightInj, ref())
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center"
:class="{ 'ml-2': active }"
@click="
() => {
if (isGallery) return
selectedImage = item
}
"
@click="() => onImageClick(item)"
>
<LazyCellAttachmentImage
:alt="item.title || `#${i}`"
class="rounded"
:class="{
'h-7.5 w-8.8': rowHeight === 1,
'h-11.5 w-12.8': rowHeight === 2,
'h-16.8 w-20.8': rowHeight === 4,
'h-20.8 !w-30': isExpandedForm || rowHeight === 6,
'h-20.8 !w-30': isForm || isExpandedForm || rowHeight === 6,
}"
:srcs="getPossibleAttachmentSrc(item)"
/>
@ -252,18 +290,18 @@ const rowHeight = inject(RowHeightInj, ref())
</div>
<div
v-if="active"
class="h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
v-if="active || (isForm && visibleItems.length)"
class="xs:hidden h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<NcTooltip v-else placement="bottom">
<template #title> View attachments</template>
<template #title> {{ $t('activity.viewAttachment') }}</template>
<component
:is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-[0.75rem]"
@click.stop="modalVisible = true"
@click.stop="onExpand"
/>
</NcTooltip>
</div>

21
packages/nc-gui/components/cell/attachment/utils.ts

@ -17,10 +17,10 @@ import {
storeToRefs,
useApi,
useAttachment,
useBase,
useFileDialog,
useI18n,
useInjectionState,
useProject,
watch,
} from '#imports'
import MdiPdfBox from '~icons/mdi/pdf-box'
@ -33,6 +33,8 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
(updateModelValue: (data: string | Record<string, any>[]) => void) => {
const isReadonly = inject(ReadonlyInj, ref(false))
const { t } = useI18n()
const isPublic = inject(IsPublicInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
@ -53,7 +55,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** for image carousel */
const selectedImage = ref()
const { project } = storeToRefs(useProject())
const { base } = storeToRefs(useBase())
const { api, isLoading } = useApi()
@ -61,8 +63,6 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const { appInfo } = useGlobal()
const { t } = useI18n()
const { getAttachmentSrc } = useAttachment()
const defaultAttachmentMeta = {
@ -138,7 +138,16 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
continue
}
}
// this prevent file with same names
const isFileNameAlreadyExist = attachments.value.some((el) => el.title === file.name)
if (isFileNameAlreadyExist) {
message.error(
t('labels.duplicateAttachment', {
filename: file.name,
}),
)
return
}
files.push(file)
}
@ -182,7 +191,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
try {
const data = await api.storage.upload(
{
path: [NOCO, project.value.title, meta.value?.title, column.value?.title].join('/'),
path: [NOCO, base.value.id, meta.value?.id, column.value?.id].join('/'),
},
{
files,

10
packages/nc-gui/components/dashboard/Sidebar.vue

@ -3,13 +3,17 @@ const workspaceStore = useWorkspace()
const { isWorkspaceLoading } = storeToRefs(workspaceStore)
const { isSharedBase } = storeToRefs(useProject())
const { isSharedBase } = storeToRefs(useBase())
const { isMobileMode } = useGlobal()
const treeViewDom = ref<HTMLElement>()
const isTreeViewOnScrollTop = ref(false)
const checkScrollTopMoreThanZero = () => {
if (isMobileMode.value) return
if (treeViewDom.value) {
if (treeViewDom.value.scrollTop > 0) {
isTreeViewOnScrollTop.value = true
@ -43,14 +47,14 @@ onUnmounted(() => {
</div>
<div
ref="treeViewDom"
class="flex flex-col nc-scrollbar-dark-md flex-grow"
class="flex flex-col nc-scrollbar-dark-md flex-grow xs:(border-transparent pt-2 pr-2)"
:class="{
'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop,
'pt-0.25': isSharedBase,
}"
>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
<DashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase">
<DashboardSidebarUserInfo />

7
packages/nc-gui/components/dashboard/Sidebar/Header.vue

@ -34,14 +34,11 @@ const showSidebarBtn = computed(() => !(isMobileMode.value && !activeViewTitleOr
hide-on-click
>
<template #title>
{{
isLeftSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
{{ isLeftSidebarOpen ? `${$t('title.hideSidebar')}` : `${$t('title.showSidebar')}` }}
</template>
<NcButton
v-if="showSidebarBtn"
v-e="['c:leftSidebar:hideToggle']"
:type="isMobileMode ? 'secondary' : 'text'"
:size="isMobileMode ? 'medium' : 'small'"
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !xs:(h-10.5 max-h-10.5 max-w-10.5) !md:(hover:bg-gray-200)"

9
packages/nc-gui/components/dashboard/Sidebar/TopSection.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
const workspaceStore = useWorkspace()
const projectStore = useProject()
const baseStore = useBase()
const { isUIAllowed } = useRoles()
@ -10,7 +10,7 @@ const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(worksp
const { navigateToWorkspaceSettings } = workspaceStore
const { isSharedBase } = storeToRefs(projectStore)
const { isSharedBase } = storeToRefs(baseStore)
const isCreateProjectOpen = ref(false)
@ -43,11 +43,12 @@ const navigateToSettings = () => {
</div>
</template>
<template v-else-if="!isSharedBase">
<div class="flex flex-col p-1 gap-y-0.5 mt-0.25 mb-0.5 truncate">
<div class="xs:hidden flex flex-col p-1 gap-y-0.5 mt-0.25 mb-0.5 truncate">
<DashboardSidebarTopSectionHeader />
<NcButton
v-if="isUIAllowed('workspaceSettings')"
v-e="['c:team:settings']"
type="text"
size="small"
class="nc-sidebar-top-button !xs:hidden"
@ -69,7 +70,7 @@ const navigateToSettings = () => {
modal
type="text"
class="nc-sidebar-top-button !hover:bg-gray-200 !xs:hidden"
data-testid="nc-sidebar-create-project-btn"
data-testid="nc-sidebar-create-base-btn"
>
<div class="gap-x-2 flex flex-row w-full items-center !font-normal">
<GeneralIcon icon="plus" />

54
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -1,12 +1,23 @@
<script lang="ts" setup>
import GithubButton from 'vue-github-button'
import {
computed,
message,
navigateTo,
onMounted,
ref,
storeToRefs,
useCopy,
useGlobal,
useSidebarStore,
useUsers,
watch,
} from '#imports'
const { user, signOut, token, appInfo } = useGlobal()
// So watcher in users store is triggered
useUsers()
const { clearWorkspaces } = useWorkspace()
const { leftSidebarState } = storeToRefs(useSidebarStore())
const { copy } = useCopy(true)
@ -26,7 +37,8 @@ const logout = async () => {
try {
await signOut(false)
await clearWorkspaces()
// No need as all stores are cleared on signout
// await clearWorkspaces()
await navigateTo('/signin')
} catch (e) {
@ -73,7 +85,7 @@ onMounted(() => {
class="flex flex-row py-2 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-10"
data-testid="nc-sidebar-userinfo"
>
<GeneralUserIcon />
<GeneralUserIcon :email="user?.email" size="base" />
<div class="flex truncate">
{{ name ? name : user?.email }}
</div>
@ -81,46 +93,46 @@ onMounted(() => {
</div>
<template #overlay>
<NcMenu data-testid="nc-sidebar-userinfo">
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout">
<NcMenuItem v-e="['c:user:logout']" data-testid="nc-sidebar-user-logout" @click="logout">
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" />
<GeneralIcon v-else icon="signout" class="menu-icon" />
<span class="menu-btn"> Log Out </span>
<span class="menu-btn"> {{ $t('general.logout') }}</span>
</NcMenuItem>
<template v-if="!isMobileMode">
<NcDivider />
<a href="https://docs.nocodb.com" target="_blank" class="!underline-transparent">
<a v-e="['c:nocodb:docs-open']" href="https://docs.nocodb.com" target="_blank" class="!underline-transparent">
<NcMenuItem>
<GeneralIcon icon="help" class="menu-icon mt-0.5" />
<span class="menu-btn"> Help Center </span>
<span class="menu-btn"> {{ $t('title.helpCenter') }} </span>
</NcMenuItem>
</a>
</template>
<NcDivider />
<a href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent">
<a v-e="['c:nocodb:discord']" href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="discord" />
<span class="menu-btn"> Join our Discord </span>
<span class="menu-btn"> {{ $t('labels.community.joinDiscord') }} </span>
</NcMenuItem>
</a>
<a href="https://www.reddit.com/r/NocoDB" target="_blank" class="!underline-transparent">
<a v-e="['c:nocodb:reddit']" href="https://www.reddit.com/r/NocoDB" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="reddit" />
<span class="menu-btn"> /r/NocoDB </span>
<span class="menu-btn"> {{ $t('labels.community.joinReddit') }} </span>
</NcMenuItem>
</a>
<a href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent">
<a v-e="['c:nocodb:twitter']" href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper group">
<GeneralIcon class="text-gray-500 group-hover:text-gray-800 my-0.5" icon="twitter" />
<span class="menu-btn"> Twitter </span>
<span class="menu-btn"> {{ $t('labels.twitter') }} </span>
</NcMenuItem>
</a>
<template v-if="!appInfo.ee">
<NcDivider />
<a-popover key="language" class="lang-menu !py-1.5" placement="rightBottom">
<NcMenuItem>
<NcMenuItem v-e="['c:translate:open']">
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" />
{{ $t('labels.language') }}
<div class="flex items-center text-gray-400 text-xs">(Community Translated)</div>
<div class="flex items-center text-gray-400 text-xs">{{ $t('labels.community.communityTranslated') }}</div>
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" />
@ -136,14 +148,14 @@ onMounted(() => {
<template v-if="!isMobileMode">
<NcDivider />
<NcMenuItem @click="onCopy">
<NcMenuItem v-e="['c:auth-token:copy']" @click="onCopy">
<GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" />
<GeneralIcon v-else icon="copy" class="menu-icon" />
<template v-if="isAuthTokenCopied"> Copied Auth Token </template>
<template v-else> Copy Auth Token </template>
<template v-if="isAuthTokenCopied"> {{ $t('title.copiedAuthToken') }} </template>
<template v-else> {{ $t('title.copyAuthToken') }} </template>
</NcMenuItem>
<nuxt-link v-e="['c:navbar:user:email']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="settings" class="menu-icon" /> Account Settings </NcMenuItem>
<nuxt-link v-e="['c:user:settings']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="settings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem>
</nuxt-link>
</template>
</NcMenu>

97
packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk'
import type { BaseType } from 'nocodb-sdk'
import { storeToRefs } from 'pinia'
import { toRef } from '@vue/reactivity'
import { resolveComponent } from '@vue/runtime-core'
@ -8,11 +8,11 @@ import { ProjectRoleInj, useDialog, useRoles } from '#imports'
const props = withDefaults(
defineProps<{
project: ProjectType
baseIndex?: number
base: BaseType
sourceIndex?: number
}>(),
{
baseIndex: 0,
sourceIndex: 0,
},
)
@ -22,18 +22,18 @@ const emit = defineEmits<{
const { isUIAllowed } = useRoles()
const project = toRef(props, 'project')
const base = toRef(props, 'base')
const { $e } = useNuxtApp()
const projectStore = useProject()
const baseStore = useBase()
const { isSharedBase } = storeToRefs(projectStore)
const { isSharedBase } = storeToRefs(baseStore)
const projectRole = inject(ProjectRoleInj)
const baseRole = inject(ProjectRoleInj)
function openSchemaMagicDialog(baseId?: string) {
if (!baseId) return
function openSchemaMagicDialog(sourceId?: string) {
if (!sourceId) return
$e('c:table:create:navdraw')
@ -41,7 +41,7 @@ function openSchemaMagicDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgSchemaMagic'), {
'modelValue': isOpen,
'baseId': baseId,
'sourceId': sourceId,
'onUpdate:modelValue': closeDialog,
})
@ -52,8 +52,8 @@ function openSchemaMagicDialog(baseId?: string) {
}
}
function openQuickImportDialog(type: string, baseId?: string) {
if (!baseId) return
function openQuickImportDialog(type: string, sourceId?: string) {
if (!sourceId) return
$e(`a:actions:import-${type}`)
@ -62,7 +62,7 @@ function openQuickImportDialog(type: string, baseId?: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen,
'importType': type,
'baseId': baseId,
'sourceId': sourceId,
'onUpdate:modelValue': closeDialog,
})
@ -73,8 +73,8 @@ function openQuickImportDialog(type: string, baseId?: string) {
}
}
function openAirtableImportDialog(baseId?: string) {
if (!baseId) return
function openAirtableImportDialog(baseId?: string, sourceId?: string) {
if (!baseId || !sourceId) return
$e('a:actions:import-airtable')
@ -83,6 +83,7 @@ function openAirtableImportDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen,
'baseId': baseId,
'sourceId': sourceId,
'onUpdate:modelValue': closeDialog,
})
@ -93,8 +94,8 @@ function openAirtableImportDialog(baseId?: string) {
}
}
function openTableCreateMagicDialog(baseId?: string) {
if (!baseId) return
function openTableCreateMagicDialog(sourceId?: string) {
if (!sourceId) return
$e('c:table:create:navdraw')
@ -102,7 +103,7 @@ function openTableCreateMagicDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgTableMagic'), {
'modelValue': isOpen,
'baseId': baseId,
'sourceId': sourceId,
'onUpdate:modelValue': closeDialog,
})
@ -116,7 +117,7 @@ function openTableCreateMagicDialog(baseId?: string) {
<template>
<div
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="emit('openTableCreateDialog')"
>
@ -139,14 +140,14 @@ function openTableCreateMagicDialog(baseId?: string) {
<GeneralIcon icon="magic" class="ml-1 text-orange-400" />
</div>
</template>
<a-menu-item key="table-magic" @click="openTableCreateMagicDialog(project.bases[baseIndex].id)">
<div class="color-transition nc-project-menu-item group">
<a-menu-item key="table-magic" @click="openTableCreateMagicDialog(base.sources[sourceIndex].id)">
<div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="magic1" class="group-hover:text-accent" />
Create table
</div>
</a-menu-item>
<a-menu-item key="schema-magic" @click="openSchemaMagicDialog(project.bases[baseIndex].id)">
<div class="color-transition nc-project-menu-item group">
<a-menu-item key="schema-magic" @click="openSchemaMagicDialog(base.sources[sourceIndex].id)">
<div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="magic1" class="group-hover:text-accent" />
Create schema
</div>
@ -158,44 +159,44 @@ function openTableCreateMagicDialog(baseId?: string) {
<!-- Quick Import From -->
<a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0">
<a-menu-item
v-if="isUIAllowed('airtableImport', { roles: projectRole })"
v-if="isUIAllowed('airtableImport', { roles: baseRole })"
key="quick-import-airtable"
@click="openAirtableImportDialog(project.bases[baseIndex].id)"
@click="openAirtableImportDialog(base.id, base.sources[sourceIndex].id)"
>
<div class="color-transition nc-project-menu-item group">
<div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="airtable" class="group-hover:text-accent" />
Airtable
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('csvImport', { roles: projectRole })"
v-if="isUIAllowed('csvImport', { roles: baseRole })"
key="quick-import-csv"
@click="openQuickImportDialog('csv', project.bases[baseIndex].id)"
@click="openQuickImportDialog('csv', base.sources[sourceIndex].id)"
>
<div class="color-transition nc-project-menu-item group">
<div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="csv" class="group-hover:text-accent" />
CSV file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('jsonImport', { roles: projectRole })"
v-if="isUIAllowed('jsonImport', { roles: baseRole })"
key="quick-import-json"
@click="openQuickImportDialog('json', project.bases[baseIndex].id)"
@click="openQuickImportDialog('json', base.sources[sourceIndex].id)"
>
<div class="color-transition nc-project-menu-item group">
<div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="json" class="group-hover:text-accent" />
JSON file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport', { roles: projectRole })"
v-if="isUIAllowed('excelImport', { roles: baseRole })"
key="quick-import-excel"
@click="openQuickImportDialog('excel', project.bases[baseIndex].id)"
@click="openQuickImportDialog('excel', base.sources[sourceIndex].id)"
>
<div class="color-transition nc-project-menu-item group">
<div class="color-transition nc-base-menu-item group">
<GeneralIcon icon="excel" class="group-hover:text-accent" />
Microsoft Excel
</div>
@ -205,26 +206,26 @@ function openTableCreateMagicDialog(baseId?: string) {
<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, project.id)">
<div class="color-transition nc-project-menu-item group">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL, base.id)">
<div class="color-transition nc-base-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, project.id)">
<div class="color-transition nc-project-menu-item group">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG, base.id)">
<div class="color-transition nc-base-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, project.id)">
<div class="color-transition nc-project-menu-item group">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE, base.id)">
<div class="color-transition nc-base-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, project.id)">
<div class="color-transition nc-project-menu-item group">
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL, base.id)">
<div class="color-transition nc-base-menu-item group">
<SimpleIconsMicrosoftsqlserver class="group-hover:text-accent" />
MSSQL
</div>
@ -232,9 +233,9 @@ function openTableCreateMagicDialog(baseId?: string) {
<a-menu-item
v-if="appInfo.ee"
key="connect-new-source"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE, project.id)"
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE, base.id)"
>
<div class="color-transition nc-project-menu-item group">
<div class="color-transition nc-base-menu-item group">
<LogosSnowflakeIcon class="group-hover:text-accent" />
Snowflake
</div>
@ -243,12 +244,12 @@ function openTableCreateMagicDialog(baseId?: string) {
<a-menu-divider class="my-0" /> -->
<a-menu-item v-if="isUIAllowed('importRequest', { roles: projectRole })" key="add-new-table" class="py-1 rounded-b">
<a-menu-item v-if="isUIAllowed('importRequest', { roles: baseRole })" 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)"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-base-menu-item group after:(!rounded-b)"
>
<GeneralIcon icon="openInNew" class="group-hover:text-accent" />
<!-- Request a data source you need? -->

41
packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

@ -1,21 +1,21 @@
<script lang="ts" setup>
import type { BaseType, ProjectType } from 'nocodb-sdk'
import type { BaseType, SourceType } from 'nocodb-sdk'
const props = defineProps<{
source: SourceType
base: BaseType
project: ProjectType
}>()
const base = toRef(props, 'base')
const source = toRef(props, 'source')
const { isUIAllowed } = useRoles()
const projectRole = inject(ProjectRoleInj)
const baseRole = inject(ProjectRoleInj)
const { $e } = useNuxtApp()
function openAirtableImportDialog(baseId?: string) {
if (!baseId) return
function openAirtableImportDialog(baseId?: string, sourceId?: string) {
if (!baseId || !sourceId) return
$e('a:actions:import-airtable')
@ -24,6 +24,7 @@ function openAirtableImportDialog(baseId?: string) {
const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen,
'baseId': baseId,
'sourceId': sourceId,
'onUpdate:modelValue': closeDialog,
})
@ -35,7 +36,7 @@ function openAirtableImportDialog(baseId?: string) {
}
function openQuickImportDialog(type: string) {
if (!base.value?.id) return
if (!source.value?.id) return
$e(`a:actions:import-${type}`)
@ -44,7 +45,7 @@ function openQuickImportDialog(type: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen,
'importType': type,
'baseId': base.value.id,
'sourceId': source.value.id,
'onUpdate:modelValue': closeDialog,
})
@ -58,7 +59,7 @@ function openQuickImportDialog(type: string) {
<template>
<!-- Quick Import From -->
<NcSubMenu class="py-0" data-testid="nc-sidebar-project-import">
<NcSubMenu class="py-0" data-testid="nc-sidebar-base-import">
<template #title>
<GeneralIcon icon="download" />
@ -68,39 +69,43 @@ function openQuickImportDialog(type: string) {
<template #expandIcon></template>
<NcMenuItem
v-if="isUIAllowed('airtableImport', { roles: projectRole })"
v-if="isUIAllowed('airtableImport', { roles: baseRole })"
key="quick-import-airtable"
@click="openAirtableImportDialog(base.id)"
v-e="['c:import:airtable']"
@click="openAirtableImportDialog(source.base_id, source.id)"
>
<GeneralIcon icon="airtable" class="max-w-3.75 group-hover:text-black" />
<div class="ml-0.5">Airtable</div>
<div class="ml-0.5">{{ $t('labels.airtable') }}</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('csvImport', { roles: projectRole })"
v-if="isUIAllowed('csvImport', { roles: baseRole })"
key="quick-import-csv"
v-e="['c:import:csv']"
@click="openQuickImportDialog('csv')"
>
<GeneralIcon icon="csv" class="w-4 group-hover:text-black" />
CSV file
{{ $t('labels.csvFile') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('jsonImport', { roles: projectRole })"
v-if="isUIAllowed('jsonImport', { roles: baseRole })"
key="quick-import-json"
v-e="['c:import:json']"
@click="openQuickImportDialog('json')"
>
<GeneralIcon icon="code" class="w-4 group-hover:text-black" />
JSON file
{{ $t('labels.jsonFile') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('excelImport', { roles: projectRole })"
v-if="isUIAllowed('excelImport', { roles: baseRole })"
key="quick-import-excel"
v-e="['c:import:excel']"
@click="openQuickImportDialog('excel')"
>
<GeneralIcon icon="excel" class="max-w-4 group-hover:text-black" />
Microsoft Excel
{{ $t('labels.microsoftExcel') }}
</NcMenuItem>
</NcSubMenu>
</template>

44
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -6,15 +6,17 @@ const { $e } = useNuxtApp()
const { refreshCommandPalette } = useCommandPalette()
const viewsStore = useViewsStore()
const { views } = storeToRefs(viewsStore)
const { loadViews, navigateToView } = viewsStore
const table = inject(SidebarTableInj)!
const project = inject(ProjectInj)!
const base = inject(ProjectInj)!
const isViewListLoading = ref(false)
const toBeCreateType = ref<ViewTypes>()
const isOpen = ref(false)
function onOpenModal({
async function onOpenModal({
title = '',
type,
copyViewId,
@ -25,7 +27,17 @@ function onOpenModal({
copyViewId?: string
groupingFieldColumnId?: string
}) {
if (isViewListLoading.value) return
toBeCreateType.value = type
isViewListLoading.value = true
await loadViews({
tableId: table.value.id!,
})
isOpen.value = false
isViewListLoading.value = false
const isDlgOpen = ref(true)
@ -36,7 +48,6 @@ function onOpenModal({
'tableId': table.value.id,
'selectedViewId': copyViewId,
groupingFieldColumnId,
'views': views,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
@ -44,13 +55,19 @@ function onOpenModal({
refreshCommandPalette()
await loadViews({
tableId: table.value.id!,
force: true,
})
table.value.meta = {
...(table.value.meta as object),
hasNonDefaultViews: true,
}
navigateToView({
view,
tableId: table.value.id!,
projectId: project.value.id!,
baseId: base.value.id!,
})
$e('a:view:create', { view: view.type })
@ -59,6 +76,7 @@ function onOpenModal({
function closeDialog() {
isOpen.value = false
isDlgOpen.value = false
close(1000)
}
@ -66,18 +84,19 @@ function onOpenModal({
</script>
<template>
<NcDropdown v-model:isOpen="isOpen" destroy-popup-on-hide @click.stop="isOpen = !isOpen">
<NcDropdown v-model:visible="isOpen" destroy-popup-on-hide @click.stop="isOpen = true">
<slot />
<template #overlay>
<NcMenu class="max-w-48">
<NcMenuItem @click="onOpenModal({ type: ViewTypes.GRID })">
<NcMenuItem @click.stop="onOpenModal({ type: ViewTypes.GRID })">
<div class="item" data-testid="sidebar-view-create-grid">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" />
<div>Grid</div>
</div>
<GeneralIcon class="plus" icon="plus" />
<GeneralLoader v-if="toBeCreateType === ViewTypes.GRID && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div>
</NcMenuItem>
@ -88,7 +107,8 @@ function onOpenModal({
<div>Form</div>
</div>
<GeneralIcon class="plus" icon="plus" />
<GeneralLoader v-if="toBeCreateType === ViewTypes.FORM && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div>
</NcMenuItem>
<NcMenuItem @click="onOpenModal({ type: ViewTypes.GALLERY })">
@ -98,7 +118,8 @@ function onOpenModal({
<div>Gallery</div>
</div>
<GeneralIcon class="plus" icon="plus" />
<GeneralLoader v-if="toBeCreateType === ViewTypes.GALLERY && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div>
</NcMenuItem>
<NcMenuItem data-testid="sidebar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })">
@ -108,7 +129,8 @@ function onOpenModal({
<div>Kanban</div>
</div>
<GeneralIcon class="plus" icon="plus" />
<GeneralLoader v-if="toBeCreateType === ViewTypes.KANBAN && isViewListLoading" />
<GeneralIcon v-else class="plus" icon="plus" />
</div>
</NcMenuItem>
</NcMenu>

472
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -2,7 +2,7 @@
import { nextTick } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import { stringifyRolesObj } from 'nocodb-sdk'
import type { BaseType, ProjectType, TableType } from 'nocodb-sdk'
import type { BaseType, SourceType, TableType } from 'nocodb-sdk'
import { LoadingOutlined } from '@ant-design/icons-vue'
import { useTitle } from '@vueuse/core'
import {
@ -10,10 +10,27 @@ import {
ProjectInj,
ProjectRoleInj,
ToggleDialogInj,
TreeViewInj,
computed,
extractSdkResponseErrorMsg,
h,
inject,
navigateTo,
openLink,
ref,
resolveComponent,
storeToRefs,
useProjects,
useBase,
useBases,
useCopy,
useDialog,
useGlobal,
useI18n,
useRoles,
useRouter,
useTablesStore,
useTabs,
useToggle,
} from '#imports'
import type { NcProject } from '#imports'
import { useNuxtApp } from '#app'
@ -27,23 +44,28 @@ const indicator = h(LoadingOutlined, {
})
const router = useRouter()
const route = router.currentRoute
const { isSharedBase } = storeToRefs(useBase())
const { setMenuContext, openRenameTableDialog, duplicateTable, contextMenuTarget } = inject(TreeViewInj)!
const project = inject(ProjectInj)!
const base = inject(ProjectInj)!
const projectsStore = useProjects()
const basesStore = useBases()
const { isMobileMode } = useGlobal()
const { loadProject, loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = projectsStore
const { projects } = storeToRefs(projectsStore)
const { createProject: _createProject, updateProject, getProjectMetaInfo } = basesStore
const { bases } = storeToRefs(basesStore)
const { loadProjectTables } = useTablesStore()
const { activeTable } = storeToRefs(useTablesStore())
const { appInfo, navigateToProject } = useGlobal()
const { appInfo } = useGlobal()
const { orgRoles, isUIAllowed } = useRoles()
@ -61,11 +83,11 @@ const { t } = useI18n()
const input = ref<HTMLInputElement>()
const projectRole = inject(ProjectRoleInj)
const baseRole = inject(ProjectRoleInj)
const { activeProjectId } = storeToRefs(useProjects())
const { activeProjectId } = storeToRefs(useBases())
const { projectUrl } = useProject()
const { baseUrl } = useBase()
const toggleDialog = inject(ToggleDialogInj, () => {})
@ -81,9 +103,9 @@ const keys = ref<Record<string, number>>({})
const isTableDeleteDialogVisible = ref(false)
const isProjectDeleteDialogVisible = ref(false)
// If only project is open, i.e in case of docs, project view is open and not the page view
const projectViewOpen = computed(() => {
const routeNameSplit = String(route.value?.name).split('projectId-index-index')
// If only base is open, i.e in case of docs, base view is open and not the page view
const baseViewOpen = computed(() => {
const routeNameSplit = String(route.value?.name).split('baseId-index-index')
if (routeNameSplit.length <= 1) return false
const routeNameAfterProjectView = routeNameSplit[routeNameSplit.length - 1]
@ -96,7 +118,7 @@ const showBaseOption = computed(() => {
const enableEditMode = () => {
editMode.value = true
tempTitle.value = project.value.title!
tempTitle.value = base.value.title!
nextTick(() => {
input.value?.focus()
input.value?.select()
@ -108,15 +130,15 @@ const updateProjectTitle = async () => {
if (!tempTitle.value) return
try {
await updateProject(project.value.id!, {
await updateProject(base.value.id!, {
title: tempTitle.value,
})
editMode.value = false
tempTitle.value = ''
$e('a:project:rename')
$e('a:base:rename')
useTitle(`${project.value?.title}`)
useTitle(`${base.value?.title}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -128,7 +150,7 @@ const copyProjectInfo = async () => {
try {
if (
await copy(
Object.entries(await getProjectMetaInfo(project.value.id!)!)
Object.entries(await getProjectMetaInfo(base.value.id!)!)
.map(([k, v]) => `${k}: **${v}**`)
.join('\n'),
)
@ -146,36 +168,34 @@ defineExpose({
enableEditMode,
})
const setIcon = async (icon: string, project: ProjectType) => {
const setIcon = async (icon: string, base: BaseType) => {
try {
const meta = {
...((project.meta as object) || {}),
...((base.meta as object) || {}),
icon,
}
projectsStore.updateProject(project.id!, { meta: JSON.stringify(meta) })
basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:project:icon:navdraw', { icon })
$e('a:base:icon:navdraw', { icon })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
function openTableCreateDialog(baseIndex?: number | undefined) {
$e('c:table:create:navdraw')
function openTableCreateDialog(sourceIndex?: number | undefined) {
const isOpen = ref(true)
let baseId = project.value!.bases?.[0].id
if (typeof baseIndex === 'number') {
baseId = project.value!.bases?.[baseIndex].id
let sourceId = base.value!.sources?.[0].id
if (typeof sourceIndex === 'number') {
sourceId = base.value!.sources?.[sourceIndex].id
}
if (!baseId || !project.value?.id) return
if (!sourceId || !base.value?.id) return
const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen,
baseId, // || bases.value[0].id,
'projectId': project.value!.id,
sourceId, // || sources.value[0].id,
'baseId': base.value!.id,
'onCreate': closeDialog,
'onUpdate:modelValue': () => closeDialog(),
})
@ -185,10 +205,10 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
if (!table) return
project.value.isExpanded = true
base.value.isExpanded = true
if (!activeKey.value || !activeKey.value.includes(`collapse-${baseId}`)) {
activeKey.value.push(`collapse-${baseId}`)
if (!activeKey.value || !activeKey.value.includes(`collapse-${sourceId}`)) {
activeKey.value.push(`collapse-${sourceId}`)
}
// TODO: Better way to know when the table node dom is available
@ -209,103 +229,90 @@ const addNewProjectChildEntity = async () => {
isAddNewProjectChildEntityLoading.value = true
const isProjectPopulated = projectsStore.isProjectPopulated(project.value.id!)
if (!isProjectPopulated && project.value.type === NcProjectType.DB) {
const isProjectPopulated = basesStore.isProjectPopulated(base.value.id!)
if (!isProjectPopulated && base.value.type === NcProjectType.DB) {
// We do not wait for tables api, so that add new table is seamless.
// Only con would be while saving table duplicate table name FE validation might not work
// If the table list api takes time to load before the table name validation
loadProjectTables(project.value.id!)
loadProjectTables(base.value.id!)
}
try {
openTableCreateDialog()
if (!project.value.isExpanded && project.value.type !== NcProjectType.DB) {
project.value.isExpanded = true
if (!base.value.isExpanded && base.value.type !== NcProjectType.DB) {
base.value.isExpanded = true
}
} finally {
isAddNewProjectChildEntityLoading.value = false
}
}
// todo: temp
const isSharedBase = ref(false)
const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => {
if (!project) {
const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => {
if (!base) {
return
}
if (!toggleIsExpanded) $e('c:base:open')
ignoreNavigation = isMobileMode.value || ignoreNavigation
toggleIsExpanded = isMobileMode.value || toggleIsExpanded
if (toggleIsExpanded) {
project.isExpanded = !project.isExpanded
base.isExpanded = !base.isExpanded
} else {
project.isExpanded = true
base.isExpanded = true
}
const isProjectPopulated = projectsStore.isProjectPopulated(project.id!)
const isProjectPopulated = basesStore.isProjectPopulated(base.id!)
let isSharedBase = false
// if shared base ignore navigation
if (route.value.params.typeOrId === 'base') {
isSharedBase = true
}
if (!isProjectPopulated) project.isLoading = true
if (!isProjectPopulated) base.isLoading = true
if (!ignoreNavigation) {
await navigateTo(
projectUrl({
id: project.id!,
baseUrl({
id: base.id!,
type: 'database',
isSharedBase,
isSharedBase: isSharedBase.value,
}),
)
}
if (!isProjectPopulated) {
await loadProjectTables(project.id!)
await loadProjectTables(base.id!)
}
if (!isProjectPopulated) {
const updatedProject = projects.value.get(project.id!)!
const updatedProject = bases.value.get(base.id!)!
updatedProject.isLoading = false
}
}
function openErdView(base: BaseType) {
activeBaseId.value = base.id
isErdModalOpen.value = !isErdModalOpen.value
}
async function openProjectErdView(_project: ProjectType) {
if (!_project.id) return
if (!projectsStore.isProjectPopulated(_project.id)) {
await loadProject(_project.id)
}
function openErdView(source: SourceType) {
$e('c:project:relation')
const project = projects.value.get(_project.id)
const isOpen = ref(true)
const base = project?.bases?.[0]
if (!base) return
openErdView(base)
}
const { close } = useDialog(resolveComponent('DlgProjectErd'), {
'modelValue': isOpen,
'sourceId': source!.id,
'onUpdate:modelValue': () => closeDialog(),
'baseId': base.value.id,
})
const reloadTables = async () => {
$e('a:table:refresh:navdraw')
function closeDialog() {
isOpen.value = false
// await loadTables()
close(1000)
}
}
const contextMenuBase = computed(() => {
if (contextMenuTarget.type === 'base') {
if (contextMenuTarget.type === 'source') {
return contextMenuTarget.value
} else if (contextMenuTarget.type === 'table') {
const base = project.value?.bases?.find((b) => b.id === contextMenuTarget.value.base_id)
if (base) return base
const source = base.value?.sources?.find((b) => b.id === contextMenuTarget.value.source_id)
if (source) return source
}
return null
})
@ -315,11 +322,11 @@ watch(
async () => {
if (!activeTable.value) return
const baseId = activeTable.value.base_id
if (!baseId) return
const sourceId = activeTable.value.source_id
if (!sourceId) return
if (!activeKey.value.includes(`collapse-${baseId}`)) {
activeKey.value.push(`collapse-${baseId}`)
if (!activeKey.value.includes(`collapse-${sourceId}`)) {
activeKey.value.push(`collapse-${sourceId}`)
}
},
{
@ -340,87 +347,69 @@ onKeyStroke('Escape', () => {
const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref()
const duplicateProject = (project: ProjectType) => {
selectedProjectToDuplicate.value = project
const duplicateProject = (base: BaseType) => {
selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true
}
const { $jobs } = useNuxtApp()
const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string }) => {
await loadProjects('workspace')
$jobs.subscribe({ id: jobData.id }, undefined, async (status: string) => {
if (status === JobStatus.COMPLETED) {
await loadProjects('workspace')
const project = projects.value.get(jobData.project_id)
// open project after duplication
if (project) {
await navigateToProject({
projectId: project.id,
type: project.type,
})
}
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate project')
await loadProjects('workspace')
}
})
$e('a:project:duplicate')
const tableDelete = () => {
isTableDeleteDialogVisible.value = true
$e('c:table:delete')
}
const projectDelete = () => {
isProjectDeleteDialogVisible.value = true
$e('c:project:delete')
}
</script>
<template>
<NcDropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu">
<div
class="mx-1 nc-project-sub-menu rounded-md"
:class="{ active: project.isExpanded }"
:data-testid="`nc-sidebar-project-${project.title}`"
:data-project-id="project.id"
class="mx-1 nc-base-sub-menu rounded-md"
:class="{ active: base.isExpanded }"
:data-testid="`nc-sidebar-base-${base.title}`"
:data-base-id="base.id"
>
<div class="flex items-center gap-0.75 py-0.25 cursor-pointer" @contextmenu="setMenuContext('project', project)">
<div class="flex items-center gap-0.75 py-0.25 cursor-pointer" @contextmenu="setMenuContext('base', base)">
<div
ref="projectNodeRefs"
ref="baseNodeRefs"
:class="{
'bg-primary-selected active': activeProjectId === project.id && projectViewOpen && !isMobileMode,
'hover:bg-gray-200': !(activeProjectId === project.id && projectViewOpen),
'bg-primary-selected active': activeProjectId === base.id && baseViewOpen && !isMobileMode,
'hover:bg-gray-200': !(activeProjectId === base.id && baseViewOpen),
}"
:data-testid="`nc-sidebar-project-title-${project.title}`"
class="nc-sidebar-node project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
:data-testid="`nc-sidebar-base-title-${base.title}`"
class="nc-sidebar-node base-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
>
<NcButton
v-e="['c:base:expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75 !xs:visible"
@click="onProjectClick(project, true, true)"
@click="onProjectClick(base, true, true)"
>
<GeneralIcon
icon="triangleFill"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90 !xs:visible"
:class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }"
:class="{ '!rotate-180': base.isExpanded, '!visible': isOptionsOpen }"
/>
</NcButton>
<div class="flex items-center mr-1" @click="onProjectClick(project)">
<div class="flex items-center mr-1" @click="onProjectClick(base)">
<div class="flex items-center select-none w-6 h-full">
<a-spin
v-if="project.isLoading"
class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8"
:indicator="indicator"
/>
<a-spin v-if="base.isLoading" class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8" :indicator="indicator" />
<LazyGeneralEmojiPicker
v-else
:key="project.meta?.icon"
:emoji="project.meta?.icon"
:key="base.meta?.icon"
v-e="['c:base:emojiSelect']"
:emoji="base.meta?.icon"
:readonly="true"
size="small"
@emoji-selected="setIcon($event, project)"
@emoji-selected="setIcon($event, base)"
>
<template #default>
<GeneralProjectIcon :type="project.type" />
<GeneralProjectIcon :type="base.type" />
</template>
</LazyGeneralEmojiPicker>
</div>
@ -431,7 +420,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
ref="input"
v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen && !isMobileMode }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode }"
@click.stop
@keyup.enter="updateProjectTitle"
@keyup.esc="updateProjectTitle"
@ -441,15 +430,16 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen }"
@click="onProjectClick(project)"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
@click="onProjectClick(base)"
>
{{ project.title }}
{{ base.title }}
</span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']">
<NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton
v-e="['c:base:options']"
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }"
data-testid="nc-sidebar-context-menu"
@ -466,32 +456,38 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
maxHeight: '70vh',
overflow: 'overlay',
}"
:data-testid="`nc-sidebar-project-${project.title}-options`"
:data-testid="`nc-sidebar-base-${base.title}-options`"
@click="isOptionsOpen = false"
>
<template v-if="!isSharedBase">
<NcMenuItem v-if="isUIAllowed('projectRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode">
<NcMenuItem
v-if="isUIAllowed('baseRename')"
v-e="['c:base:rename']"
data-testid="nc-sidebar-project-rename"
@click="enableEditMode"
>
<GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.rename') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-duplicate"
@click="duplicateProject(project)"
v-if="isUIAllowed('baseDuplicate', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
v-e="['c:base:duplicate']"
data-testid="nc-sidebar-base-duplicate"
@click="duplicateProject(base)"
>
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</NcMenuItem>
<NcDivider v-if="['projectDuplicate', 'projectRename'].some((permission) => isUIAllowed(permission))" />
<NcDivider v-if="['baseDuplicate', 'baseRename'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info -->
<NcMenuItem
v-if="!isEeUI"
key="copy"
v-e="['c:navbar:user:copy-proj-info']"
data-testid="nc-sidebar-project-copy-project-info"
v-e="['c:base:copy-proj-info']"
data-testid="nc-sidebar-base-copy-base-info"
@click.stop="copyProjectInfo"
>
<GeneralIcon icon="copy" class="group-hover:text-black" />
@ -499,47 +495,57 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</NcMenuItem>
<!-- ERD View -->
<NcMenuItem key="erd" data-testid="nc-sidebar-project-relations" @click="openProjectErdView(project)">
<NcMenuItem
key="erd"
v-e="['c:base:erd']"
data-testid="nc-sidebar-base-relations"
@click="openErdView(base?.sources?.[0]!)"
>
<GeneralIcon icon="erd" />
Relations
{{ $t('title.relations') }}
</NcMenuItem>
<!-- Swagger: Rest APIs -->
<NcMenuItem
v-if="isUIAllowed('apiDocs')"
key="api"
v-e="['e:api-docs']"
data-testid="nc-sidebar-project-rest-apis"
@click.stop="openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl)"
v-e="['c:base:api-docs']"
data-testid="nc-sidebar-base-rest-apis"
@click.stop="
() => {
$e('c:base:api-docs')
openLink(`/api/v1/db/meta/projects/${base.id}/swagger`, appInfo.ncSiteUrl)
}
"
>
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" />
{{ $t('activity.account.swagger') }}
</NcMenuItem>
</template>
<template v-if="project.bases && project.bases[0] && showBaseOption">
<template v-if="base.sources && base.sources[0] && showBaseOption">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" />
<DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template>
<NcDivider v-if="['projectMiscSettings', 'projectDelete'].some((permission) => isUIAllowed(permission))" />
<NcDivider v-if="['baseMiscSettings', 'baseDelete'].some((permission) => isUIAllowed(permission))" />
<NcMenuItem
v-if="isUIAllowed('projectMiscSettings')"
v-if="isUIAllowed('baseMiscSettings')"
key="teamAndSettings"
v-e="['c:navdraw:project-settings']"
data-testid="nc-sidebar-project-settings"
class="nc-sidebar-project-project-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)"
v-e="['c:base:settings']"
data-testid="nc-sidebar-base-settings"
class="nc-sidebar-base-base-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, base.id)"
>
<GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.settings') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-delete"
v-if="isUIAllowed('baseDelete', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
data-testid="nc-sidebar-base-delete"
class="!text-red-500 !hover:bg-red-50"
@click="isProjectDeleteDialogVisible = true"
@click="projectDelete"
>
<GeneralIcon icon="delete" class="w-4" />
{{ $t('general.delete') }}
@ -549,11 +555,12 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</NcDropdown>
<NcButton
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-e="['c:base:create-table']"
class="nc-sidebar-node-btn"
size="xxsmall"
type="text"
data-testid="nc-sidebar-add-project-entity"
data-testid="nc-sidebar-add-base-entity"
:class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }"
:loading="isAddNewProjectChildEntityLoading"
@click.stop="addNewProjectChildEntity"
@ -564,67 +571,70 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
<div
v-if="project.id && !project.isLoading"
v-if="base.id && !base.isLoading"
key="g1"
class="overflow-x-hidden transition-max-height"
:class="{ 'max-h-0': !project.isExpanded }"
:class="{ 'max-h-0': !base.isExpanded }"
>
<template v-if="project && project?.bases">
<template v-if="base && base?.sources">
<div class="flex-1 overflow-y-auto overflow-x-hidden flex flex-col" :class="{ 'mb-[20px]': isSharedBase }">
<div v-if="project?.bases?.[0]?.enabled" class="flex-1">
<div v-if="base?.sources?.[0]?.enabled" class="flex-1">
<div class="transition-height duration-200">
<DashboardTreeViewTableList :project="project" :base-index="0" />
<DashboardTreeViewTableList :base="base" :source-index="0" />
</div>
</div>
<div v-if="project?.bases?.slice(1).filter((el) => el.enabled)?.length" class="transition-height duration-200">
<div v-if="base?.sources?.slice(1).filter((el) => el.enabled)?.length" class="transition-height duration-200">
<div class="border-none sortable-list">
<div v-for="(base, baseIndex) of project.bases" :key="`base-${base.id}`">
<template v-if="baseIndex === 0"></template>
<div v-for="(source, sourceIndex) of base.sources" :key="`source-${source.id}`">
<template v-if="sourceIndex === 0"></template>
<a-collapse
v-else-if="base && base.enabled"
v-else-if="source && source.enabled"
v-model:activeKey="activeKey"
class="!mx-0 !px-0 nc-sidebar-base-node"
v-e="['c:source:toggle-expand']"
class="!mx-0 !px-0 nc-sidebar-source-node"
:class="[{ hidden: searchActive && !!filterQuery }]"
expand-icon-position="left"
:bordered="false"
ghost
>
<template #expandIcon="{ isActive }">
<div class="flex flex-row items-center -mt-2">
<div
class="nc-sidebar-expand nc-sidebar-node-btn flex flex-row items-center -mt-2 xs:(mt-3 border-1 border-gray-200 px-2.25 py-0.5 rounded-md !mr-0.25)"
>
<GeneralIcon
icon="triangleFill"
class="nc-sidebar-base-node-btns -mt-0.75 invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90"
class="nc-sidebar-source-node-btns -mt-0.75 invisible xs:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90"
:class="{ '!rotate-180': isActive }"
/>
</div>
</template>
<a-collapse-panel :key="`collapse-${base.id}`">
<a-collapse-panel :key="`collapse-${source.id}`">
<template #header>
<div class="min-w-20 w-full flex flex-row group">
<div class="nc-sidebar-node min-w-20 w-full flex flex-row group py-0.25">
<div
v-if="baseIndex === 0"
class="base-context flex items-center gap-2 text-gray-800"
@contextmenu="setMenuContext('base', base)"
v-if="sourceIndex === 0"
class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@contextmenu="setMenuContext('source', source)"
>
<GeneralBaseLogo :base-type="base.type" />
Default
<GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
{{ $t('general.default') }}
</div>
<div
v-else
class="base-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('base', base)"
class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('source', source)"
>
<GeneralBaseLogo :base-type="base.type" class="min-w-4" />
<GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
<div
:data-testid="`nc-sidebar-project-${base.alias}`"
class="flex capitalize text-ellipsis overflow-hidden select-none"
:data-testid="`nc-sidebar-base-${source.alias}`"
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ base.alias || '' }}
{{ source.alias || '' }}
</div>
<a-tooltip>
<template #title>External DB</template>
<a-tooltip class="xs:(hidden)">
<template #title>{{ $t('objects.externalDb') }}</template>
<div>
<GeneralIcon icon="info" class="text-gray-400 -mt-0.5 hover:text-gray-700 mr-1" />
</div>
@ -632,16 +642,17 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
<div class="flex flex-row items-center gap-x-0.25 w-12.25">
<NcDropdown
:visible="isBasesOptionsOpen[base!.id!]"
:visible="isBasesOptionsOpen[source!.id!]"
:trigger="['click']"
@update:visible="isBasesOptionsOpen[base!.id!] = $event"
@update:visible="isBasesOptionsOpen[source!.id!] = $event"
>
<NcButton
v-e="['c:source:options']"
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isBasesOptionsOpen[base!.id!] }"
:class="{ '!text-black !opacity-100': isBasesOptionsOpen[source!.id!] }"
type="text"
size="xxsmall"
@click.stop="isBasesOptionsOpen[base!.id!] = !isBasesOptionsOpen[base!.id!]"
@click.stop="isBasesOptionsOpen[source!.id!] = !isBasesOptionsOpen[source!.id!]"
>
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton>
@ -652,25 +663,26 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
maxHeight: '70vh',
overflow: 'overlay',
}"
@click="isBasesOptionsOpen[base!.id!] = false"
@click="isBasesOptionsOpen[source!.id!] = false"
>
<!-- ERD View -->
<NcMenuItem key="erd" @click="openErdView(base)">
<NcMenuItem key="erd" v-e="['c:source:erd']" @click="openErdView(source)">
<GeneralIcon icon="erd" />
Relations
{{ $t('title.relations') }}
</NcMenuItem>
<DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:project="project" :base="base" />
<DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:base="base" :source="source" />
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-e="['c:source:add-table']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn"
@click.stop="openTableCreateDialog(baseIndex)"
@click.stop="openTableCreateDialog(sourceIndex)"
>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
@ -679,10 +691,10 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</template>
<div
ref="menuRefs"
:key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`"
:nc-base="base.id"
:key="`sortable-${source.id}-${source.id && source.id in keys ? keys[source.id] : '0'}`"
:nc-source="source.id"
>
<DashboardTreeViewTableList :project="project" :base-index="baseIndex" />
<DashboardTreeViewTableList :base="base" :source-index="sourceIndex" />
</div>
</a-collapse-panel>
</a-collapse>
@ -695,13 +707,17 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
<template v-if="!isSharedBase" #overlay>
<NcMenu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'project' && project.type === 'database'"></template>
<template v-if="contextMenuTarget.type === 'base' && base.type === 'database'"></template>
<template v-else-if="contextMenuTarget.type === 'base'"></template>
<template v-else-if="contextMenuTarget.type === 'source'"></template>
<template v-else-if="contextMenuTarget.type === 'table'">
<NcMenuItem v-if="isUIAllowed('tableRename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<div class="nc-project-option-item">
<NcMenuItem
v-if="isUIAllowed('tableRename')"
v-e="['c:table:rename']"
@click="openRenameTableDialog(contextMenuTarget.value, true)"
>
<div class="nc-base-option-item">
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</div>
@ -709,62 +725,54 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<NcMenuItem
v-if="isUIAllowed('tableDuplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
v-e="['c:table:duplicate']"
@click="duplicateTable(contextMenuTarget.value)"
>
<div class="nc-project-option-item">
<div class="nc-base-option-item">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="isTableDeleteDialogVisible = true">
<div class="nc-project-option-item text-red-600">
<NcMenuItem v-if="isUIAllowed('table-delete')" class="!hover:bg-red-50" @click="tableDelete">
<div class="nc-base-option-item text-red-600">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</template>
<template v-else>
<NcMenuItem @click="reloadTables">
<div class="nc-project-option-item">
{{ $t('general.reload') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<DlgTableDelete
v-if="contextMenuTarget.value?.id && project?.id"
v-if="contextMenuTarget.value?.id && base?.id"
v-model:visible="isTableDeleteDialogVisible"
:table-id="contextMenuTarget.value?.id"
:project-id="project?.id"
/>
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :project-id="project?.id" />
<DlgProjectDuplicate
v-if="selectedProjectToDuplicate"
v-model="isDuplicateDlgOpen"
:project="selectedProjectToDuplicate"
:on-ok="DlgProjectDuplicateOnOk"
:base-id="base?.id"
/>
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
<DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :base-id="activeBaseId" />
<LazyDashboardSettingsErd :source-id="activeBaseId" />
</div>
</GeneralModal>
</template>
<style lang="scss" scoped>
:deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 !pr-0.5 !py-0.75 hover:bg-gray-200 !rounded-md;
@apply !mx-0 !pl-8.75 !xs:(pl-8) !pr-0.5 !py-0.5 hover:bg-gray-200 xs:(hover:bg-gray-50 ) !rounded-md;
}
:deep(.ant-collapse-item) {
@apply h-full;
}
:deep(.ant-collapse-content-box) {
@apply !px-0 !pb-0 !pt-0.25;
}
:deep(.ant-collapse-header:hover .nc-sidebar-base-node-btns) {
:deep(.ant-collapse-header:hover .nc-sidebar-source-node-btns) {
@apply visible;
}
</style>

14
packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue

@ -1,17 +1,17 @@
<script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk'
import type { BaseType } from 'nocodb-sdk'
import { ProjectInj, ProjectRoleInj } from '#imports'
const props = defineProps<{
projectRole: string | string[]
project: ProjectType
baseRole: string | string[]
base: BaseType
}>()
const projectRole = toRef(props, 'projectRole')
const project = toRef(props, 'project')
const baseRole = toRef(props, 'baseRole')
const base = toRef(props, 'base')
provide(ProjectRoleInj, projectRole)
provide(ProjectInj, project)
provide(ProjectRoleInj, baseRole)
provide(ProjectInj, base)
</script>
<template>

57
packages/nc-gui/components/dashboard/TreeView/TableList.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { ProjectType, TableType } from 'nocodb-sdk'
import type { BaseType, TableType } from 'nocodb-sdk'
import { storeToRefs } from 'pinia'
import Sortable from 'sortablejs'
import TableNode from './TableNode.vue'
@ -8,23 +8,23 @@ import { toRef } from '#imports'
const props = withDefaults(
defineProps<{
project: ProjectType
baseIndex?: number
base: BaseType
sourceIndex?: number
}>(),
{
baseIndex: 0,
sourceIndex: 0,
},
)
const project = toRef(props, 'project')
const baseIndex = toRef(props, 'baseIndex')
const base = toRef(props, 'base')
const sourceIndex = toRef(props, 'sourceIndex')
const base = computed(() => project.value?.bases?.[baseIndex.value])
const source = computed(() => base.value?.sources?.[sourceIndex.value])
const { isMobileMode } = useGlobal()
const { projectTables } = storeToRefs(useTablesStore())
const tables = computed(() => projectTables.value.get(project.value.id!) ?? [])
const { baseTables } = storeToRefs(useTablesStore())
const tables = computed(() => baseTables.value.get(base.value.id!) ?? [])
const { $api } = useNuxtApp()
@ -44,14 +44,14 @@ const sortables: Record<string, Sortable> = {}
// todo: replace with vuedraggable
const initSortable = (el: Element) => {
const base_id = el.getAttribute('nc-base')
if (!base_id) return
const source_id = el.getAttribute('nc-source')
if (!source_id) return
if (isMobileMode.value) return
if (sortables[base_id]) sortables[base_id].destroy()
if (sortables[source_id]) sortables[source_id].destroy()
Sortable.create(el as HTMLLIElement, {
onEnd: async (evt) => {
const offset = tables.value.findIndex((table) => table.base_id === base_id)
const offset = tables.value.findIndex((table) => table.source_id === source_id)
const { newIndex = 0, oldIndex = 0 } = evt
@ -87,10 +87,10 @@ const initSortable = (el: Element) => {
tables.value?.splice(newIndex + offset, 0, ...tables.value?.splice(oldIndex + offset, 1))
// force re-render the list
if (keys.value[base_id]) {
keys.value[base_id] = keys.value[base_id] + 1
if (keys.value[source_id]) {
keys.value[source_id] = keys.value[source_id] + 1
} else {
keys.value[base_id] = 1
keys.value[source_id] = 1
}
// update the item order
@ -106,7 +106,7 @@ const initSortable = (el: Element) => {
id: dragEl.dataset.id,
title: dragEl.dataset.title,
type: dragEl.dataset.type,
baseId: dragEl.dataset.baseId,
sourceId: dragEl.dataset.sourceId,
}),
)
},
@ -125,41 +125,40 @@ watchEffect(() => {
})
const availableTables = computed(() => {
return tables.value.filter((table) => table.base_id === project.value?.bases?.[baseIndex.value].id)
return tables.value.filter((table) => table.source_id === base.value?.sources?.[sourceIndex.value].id)
})
</script>
<template>
<div class="border-none sortable-list">
<template v-if="project">
<template v-if="base">
<div
v-if="availableTables.length === 0"
class="py-0.5 text-gray-500"
:class="{
'ml-13.55': baseIndex === 0,
'ml-19.25': baseIndex !== 0,
'ml-13.55': sourceIndex === 0,
'ml-19.25': sourceIndex !== 0,
}"
>
Empty
{{ $t('general.empty') }}
</div>
<div
v-if="project.bases?.[baseIndex] && project!.bases[baseIndex].enabled"
v-if="base.sources?.[sourceIndex] && base!.sources[sourceIndex].enabled"
ref="menuRefs"
:key="`sortable-${base?.id}-${base?.id && base?.id in keys ? keys[base?.id] : '0'}`"
:nc-base="base?.id"
:key="`sortable-${source?.id}-${source?.id && source?.id in keys ? keys[source?.id] : '0'}`"
:nc-source="source?.id"
>
<TableNode
v-for="table of availableTables"
:key="table.id"
v-e="['a:table:open']"
class="nc-tree-item text-sm"
:data-order="table.order"
:data-id="table.id"
:table="table"
:project="project"
:base-index="baseIndex"
:base="base"
:source-index="sourceIndex"
:data-title="table.title"
:data-base-id="base?.id"
:data-source-id="source?.id"
:data-type="table.type"
>
</TableNode>

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ProjectType, TableType } from 'nocodb-sdk'
import type { BaseType, TableType } from 'nocodb-sdk'
import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia'
@ -9,19 +9,19 @@ import { ProjectRoleInj, TreeViewInj, useRoles, useTabs } from '#imports'
const props = withDefaults(
defineProps<{
project: ProjectType
base: BaseType
table: TableType
baseIndex: number
sourceIndex: number
}>(),
{ baseIndex: 0 },
{ sourceIndex: 0 },
)
const project = toRef(props, 'project')
const base = toRef(props, 'base')
const table = toRef(props, 'table')
const baseIndex = toRef(props, 'baseIndex')
const sourceIndex = toRef(props, 'sourceIndex')
const { openTable: _openTable } = useTableNew({
projectId: project.value.id!,
baseId: base.value.id!,
})
const route = useRoute()
@ -36,20 +36,21 @@ const { updateTab } = tabStore
const { $e, $api } = useNuxtApp()
useTableNew({
projectId: project.value.id!,
baseId: base.value.id!,
})
const projectRole = inject(ProjectRoleInj)
const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table)
const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)!
const { loadViews: _loadViews } = useViewsStore()
const { activeView } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
// todo: temp
const { projectTables } = storeToRefs(useTablesStore())
const tables = computed(() => projectTables.value.get(project.value.id!) ?? [])
const { baseTables } = storeToRefs(useTablesStore())
const tables = computed(() => baseTables.value.get(base.value.id!) ?? [])
const openedTableId = computed(() => route.params.viewId)
@ -77,11 +78,11 @@ const setIcon = async (icon: string, table: TableType) => {
// Todo: temp
const { isSharedBase } = useProject()
// const isMultiBase = computed(() => project.bases && project.bases.length > 1)
const { isSharedBase } = useBase()
// const isMultiBase = computed(() => base.sources && base.sources.length > 1)
const canUserEditEmote = computed(() => {
return isUIAllowed('tableIconEdit', { roles: projectRole?.value })
return isUIAllowed('tableIconEdit', { roles: baseRole?.value })
})
const isExpanded = ref(false)
@ -108,6 +109,10 @@ const onOpenTable = async () => {
isLoading.value = true
try {
await _openTable(table.value)
if (isMobileMode.value) {
isLeftSidebarOpen.value = false
}
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
@ -141,34 +146,43 @@ const isTableOpened = computed(() => {
:data-order="table.order"
:data-id="table.id"
:data-table-id="table.id"
:class="[`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`]"
:class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title}`]"
:data-active="openedTableId === table.id"
>
<GeneralTooltip
class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-12': baseIndex !== 0,
'pl-6.5': baseIndex === 0,
'pl-12 xs:(pl-14)': sourceIndex !== 0,
'pl-6.5': sourceIndex === 0,
'!bg-primary-selected': isTableOpened,
}"
modifier-key="Alt"
>
<template #title>{{ table.table_name }}</template>
<div
v-e="['a:table:open']"
class="table-context flex items-center gap-1 h-full"
:data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="onOpenTable"
>
<div class="flex flex-row h-full items-center">
<NcButton type="text" size="xxsmall" class="nc-sidebar-node-btn nc-sidebar-expand" @click.stop="onExpand">
<NcButton
v-if="(table.meta as any)?.hasNonDefaultViews"
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
@click.stop="onExpand"
>
<GeneralIcon
icon="triangleFill"
class="nc-sidebar-base-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
class="nc-sidebar-source-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
<div v-else class="sm:min-w-5.75 xs:min-w-7.5 h-2"></div>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
class="flex items-center nc-table-icon"
@ -179,30 +193,33 @@ const isTableOpened = computed(() => {
>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
v-e="['c:table:emoji-picker']"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote"
:readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)"
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ 'Change icon' }}
{{ $t('general.changeIcon') }}
</template>
<MdiTable
<component
:is="iconMap.table"
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
@ -224,13 +241,13 @@ const isTableOpened = computed(() => {
{{ table.title }}
</span>
<div class="flex flex-grow h-full"></div>
<div class="flex flex-row items-center">
<NcDropdown
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole }))
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
v-e="['c:table:option']"
:trigger="['click']"
class="nc-sidebar-node-btn"
@click.stop
@ -243,9 +260,10 @@ const isTableOpened = computed(() => {
<template #overlay>
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: projectRole })"
v-if="isUIAllowed('tableRename', { roles: baseRole })"
v-e="['c:table:rename']"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
@ -254,9 +272,10 @@ const isTableOpened = computed(() => {
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
project.bases?.[baseIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local)
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
"
v-e="['c:table:duplicate']"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
@ -265,7 +284,8 @@ const isTableOpened = computed(() => {
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: projectRole })"
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
v-e="['c:table:delete']"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
@ -278,6 +298,7 @@ const isTableOpened = computed(() => {
</NcDropdown>
<DashboardTreeViewCreateViewBtn v-if="isUIAllowed('viewCreateOrEdit')">
<NcButton
v-e="['c:view:create']"
type="text"
size="xxsmall"
class="nc-create-view-btn nc-sidebar-node-btn"
@ -291,13 +312,13 @@ const isTableOpened = computed(() => {
</div>
</div>
<DlgTableDelete
v-if="table.id && project?.id"
v-if="table.id && base?.id"
v-model:visible="isTableDeleteDialogVisible"
:table-id="table.id"
:project-id="project.id"
:base-id="base.id"
/>
</GeneralTooltip>
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :project-id="project.id" />
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" />
</div>
</template>

66
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -5,7 +5,6 @@ import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import {
isDefaultBase as _isDefaultBase,
extractSdkResponseErrorMsg,
message,
onMounted,
@ -16,7 +15,6 @@ import {
useCommandPalette,
useDialog,
useNuxtApp,
useRouter,
useUndoRedo,
viewTypeAlias,
watch,
@ -29,21 +27,18 @@ interface Emits {
}
const emits = defineEmits<Emits>()
const project = inject(ProjectInj)!
const base = inject(ProjectInj)!
const table = inject(SidebarTableInj)!
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { isMobileMode } = useGlobal()
const { $e } = useNuxtApp()
const isDefaultBase = computed(() => {
const base = project.value?.bases?.find((b) => b.id === table.value.base_id)
if (!base) return false
return _isDefaultBase(base)
})
const { t } = useI18n()
const { viewsByTable, activeView } = storeToRefs(useViewsStore())
const { viewsByTable, activeView, allRecentViews } = storeToRefs(useViewsStore())
const { navigateToTable } = useTablesStore()
@ -55,7 +50,7 @@ const { refreshCommandPalette } = useCommandPalette()
const { addUndo, defineModelScope } = useUndoRedo()
const { navigateToView, loadViews } = useViewsStore()
const { navigateToView, loadViews, removeFromRecentViews } = useViewsStore()
/** Selected view(s) for menu */
const selected = ref<string[]>([])
@ -85,11 +80,11 @@ function markItem(id: string) {
/** validate view title */
function validate(view: ViewType) {
if (!view.title || view.title.trim().length < 0) {
return 'View name is required'
return t('msg.error.viewNameRequired')
}
if (views.value.some((v) => v.title === view.title && v.id !== view.id)) {
return 'View name should be unique'
return t('msg.error.viewNameDuplicate')
}
return true
@ -191,13 +186,18 @@ const initSortable = (el: HTMLElement) => {
onMounted(() => menuRef.value && initSortable(menuRef.value.$el))
/** Navigate to view by changing url param */
function changeView(view: ViewType) {
navigateToView({
async function changeView(view: ViewType) {
await navigateToView({
view,
tableId: table.value.id!,
projectId: project.value.id!,
baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id,
doNotSwitchTab: true,
})
if (isMobileMode.value) {
isLeftSidebarOpen.value = false
}
}
/** Rename a view */
@ -211,7 +211,7 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) {
navigateToView({
view,
tableId: table.value.id!,
projectId: project.value.id!,
baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id,
})
@ -238,6 +238,13 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) {
scope: defineModelScope({ view: activeView.value }),
})
}
// update view name in recent views
allRecentViews.value = allRecentViews.value.map((rv) => {
if (rv.viewId === view.id && rv.tableID === view.fk_model_id) {
rv.viewName = view.title
}
return rv
})
// View renamed successfully
// message.success(t('msg.success.viewRenamed'))
@ -259,11 +266,12 @@ function openDeleteDialog(view: ViewType) {
emits('deleted')
removeFromRecentViews({ viewId: view.id, tableId: view.fk_model_id, baseId: base.value.id })
refreshCommandPalette()
if (activeView.value?.id === view.id) {
navigateToTable({
tableId: table.value.id!,
projectId: project.value.id!,
baseId: base.value.id!,
})
}
@ -271,6 +279,13 @@ function openDeleteDialog(view: ViewType) {
tableId: table.value.id!,
force: true,
})
const activeNonDefaultViews = viewsByTable.value.get(table.value.id!)?.filter((v) => !v.is_default) ?? []
table.value.meta = {
...(table.value.meta as object),
hasNonDefaultViews: activeNonDefaultViews.length > 1,
}
},
})
@ -328,12 +343,13 @@ function onOpenModal({
await loadViews({
force: true,
tableId: table.value.id!,
})
navigateToView({
view,
tableId: table.value.id!,
projectId: project.value.id!,
baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id,
})
@ -350,18 +366,8 @@ function onOpenModal({
</script>
<template>
<div
v-if="!views.length"
class="text-gray-500 my-1.75"
:class="{
'ml-19.25': isDefaultBase,
'ml-24.75': !isDefaultBase,
}"
>
No Views
</div>
<a-menu
v-if="views.length"
ref="menuRef"
:class="{ dragging }"
class="nc-views-menu flex flex-col w-full !border-r-0 !bg-inherit"

58
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -41,23 +41,21 @@ const vModel = useVModel(props, 'view', emits) as WritableComputedRef<ViewType &
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal()
const { activeViewTitleOrId } = storeToRefs(useViewsStore())
const { isUIAllowed } = useRoles()
const project = inject(ProjectInj, ref())
const base = inject(ProjectInj, ref())
const activeView = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false))
const { rightSidebarState } = storeToRefs(useSidebarStore())
const isDefaultBase = computed(() => {
const base = project.value?.bases?.find((b) => b.id === vModel.value.base_id)
if (!base) return false
const source = base.value?.sources?.find((b) => b.id === vModel.value.source_id)
if (!source) return false
return _isDefaultBase(base)
return _isDefaultBase(source)
})
const isDropdownOpen = ref(false)
@ -80,6 +78,7 @@ const onClick = useDebounceFn(() => {
/** Enable editing view name on dbl click */
function onDblClick() {
if (isMobileMode.value) return
if (!isUIAllowed('viewCreateOrEdit')) return
if (!isEditing.value) {
@ -190,47 +189,29 @@ function onStopEdit() {
isStopped.value = false
}, 250)
}
watch(rightSidebarState, () => {
if (rightSidebarState.value === 'peekCloseEnd') {
isDropdownOpen.value = false
}
})
function onRef(el: HTMLElement) {
if (activeViewTitleOrId.value === vModel.value.id) {
nextTick(() => {
setTimeout(() => {
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}, 1000)
})
}
}
</script>
<template>
<a-menu-item
v-e="['c:view:open']"
class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
:class="{
'!pl-18': isDefaultBase,
'!pl-23.5': !isDefaultBase,
'!pl-18 !xs:(pl-19.75)': isDefaultBase,
'!pl-23.5 !xs:(pl-27)': !isDefaultBase,
}"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick"
@click="onClick"
>
<div
:ref="onRef"
v-e="['a:view:open', { view: vModel.type }]"
class="text-sm flex items-center w-full gap-1"
data-testid="view-item"
>
<div v-e="['a:view:open', { view: vModel.type }]" class="text-sm flex items-center w-full gap-1" data-testid="view-item">
<div class="flex min-w-6" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<LazyGeneralEmojiPicker
v-e="['c:view:emoji-picker']"
class="nc-table-icon"
:emoji="props.view?.meta?.icon"
size="small"
:clearable="true"
:readonly="isMobileMode"
@emoji-selected="emits('selectIcon', $event)"
>
<template #default>
@ -268,6 +249,7 @@ function onRef(el: HTMLElement) {
<template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
<NcButton
v-e="['c:view:option']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn invisible !group-hover:visible nc-sidebar-view-node-context-btn"
@ -281,21 +263,21 @@ function onRef(el: HTMLElement) {
<template #overlay>
<NcMenu class="min-w-27" :data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`">
<NcMenuItem @click.stop="onDblClick">
<NcMenuItem v-e="['c:view:rename']" @click.stop="onDblClick">
<GeneralIcon icon="edit" />
<div class="-ml-0.25">Rename</div>
<div class="-ml-0.25">{{ $t('general.rename') }}</div>
</NcMenuItem>
<NcMenuItem @click.stop="onDuplicate">
<NcMenuItem v-e="['c:view:duplicate']" @click.stop="onDuplicate">
<GeneralIcon icon="duplicate" class="nc-view-copy-icon" />
Duplicate
{{ $t('general.duplicate') }}
</NcMenuItem>
<NcDivider />
<template v-if="!vModel.is_default">
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click.stop="onDelete">
<NcMenuItem v-e="['c:view:delete']" class="!text-red-500 !hover:bg-red-50" @click.stop="onDelete">
<GeneralIcon icon="delete" class="text-sm nc-view-delete-icon" />
<div class="-ml-0.25">Delete</div>
<div class="-ml-0.25">{{ $t('general.delete') }}</div>
</NcMenuItem>
</template>
</NcMenu>

97
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -1,11 +1,8 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import ProjectWrapper from './ProjectWrapper.vue'
import type { TabType } from '#imports'
import {
TreeViewInj,
computed,
@ -16,67 +13,58 @@ import {
ref,
resolveComponent,
storeToRefs,
useBase,
useBases,
useDialog,
useNuxtApp,
useProject,
useProjects,
useRoles,
useTablesStore,
useTabs,
} from '#imports'
import { useRouter } from '#app'
const { isUIAllowed } = useRoles()
const { addTab } = useTabs()
const { $e, $jobs } = useNuxtApp()
const { $e } = useNuxtApp()
const router = useRouter()
const route = router.currentRoute
const projectsStore = useProjects()
const basesStore = useBases()
const { createProject: _createProject } = projectsStore
const { createProject: _createProject } = basesStore
const { projects, projectsList, activeProjectId } = storeToRefs(projectsStore)
const { bases, basesList, activeProjectId } = storeToRefs(basesStore)
const { isWorkspaceLoading } = storeToRefs(useWorkspace())
const { openTable } = useTablesStore()
const projectCreateDlg = ref(false)
const projectStore = useProject()
const baseCreateDlg = ref(false)
const { loadTables } = projectStore
const baseStore = useBase()
const { tables } = storeToRefs(projectStore)
const { isSharedBase } = storeToRefs(baseStore)
const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const { refreshCommandPalette } = useCommandPalette()
const contextMenuTarget = reactive<{ type?: 'base' | 'source' | 'table' | 'main' | 'layout'; value?: any }>({})
const contextMenuTarget = reactive<{ type?: 'project' | 'base' | 'table' | 'main' | 'layout'; value?: any }>({})
const setMenuContext = (type: 'project' | 'base' | 'table' | 'main' | 'layout', value?: any) => {
const setMenuContext = (type: 'base' | 'source' | 'table' | 'main' | 'layout', value?: any) => {
contextMenuTarget.type = type
contextMenuTarget.value = value
}
function openRenameTableDialog(table: TableType, rightClick = false) {
if (!table || !table.base_id) return
function openRenameTableDialog(table: TableType, _ = false) {
if (!table || !table.source_id) return
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
$e('c:table:rename')
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen,
'tableMeta': table,
'baseId': table.base_id, // || bases.value[0].id,
'sourceId': table.source_id, // || sources.value[0].id,
'onUpdate:modelValue': closeDialog,
})
@ -87,8 +75,8 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
}
}
function openTableCreateDialog(baseId?: string, projectId?: string) {
if (!baseId && !(projectId || projectsList.value[0].id)) return
function openTableCreateDialog(sourceId?: string, baseId?: string) {
if (!sourceId && !(baseId || basesList.value[0].id)) return
$e('c:table:create:navdraw')
@ -96,8 +84,8 @@ function openTableCreateDialog(baseId?: string, projectId?: string) {
const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen,
'baseId': baseId, // || bases.value[0].id,
'projectId': projectId || projectsList.value[0].id,
'sourceId': sourceId, // || sources.value[0].id,
'baseId': baseId || basesList.value[0].id,
'onUpdate:modelValue': closeDialog,
})
@ -109,30 +97,15 @@ function openTableCreateDialog(baseId?: string, projectId?: string) {
}
const duplicateTable = async (table: TableType) => {
if (!table || !table.id || !table.project_id) return
if (!table || !table.id || !table.base_id) return
const isOpen = ref(true)
$e('c:table:duplicate')
const { close } = useDialog(resolveComponent('DlgTableDuplicate'), {
'modelValue': isOpen,
'table': table,
'onOk': async (jobData: { id: string }) => {
$jobs.subscribe({ id: jobData.id }, undefined, async (status: string, data?: any) => {
if (status === JobStatus.COMPLETED) {
await loadTables()
refreshCommandPalette()
const newTable = tables.value.find((el) => el.id === data?.result?.id)
if (newTable) addTab({ title: newTable.title, id: newTable.id, type: newTable.type as TabType })
openTable(newTable!)
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate table')
await loadTables()
}
})
$e('a:table:duplicate')
},
'onUpdate:modelValue': closeDialog,
})
@ -163,17 +136,17 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
// prevent the key `T` is inputted to table title input
e.preventDefault()
$e('c:shortcut', { key: 'ALT + T' })
const projectId = activeProjectId.value
const project = projectId ? projects.value.get(projectId) : undefined
if (!project) return
const baseId = activeProjectId.value
const base = baseId ? bases.value.get(baseId) : undefined
if (!base) return
if (projectId) openTableCreateDialog(project.bases?.[0].id, projectId)
if (baseId) openTableCreateDialog(base.sources?.[0].id, baseId)
}
break
}
// ALT + L - only show active project
// ALT + L - only show active base
case 76: {
if (route.value.params.projectId) {
if (route.value.params.baseId) {
router.push({
query: {
...route.value.query,
@ -186,7 +159,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
// ALT + D
case 68: {
e.stopPropagation()
projectCreateDlg.value = true
baseCreateDlg.value = true
break
}
}
@ -194,7 +167,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
})
const handleContext = (e: MouseEvent) => {
if (!document.querySelector('.base-context, .table-context')?.contains(e.target as Node)) {
if (!document.querySelector('.source-context, .table-context')?.contains(e.target as Node)) {
setMenuContext('main')
}
}
@ -234,7 +207,7 @@ watch(
watch(
activeProjectId,
() => {
const activeProjectDom = document.querySelector(`.nc-treeview [data-project-id="${activeProjectId.value}"]`)
const activeProjectDom = document.querySelector(`.nc-treeview [data-base-id="${activeProjectId.value}"]`)
if (!activeProjectDom) return
if (isElementInvisible(activeProjectDom)) {
@ -250,17 +223,17 @@ watch(
<template>
<div class="nc-treeview-container flex flex-col justify-between select-none">
<div class="text-gray-500 font-medium pl-3.5 mb-1">{{ $t('objects.projects') }}</div>
<div v-if="!isSharedBase" class="text-gray-500 font-medium pl-3.5 mb-1">{{ $t('objects.projects') }}</div>
<div mode="inline" class="nc-treeview pb-0.5 flex-grow min-h-50 overflow-x-hidden">
<template v-if="projectsList?.length">
<ProjectWrapper v-for="project of projectsList" :key="project.id" :project-role="project.project_role" :project="project">
<template v-if="basesList?.length">
<ProjectWrapper v-for="base of basesList" :key="base.id" :base-role="base.project_role" :base="base">
<DashboardTreeViewProjectNode />
</ProjectWrapper>
</template>
<WorkspaceEmptyPlaceholder v-else-if="!isWorkspaceLoading" />
</div>
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" />
<WorkspaceCreateProjectDlg v-model="baseCreateDlg" />
</div>
</template>

23
packages/nc-gui/components/dashboard/View.vue

@ -12,6 +12,7 @@ const {
leftSidebarWidthPercent,
leftSideBarSize: sideBarSize,
leftSidebarState: sidebarState,
mobileNormalizedSidebarSize,
} = storeToRefs(useSidebarStore())
const wrapperRef = ref<HTMLDivElement>()
@ -31,14 +32,6 @@ const { handleSidebarOpenOnMobileForNonViews } = useConfigStore()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const mobileNormalizedSidebarSize = computed(() => {
if (isMobileMode.value) {
return isLeftSidebarOpen.value ? 100 : 0
}
return currentSidebarSize.value
})
const mobileNormalizedContentSize = computed(() => {
if (isMobileMode.value) {
return isLeftSidebarOpen.value ? 0 : 100
@ -64,12 +57,11 @@ watch(isLeftSidebarOpen, () => {
setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration)
} else {
sideBarSize.value.old = sideBarSize.value.current
sideBarSize.value.current = 0
sidebarState.value = 'hiddenStart'
setTimeout(() => {
sideBarSize.value.current = 0
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
@ -119,6 +111,14 @@ watch(isMobileMode, () => {
isLeftSidebarOpen.value = !isMobileMode.value
})
watch(sidebarState, () => {
if (sidebarState.value === 'peekCloseEnd') {
setTimeout(() => {
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
onMounted(() => {
handleSidebarOpenOnMobileForNonViews()
})
@ -140,7 +140,7 @@ onMounted(() => {
>
<div
ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-12 absolute overflow-visible"
:class="{
'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen,
@ -179,6 +179,7 @@ onMounted(() => {
> * {
@apply opacity-0;
z-index: -1 !important;
transform: translateX(-100%);
}
}

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

@ -102,38 +102,38 @@ onMounted(async () => {
{{ `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>
<NcButton type="secondary" @click="showPluginUninstallModal = false"> {{ $t('general.cancel') }} </NcButton>
<NcButton type="danger" @click="resetPlugin"> {{ $t('general.confirm') }} </NcButton>
</div>
</div>
</a-modal>
<div class="grid grid-cols-2 gap-x-2 gap-y-4 mt-4">
<div class="flex flex-wrap mt-4 w-full gap-5 mb-10">
<a-card
v-for="(app, i) in apps"
:key="i"
class="sm:w-100 md:w-138.1"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
:body-style="{ width: '100%' }"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
Edit
{{ $t('general.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">
<component :is="iconMap.closeCircle" />
<div class="flex ml-0.5">Reset</div>
<div class="flex ml-0.5">{{ $t('general.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">
<component :is="iconMap.plus" />
Install
{{ $t('general.install') }}
</div>
</a-button>
</div>

10
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -1,14 +1,14 @@
<script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, timeAgo, useGlobal, useI18n, useNuxtApp, useProject } from '#imports'
import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, timeAgo, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $api } = useNuxtApp()
const { project } = storeToRefs(useProject())
const { base } = storeToRefs(useBase())
const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId.value ?? project.value?.id)
const baseId = computed(() => _projectId.value ?? base.value?.id)
const { t } = useI18n()
@ -26,11 +26,11 @@ const { appInfo } = useGlobal()
async function loadAudits(page = currentPage.value, limit = currentLimit.value) {
try {
if (!project.value?.id) return
if (!base.value?.id) return
isLoading.value = true
const { list, pageInfo } = await $api.project.auditList(projectId.value, {
const { list, pageInfo } = await $api.base.auditList(baseId.value, {
offset: limit * (page - 1),
limit,
})

16
packages/nc-gui/components/dashboard/settings/BaseAudit.vue

@ -1,17 +1,17 @@
<script setup lang="ts">
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { h, iconMap, onMounted, storeToRefs, timeAgo, useGlobal, useI18n, useNuxtApp, useProject } from '#imports'
import { h, iconMap, onMounted, storeToRefs, timeAgo, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
interface Props {
baseId: string
sourceId: string
}
const props = defineProps<Props>()
const projectStore = useProject()
const baseStore = useBase()
const { project } = storeToRefs(projectStore)
const { base } = storeToRefs(baseStore)
const { $api } = useNuxtApp()
@ -31,14 +31,14 @@ const { appInfo } = useGlobal()
async function loadAudits(page = currentPage.value, limit = currentLimit.value) {
try {
if (!props.baseId) return
if (!props.sourceId) return
isLoading.value = true
const { list, pageInfo } = await $api.project.auditList(project.value.id!, {
const { list, pageInfo } = await $api.base.auditList(base.value.id!, {
offset: limit * (page - 1),
limit,
baseId: props.baseId,
sourceId: props.sourceId,
})
audits.value = list
@ -106,7 +106,7 @@ const columns = [
<div class="flex flex-col gap-4 w-full">
<div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div>
<div class="flex flex-row justify-between items-center">
<h6 class="mb-4 first-letter:capital font-bold">Audit : {{ project.title }}</h6>
<h6 class="mb-4 first-letter:capital font-bold">Audit : {{ base.title }}</h6>
<a-button class="self-start !rounded-md" @click="loadAudits">
<!-- Reload -->
<div class="flex items-center gap-2 text-gray-600 font-light">

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

@ -1,7 +1,7 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import type { BaseType } from 'nocodb-sdk'
import { ClientType, DataSourcesSubTab, storeToRefs, useCommandPalette, useNuxtApp, useProject } from '#imports'
import type { SourceType } from 'nocodb-sdk'
import { ClientType, DataSourcesSubTab, storeToRefs, useBase, useCommandPalette, useNuxtApp } from '#imports'
interface Props {
state: string
@ -18,19 +18,21 @@ const vReload = useVModel(props, 'reload', emits)
const { $api, $e } = useNuxtApp()
const { loadProject } = useProjects()
const { t } = useI18n()
const projectStore = useProject()
const { project } = storeToRefs(projectStore)
const { loadProject } = useBases()
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
const { projectPageTab } = storeToRefs(useConfigStore())
const { refreshCommandPalette } = useCommandPalette()
const sources = ref<BaseType[]>([])
const sources = ref<SourceType[]>([])
const activeBaseId = ref('')
const metadiffbases = ref<string[]>([])
const clientType = ref<ClientType>(ClientType.MYSQL)
const isReloading = ref(false)
@ -40,21 +42,19 @@ const forceAwakened = ref(false)
const dataSourcesAwakened = ref(false)
const isDeleteBaseModalOpen = ref(false)
const toBeDeletedBase = ref<BaseType | undefined>()
const toBeDeletedBase = ref<SourceType | undefined>()
async function loadBases(changed?: boolean) {
try {
if (changed) refreshCommandPalette()
await until(() => !!project.value.id).toBeTruthy()
await until(() => !!base.value.id).toBeTruthy()
isReloading.value = true
vReload.value = true
const baseList = await $api.base.list(project.value.id as string)
const baseList = await $api.source.list(base.value.id as string)
if (baseList.list && baseList.list.length) {
sources.value = baseList.list
}
await loadMetaDiff()
} catch (e) {
console.error(e)
} finally {
@ -63,63 +63,52 @@ async function loadBases(changed?: boolean) {
}
}
async function loadMetaDiff() {
try {
metadiffbases.value = []
const metadiff = await $api.project.metaDiffGet(project.value.id as string)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
metadiffbases.value.push(model.base_id)
}
}
} catch (e) {
console.error(e)
}
}
const baseAction = (baseId?: string, action?: string) => {
if (!baseId) return
activeBaseId.value = baseId
const baseAction = (sourceId?: string, action?: string) => {
if (!sourceId) return
activeBaseId.value = sourceId
vState.value = action || ''
}
const openDeleteBase = (base: BaseType) => {
$e('c:base:delete')
const openDeleteBase = (source: SourceType) => {
$e('c:source:delete')
isDeleteBaseModalOpen.value = true
toBeDeletedBase.value = base
toBeDeletedBase.value = source
}
const deleteBase = async () => {
if (!toBeDeletedBase.value) return
try {
await $api.base.delete(toBeDeletedBase.value.project_id as string, toBeDeletedBase.value.id as string)
await $api.source.delete(toBeDeletedBase.value.base_id as string, toBeDeletedBase.value.id as string)
$e('a:base:delete')
$e('a:source:delete')
sources.value.splice(sources.value.indexOf(toBeDeletedBase.value), 1)
await loadProject(project.value.id as string, true)
await loadProject(base.value.id as string, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
// TODO @mertmit
refreshCommandPalette()
}
}
const toggleBase = async (base: BaseType, state: boolean) => {
const toggleBase = async (source: BaseType, state: boolean) => {
try {
if (!state && sources.value.filter((src) => src.enabled).length < 2) {
message.info('There should be at least one enabled base!')
message.info('There should be at least one enabled source!')
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,
source.enabled = state
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
enabled: source.enabled,
})
await loadProject(project.value.id as string, true)
await loadProject(base.value.id as string, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
refreshCommandPalette()
}
}
@ -127,27 +116,29 @@ 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.value[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,
const source = sources.value[e.newIndex]
if (source) {
if (!source.order) {
// empty update call to reorder sources (migration)
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
})
message.info('Bases are migrated. Please try again.')
message.info(t('info.basesMigrated'))
} else {
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
order: e.newIndex + 1,
})
}
}
await loadProject(project.value.id as string, true)
await loadProject(base.value.id as string, true)
await loadBases()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
await refreshCommandPalette()
}
}
@ -157,11 +148,17 @@ const forceAwaken = () => {
emits('awaken', forceAwakened.value)
}
onMounted(async () => {
if (sources.value.length === 0) {
loadBases()
}
})
watch(
projectPageTab,
() => {
if (projectPageTab.value === 'data-source') {
loadBases()
}
},
{
immediate: true,
},
)
watch(
() => props.reload,
@ -219,6 +216,7 @@ watch(
}
break
}
refreshCommandPalette()
},
{ immediate: true },
)
@ -302,7 +300,7 @@ const isEditBaseModalOpen = computed({
>
<div class="flex flex-row items-center w-full gap-x-1">
<component :is="iconMap.plus" />
<div class="flex">New Source</div>
<div class="flex">{{ $t('activity.newSource') }}</div>
</div>
</NcButton>
</div>
@ -314,10 +312,10 @@ const isEditBaseModalOpen = computed({
>
<div class="ds-table-head">
<div class="ds-table-row">
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">Visibility</div>
<div class="ds-table-col ds-table-name">Name</div>
<div class="ds-table-col ds-table-type">Type</div>
<div class="ds-table-col ds-table-actions pl-2">Actions</div>
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">{{ $t('general.visibility') }}</div>
<div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div>
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div>
<div class="ds-table-col ds-table-actions -ml-13">{{ $t('labels.actions') }}</div>
<div class="ds-table-col ds-table-crud"></div>
</div>
</div>
@ -329,8 +327,8 @@ const isEditBaseModalOpen = computed({
<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 v-if="sources[0].enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch
:checked="sources[0].enabled ? true : false"
@ -343,7 +341,7 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-name font-medium">
<div class="flex items-center gap-1">
<!-- <GeneralBaseLogo :base-type="sources[0].type" /> -->
Default
{{ $t('general.default') }}
</div>
</div>
@ -353,198 +351,240 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<a-button
v-if="!sources[0].is_meta && !sources[0].is_local"
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<a-tooltip v-if="metadiffbases.includes(sources[0].id)">
<template #title>Out of sync</template>
<GeneralIcon icon="warning" class="group-hover:text-accent text-primary" />
</a-tooltip>
<GeneralIcon v-else icon="sync" class="group-hover:text-accent" />
Sync Metadata
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
Relations
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
UI ACL
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Audit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
Audit
</div>
</a-button>
<NcTooltip v-if="!sources[0].is_meta && !sources[0].is_local">
<template #title>
{{ $t('tooltip.metaSync') }}
</template>
<NcButton
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-meta-sync"
size="small"
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.relations') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-erd"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-ui-acl"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.audit') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-audit"
@click="baseAction(sources[0].id, DataSourcesSubTab.Audit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
</div>
</div>
<div class="ds-table-col ds-table-crud">
<a-button
<NcButton
v-if="!sources[0].is_meta && !sources[0].is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</a-button>
</NcButton>
</div>
</div>
</template>
<template #item="{ element: base, index }">
<template #item="{ element: source, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200">
<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 v-if="source.enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch :checked="base.enabled ? true : false" @change="toggleBase(base, $event)" />
<a-switch :checked="source.enabled ? true : false" @change="toggleBase(source, $event)" />
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium">
<div class="ds-table-col ds-table-name font-medium w-full">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div v-if="base.is_meta || base.is_local">-</div>
<div v-else class="flex items-center gap-1">
{{ base.is_meta || base.is_local ? 'BASE' : base.alias }}
</div>
<div v-if="source.is_meta || source.is_local">-</div>
<span v-else class="truncate">
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</span>
</div>
<div class="ds-table-col ds-table-type">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div class="flex items-center gap-2">
<GeneralBaseLogo :base-type="base.type" />
<span class="text-gray-700 capitalize">{{ base.type }}</span>
<GeneralBaseLogo :source-type="source.type" />
<span class="text-gray-700 capitalize">{{ source.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"
type="text"
@click="baseAction(base.id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
Relations
</div>
</a-button>
<a-button
<NcTooltip>
<template #title>
{{ $t('title.relations') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-erd"
@click="baseAction(source.id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }}
</template>
<NcButton
size="small"
type="text"
class="nc-action-btn cursor-pointer outline-0"
data-testid="nc-data-sources-view-ui-acl"
@click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('tooltip.metaSync') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
type="text"
data-testid="nc-data-sources-view-meta-sync"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(source.id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
</div>
</div>
<div class="ds-table-col ds-table-crud justify-end gap-x-1">
<NcTooltip>
<template #title>
{{ $t('general.edit') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.UIAcl)"
@click="baseAction(source.id, DataSourcesSubTab.Edit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
UI ACL
</div>
</a-button>
<a-button
v-if="!base.is_meta && !base.is_local"
<GeneralIcon icon="edit" class="text-gray-600" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('general.delete') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.Metadata)"
@click="openDeleteBase(source)"
>
<div class="flex items-center gap-2 text-gray-600">
<a-tooltip v-if="metadiffbases.includes(base.id)">
<template #title>Out of sync</template>
<GeneralIcon icon="warning" class="group-hover:text-accent text-primary" />
</a-tooltip>
<GeneralIcon v-else icon="sync" class="group-hover:text-accent" />
Sync Metadata
</div>
</a-button>
</div>
</div>
<div class="ds-table-col ds-table-crud justify-end gap-x-1">
<a-button
v-if="!base.is_meta && !base.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="baseAction(base.id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600 -mt-0.5" />
</a-button>
<a-button
v-if="!base.is_meta && !base.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="openDeleteBase(base)"
>
<GeneralIcon icon="delete" class="text-red-500 -mt-0.5" />
</a-button>
<GeneralIcon icon="delete" class="text-red-500" />
</NcButton>
</NcTooltip>
</div>
</div>
</template>
</Draggable>
</div>
</div>
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium">
<div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase
:connection-type="clientType"
@base-created="loadBases(true)"
@close="isNewBaseModalOpen = false"
/>
</div>
</GeneralModal>
<LazyDashboardSettingsDataSourcesCreateBase
v-model:open="isNewBaseModalOpen"
:connection-type="clientType"
@source-created="loadBases(true)"
/>
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :base-id="activeBaseId" />
<LazyDashboardSettingsErd :source-id="activeBaseId" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isMetaDataModal" size="medium">
<div class="p-6">
<LazyDashboardSettingsMetadata :base-id="activeBaseId" @base-synced="loadBases(true)" />
<LazyDashboardSettingsMetadata :source-id="activeBaseId" @source-synced="loadBases(true)" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isUIAclModalOpen" class="!w-[60rem]">
<div class="p-6">
<LazyDashboardSettingsUIAcl :base-id="activeBaseId" />
<LazyDashboardSettingsUIAcl :source-id="activeBaseId" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isEditBaseModalOpen" size="medium">
<GeneralModal v-model:visible="isEditBaseModalOpen" closable :mask-closable="false" size="medium">
<div class="p-6">
<LazyDashboardSettingsDataSourcesEditBase
:base-id="activeBaseId"
@base-updated="loadBases(true)"
:source-id="activeBaseId"
@source-updated="loadBases(true)"
@close="isEditBaseModalOpen = false"
/>
</div>
</GeneralModal>
<GeneralModal v-model:visible="isBaseAuditModalOpen" class="!w-[70rem]">
<div class="p-6">
<LazyDashboardSettingsBaseAudit :base-id="activeBaseId" @close="isBaseAuditModalOpen = false" />
<LazyDashboardSettingsBaseAudit :source-id="activeBaseId" @close="isBaseAuditModalOpen = false" />
</div>
</GeneralModal>
<GeneralDeleteModal v-model:visible="isDeleteBaseModalOpen" entity-name="base" :on-delete="deleteBase">
<GeneralDeleteModal v-model:visible="isDeleteBaseModalOpen" :entity-name="$t('general.datasource')" :on-delete="deleteBase">
<template #entity-preview>
<div v-if="toBeDeletedBase" class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralBaseLogo :base-type="toBeDeletedBase.type" />
<GeneralBaseLogo :source-type="toBeDeletedBase.type" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
@ -560,7 +600,7 @@ const isEditBaseModalOpen = computed({
<style>
.ds-table-head {
@apply flex items-center border-0 text-gray-400;
@apply flex items-center border-0 text-gray-500;
}
.ds-table-body {
@ -580,15 +620,15 @@ const isEditBaseModalOpen = computed({
}
.ds-table-name {
@apply col-span-6 items-center capitalize;
@apply col-span-9 items-center capitalize;
}
.ds-table-type {
@apply col-span-3 items-center;
@apply col-span-2 items-center;
}
.ds-table-actions {
@apply col-span-7;
@apply col-span-5 flex w-full justify-end;
}
.ds-table-crud {

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

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

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

@ -1,17 +1,17 @@
<script setup lang="ts">
import { Empty, extractSdkResponseErrorMsg, h, iconMap, message, storeToRefs, useI18n, useNuxtApp, useProject } from '#imports'
import { Empty, extractSdkResponseErrorMsg, h, iconMap, message, storeToRefs, useBase, useI18n, useNuxtApp } from '#imports'
const props = defineProps<{
baseId: string
sourceId: string
}>()
const emit = defineEmits(['baseSynced'])
const { $api } = useNuxtApp()
const projectStore = useProject()
const { loadTables } = projectStore
const { project } = storeToRefs(projectStore)
const baseStore = useBase()
const { loadTables } = baseStore
const { base } = storeToRefs(baseStore)
const { t } = useI18n()
@ -23,11 +23,11 @@ const metadiff = ref<any[]>([])
async function loadMetaDiff() {
try {
if (!project.value?.id) return
if (!base.value?.id) return
isLoading.value = true
isDifferent.value = false
metadiff.value = await $api.base.metaDiffGet(project.value?.id, props.baseId)
metadiff.value = await $api.source.metaDiffGet(base.value?.id, props.sourceId)
for (const model of metadiff.value) {
if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
@ -41,21 +41,45 @@ async function loadMetaDiff() {
}
}
const { $poller } = useNuxtApp()
async function syncMetaDiff() {
try {
if (!project.value?.id || !isDifferent.value) return
if (!base.value?.id || !isDifferent.value) return
isLoading.value = true
await $api.base.metaDiffSync(project.value?.id, props.baseId)
// Table metadata recreated successfully
message.info(t('msg.info.metaDataRecreated'))
await loadTables()
await loadMetaDiff()
emit('baseSynced')
const jobData = await $api.source.metaDiffSync(base.value?.id, props.sourceId)
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
// Table metadata recreated successfully
message.info(t('msg.info.metaDataRecreated'))
await loadTables()
await loadMetaDiff()
emit('baseSynced')
isLoading.value = false
} else if (status === JobStatus.FAILED) {
message.error('Failed to sync base metadata')
isLoading.value = false
}
}
},
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
}
}
@ -141,7 +165,7 @@ const columns = [
<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>
<GeneralTableIcon :meta="record" class="text-gray-500" />
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title || record.table_name }}</span>
</div>

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

@ -1,37 +1,39 @@
<script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { onMounted } from '@vue/runtime-core'
import { ProjectIdInj, storeToRefs, useGlobal, useProject, watch } from '#imports'
import { ProjectIdInj, storeToRefs, useBase, useGlobal, watch } from '#imports'
const { includeM2M, showNull } = useGlobal()
const projectStore = useProject()
const projectsStore = useProjects()
const { loadTables, hasEmptyOrNullFilters } = projectStore
const { project } = storeToRefs(projectStore)
const baseStore = useBase()
const basesStore = useBases()
const { loadTables, hasEmptyOrNullFilters } = baseStore
const { base } = storeToRefs(baseStore)
const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId?.value ?? project.value?.id)
const baseId = computed(() => _projectId?.value ?? base.value?.id)
const { t } = useI18n()
watch(includeM2M, async () => await loadTables())
const showNullAndEmptyInFilter = ref()
onMounted(async () => {
await projectsStore.loadProject(projectId.value!, true)
showNullAndEmptyInFilter.value = projectsStore.getProjectMeta(projectId.value!)?.showNullAndEmptyInFilter
await basesStore.loadProject(baseId.value!, true)
showNullAndEmptyInFilter.value = basesStore.getProjectMeta(baseId.value!)?.showNullAndEmptyInFilter
})
async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) {
const project = projectsStore.projects.get(projectId.value!)
if (!project) throw new Error(`Project ${projectId.value} not found`)
const base = basesStore.bases.get(baseId.value!)
if (!base) throw new Error(`Base ${baseId.value} not found`)
const meta = projectsStore.getProjectMeta(projectId.value!) ?? {}
const meta = basesStore.getProjectMeta(baseId.value!) ?? {}
// users cannot hide null & empty option if there is existing null / empty filters
if (!evt.target.checked) {
if (await hasEmptyOrNullFilters()) {
showNullAndEmptyInFilter.value = true
message.warning('Null / Empty filters exist. Please remove them first.')
message.warning(t('msg.error.nullFilterExists'))
}
}
const newProjectMeta = {
@ -39,9 +41,9 @@ async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) {
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value,
}
// update local state
project.meta = newProjectMeta
base.meta = newProjectMeta
// update db
await projectsStore.updateProject(projectId.value!, {
await basesStore.updateProject(baseId.value!, {
meta: JSON.stringify(newProjectMeta),
})
}

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

@ -7,11 +7,12 @@ interface Props {
modelValue?: boolean
openKey?: string
dataSourcesState?: string
projectId?: string
baseId?: string
}
interface SubTabGroup {
[key: string]: {
key: string
title: string
body: any
onClick?: () => void
@ -37,12 +38,14 @@ const vOpenKey = useVModel(props, 'openKey', emits)
const vDataState = useVModel(props, 'dataSourcesState', emits)
const projectId = toRef(props, 'projectId')
const baseId = toRef(props, 'baseId')
provide(ProjectIdInj, projectId)
provide(ProjectIdInj, baseId)
const { $e } = useNuxtApp()
const { t } = useI18n()
const dataSourcesReload = ref(false)
const dataSourcesAwakened = ref(false)
@ -105,19 +108,20 @@ const tabsInfo: TabGroup = {
// $e('c:settings:audit')
// },
// },
projectSettings: {
// Project Settings
title: 'Project Settings',
baseSettings: {
// Base Settings
title: t('labels.projectSettings'),
icon: iconMap.settings,
subTabs: {
misc: {
// Misc
title: 'Misc',
key: 'Misc',
title: t('general.misc'),
body: Misc,
},
},
onClick: () => {
$e('c:settings:project-settings')
$e('c:settings:base-settings')
},
},
}
@ -252,16 +256,16 @@ watch(
v-model:state="vDataState"
v-model:reload="dataSourcesReload"
class="px-2 pb-2"
:data-testid="`nc-settings-subtab-${selectedSubTab.title}`"
:project-id="projectId"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
:base-id="baseId"
@awaken="handleAwaken"
/>
<component
:is="selectedSubTab?.body"
v-else
class="px-2 py-6"
:project-id="projectId"
:data-testid="`nc-settings-subtab-${selectedSubTab.title}`"
:base-id="baseId"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
/>
</div>
</a-layout-content>

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

@ -10,24 +10,24 @@ import {
message,
onMounted,
storeToRefs,
useBase,
useGlobal,
useI18n,
useNuxtApp,
useProject,
} from '#imports'
const props = defineProps<{
baseId: string
sourceId: string
}>()
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
const { project } = storeToRefs(useProject())
const { base } = storeToRefs(useBase())
const _projectId = inject(ProjectIdInj, ref())
const projectId = computed(() => _projectId.value ?? project.value?.id)
const baseId = computed(() => _projectId.value ?? base.value?.id)
const { includeM2M } = useGlobal()
@ -42,7 +42,7 @@ const searchInput = ref('')
const filteredTables = computed(() =>
tables.value.filter(
(el) =>
el?.base_id === props.baseId &&
el?.source_id === props.sourceId &&
((typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.value.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.value.toLowerCase()))),
),
@ -50,11 +50,11 @@ const filteredTables = computed(() =>
async function loadTableList() {
try {
if (!projectId.value) return
if (!baseId.value) return
isLoading.value = true
tables.value = await $api.project.modelVisibilityList(projectId.value, {
tables.value = await $api.base.modelVisibilityList(baseId.value, {
includeM2M: includeM2M.value,
})
} catch (e) {
@ -66,10 +66,10 @@ async function loadTableList() {
async function saveUIAcl() {
try {
if (!projectId.value) return
if (!baseId.value) return
await $api.project.modelVisibilitySet(
projectId.value,
await $api.base.modelVisibilitySet(
baseId.value,
tables.value.filter((t) => t.edited),
)
// Updated UI ACL for tables successfully
@ -95,25 +95,25 @@ const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gra
const columns = [
{
title: tableHeaderRenderer('Table name'),
title: tableHeaderRenderer(t('labels.tableName')),
name: 'table_name',
},
{
title: tableHeaderRenderer('View name'),
title: tableHeaderRenderer(t('labels.viewName')),
name: 'view_name',
},
{
title: tableHeaderRenderer('Editor'),
title: tableHeaderRenderer(t('objects.roleType.editor')),
name: 'editor',
width: 120,
},
{
title: tableHeaderRenderer('Commenter'),
title: tableHeaderRenderer(t('objects.roleType.commenter')),
name: 'commenter',
width: 120,
},
{
title: tableHeaderRenderer('Viewer'),
title: tableHeaderRenderer(t('objects.roleType.viewer')),
name: 'viewer',
width: 120,
},
@ -123,9 +123,9 @@ const columns = [
<template>
<div class="flex flex-row w-full items-center justify-center">
<div class="flex flex-col w-[900px]">
<span class="mb-4 first-letter:capital font-bold"> UI ACL : {{ project.title }} </span>
<span class="mb-4 first-letter:capital font-bold"> UI ACL : {{ base.title }} </span>
<div class="flex flex-row items-center w-full mb-4 gap-2 justify-between">
<a-input v-model:value="searchInput" placeholder="Search models" class="nc-acl-search !w-[400px]">
<a-input v-model:value="searchInput" :placeholder="$t('placeholder.searchModels')" class="nc-acl-search !w-[400px]">
<template #prefix>
<component :is="iconMap.search" />
</template>
@ -134,14 +134,14 @@ const columns = [
<a-button type="text" ghost class="self-start !rounded-md nc-acl-reload" @click="loadTableList">
<div class="flex items-center gap-2 text-gray-600 font-light">
<component :is="iconMap.reload" :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload
{{ $t('general.reload') }}
</div>
</a-button>
<NcButton size="large" class="z-10 !rounded-lg !px-2 mr-2.5" type="primary" @click="saveUIAcl">
<div class="flex flex-row items-center w-full gap-x-1">
<component :is="iconMap.save" />
<div class="flex">Save</div>
<div class="flex">{{ $t('general.save') }}</div>
</div>
</NcButton>
</div>
@ -171,10 +171,7 @@ const columns = [
<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>
<GeneralTableIcon :meta="{ meta: record.table_meta, type: record.ptype }" class="text-gray-500" />
</div>
<GeneralTruncateText>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record._ptn }}</span>
@ -196,9 +193,13 @@ const columns = [
<a-tooltip>
<template #title>
<span v-if="record.disabled[role]">
Click to make '{{ record.title }}' visible for role:{{ role }} in UI dashboard</span
{{ $t('labels.clickToMake') }} '{{ record.title }}' {{ $t('labels.visibleForRole') }} {{ role }}
{{ $t('labels.inUI') }} dashboard</span
>
<span v-else
>{{ $t('labels.clickToHide') }}'{{ record.title }}' {{ $t('labels.forRole') }}:{{ role }}
{{ $t('labels.inUI') }}</span
>
<span v-else>Click to hide '{{ record.title }}' for role:{{ role }} in UI dashboard</span>
</template>
<a-checkbox

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

@ -1,22 +1,22 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
const { project } = storeToRefs(useProject())
const { base } = storeToRefs(useBase())
</script>
<template>
<div v-if="!project || !project.bases"></div>
<template v-else-if="project.bases.length === 1">
<DashboardSettingsUIAcl :base-id="project.bases[0].id" class="mt-6" />
<div v-if="!base || !base.sources"></div>
<template v-else-if="base.sources.length === 1">
<DashboardSettingsUIAcl :source-id="base.sources[0].id" class="mt-6" />
</template>
<a-tabs v-else class="w-full">
<a-tab-pane v-for="base of project.bases" :key="base.id">
<a-tab-pane v-for="source of base.sources" :key="source.id">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__all-tables">
<div class="capitalize">{{ base.alias ?? 'Default' }}</div>
<div class="capitalize">{{ source.alias ?? 'Default' }}</div>
</div>
</template>
<DashboardSettingsUIAcl :base-id="base.id" class="mt-6" />
<DashboardSettingsUIAcl :source-id="source.id" class="mt-6" />
</a-tab-pane>
</a-tabs>
</template>

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

@ -8,6 +8,7 @@ import {
ProjectIdInj,
SSLUsage,
clientTypes as _clientTypes,
baseTitleValidator,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
@ -17,7 +18,6 @@ import {
iconMap,
nextTick,
onMounted,
projectTitleValidator,
readFile,
ref,
storeToRefs,
@ -27,23 +27,31 @@ import {
watch,
} from '#imports'
const { connectionType } = defineProps<{ connectionType: ClientType }>()
const props = defineProps<{ open: boolean; connectionType?: ClientType }>()
const emit = defineEmits(['baseCreated', 'close'])
const emit = defineEmits(['update:open', 'sourceCreated'])
const projectStore = useProject()
const { loadProject } = useProjects()
const { project } = storeToRefs(projectStore)
const vOpen = useVModel(props, 'open', emit)
const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL)
const baseStore = useBase()
const { loadProject } = useBases()
const { base } = storeToRefs(baseStore)
const { loadProjectTables } = useTablesStore()
const { refreshCommandPalette } = useCommandPalette()
const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId?.value ?? project.value?.id)
const baseId = computed(() => _projectId?.value ?? base.value?.id)
const useForm = Form.useForm
const testSuccess = ref(false)
const testingConnection = ref(false)
const form = ref<typeof Form>()
const { api } = useApi()
@ -52,6 +60,8 @@ const { $e } = useNuxtApp()
const { t } = useI18n()
const creatingSource = ref(false)
const formState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
@ -116,9 +126,9 @@ const validators = computed(() => {
'title': [
{
required: true,
message: 'Base name is required',
message: 'Source name is required',
},
projectTitleValidator,
baseTitleValidator,
],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
@ -225,26 +235,27 @@ function getConnectionConfig() {
const focusInvalidInput = () => {
form.value?.$el.querySelector('.ant-form-item-explain-error')?.parentNode?.parentNode?.querySelector('input')?.focus()
}
const isConnSuccess = ref(false)
const createBase = async () => {
const { $poller } = useNuxtApp()
const createSource = async () => {
try {
await validate()
isConnSuccess.value = false
} catch (e) {
focusInvalidInput()
isConnSuccess.value = false
return
}
try {
if (!projectId.value) return
if (!baseId.value) return
creatingSource.value = true
const connection = getConnectionConfig()
const config = { ...formState.value.dataSource, connection }
await api.base.create(projectId.value, {
const jobData = await api.source.create(baseId.value, {
alias: formState.value.title,
type: formState.value.dataSource.client,
config,
@ -252,14 +263,43 @@ const createBase = async () => {
inflection_table: formState.value.inflection.inflectionTable,
})
$e('a:base:create:extdb')
await loadProject(projectId.value, true)
await loadProjectTables(projectId.value, true)
emit('baseCreated')
emit('close')
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
$e('a:base:create:extdb')
if (baseId.value) {
await loadProject(baseId.value, true)
await loadProjectTables(baseId.value, true)
}
emit('sourceCreated')
vOpen.value = false
creatingSource.value = false
} else if (status === JobStatus.FAILED) {
message.error('Failed to create base')
creatingSource.value = false
}
}
},
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
creatingSource.value = false
} finally {
refreshCommandPalette()
}
}
@ -271,9 +311,11 @@ const testConnection = async () => {
return
}
$e('a:base:create:extdb:test-connection', [])
$e('a:source:create:extdb:test-connection', [])
try {
testingConnection.value = true
if (formState.value.dataSource.client === ClientType.SQLITE) {
testSuccess.value = true
} else {
@ -290,7 +332,6 @@ const testConnection = async () => {
if (result.code === 0) {
testSuccess.value = true
isConnSuccess.value = true
} else {
testSuccess.value = false
@ -302,6 +343,8 @@ const testConnection = async () => {
message.error(await extractSdkResponseErrorMsg(e))
}
testingConnection.value = false
}
const handleImportURL = async () => {
@ -357,289 +400,302 @@ onMounted(async () => {
})
watch(
() => connectionType,
connectionType,
(v) => {
formState.value.dataSource.client = v
onClientChange()
},
{ immediate: true },
)
const toggleModal = (val: boolean) => {
vOpen.value = val
}
</script>
<template>
<GeneralModal v-model:visible="isConnSuccess" class="!w-[25rem]">
<div class="flex flex-col h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ t('msg.info.dbConnected') }}</div>
<div class="flex gap-x-2 mt-5 ml-7 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="isConnSuccess = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" @click="createBase">Ok & Add Base</NcButton>
</div>
</div>
</GeneralModal>
<div class="create-base bg-white relative flex flex-col justify-center gap-2 w-full">
<h1 class="prose-2xl font-bold self-start mb-4 flex items-center gap-2">
New Base
<DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span>
</h1>
<a-form
ref="form"
:model="formState"
name="external-project-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<div
class="nc-scrollbar-md"
:style="{
maxHeight: '60vh',
}"
>
<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']"
<GeneralModal
:visible="vOpen"
:closable="!creatingSource"
:keyboard="!creatingSource"
:mask-closable="false"
size="medium"
@update:visible="toggleModal"
>
<div class="py-6 px-8">
<div class="create-source bg-white relative flex flex-col justify-center gap-2 w-full">
<h1 class="prose-xl font-bold self-start mb-4 flex items-center gap-2">
{{ $t('title.newBase') }}
<DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span>
</h1>
<a-form
ref="form"
:model="formState"
name="external-base-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).host" class="nc-extdb-host-address" />
</a-form-item>
<!-- Port Number -->
<a-form-item :label="$t('labels.port')" v-bind="validateInfos['dataSource.connection.port']">
<a-input-number
v-model:value="(formState.dataSource.connection as DefaultConnection).port"
class="!w-full nc-extdb-host-port"
/>
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).user" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')">
<a-input-password
v-model:value="(formState.dataSource.connection as DefaultConnection).password"
class="nc-extdb-host-password"
/>
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
:label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.searchPath.0']"
<div
class="nc-scrollbar-md"
:style="{
maxHeight: '60vh',
}"
>
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
<div class="flex items-right justify-end gap-2">
<!-- Use Connection URL -->
<NcButton size="small" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</NcButton>
</div>
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1">
<template #header>
<span>{{ $t('title.advancedParameters') }}</span>
</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 label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select
v-model:value="formState.dataSource.client"
class="nc-extdb-db-type"
dropdown-class-name="nc-dropdown-ext-db-type"
@change="onClientChange"
>
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
>{{ client.text }}
</a-select-option>
</a-select>
</a-form-item>
<!-- SQLite File -->
<a-form-item
v-if="formState.dataSource.client === ClientType.SQLITE"
:label="$t('labels.sqliteFile')"
v-bind="validateInfos['dataSource.connection.connection.filename']"
>
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input
v-model:value="(formState.dataSource.connection as DefaultConnection).host"
class="nc-extdb-host-address"
/>
</a-form-item>
<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>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="certFileInput?.click()">
{{ $t('labels.clientCert') }}
</NcButton>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select .key file -->
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput?.click()">
{{ $t('labels.clientKey') }}
</NcButton>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select CA file -->
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="caFileInput?.click()">
{{ $t('labels.serverCA') }}
</NcButton>
</a-tooltip>
</div>
<!-- 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>
<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" />
<component
:is="iconMap.close"
:style="{ 'font-size': '1.5em', 'color': 'red' }"
@click="removeParam(index)"
/>
</div>
</div>
<NcButton size="small" type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center">
<component :is="iconMap.plus" />
</div>
</NcButton>
</a-card>
<!-- 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>
<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>
<!-- 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>
<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>
<!-- 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>
<div class="flex justify-end">
<NcButton type="primary" size="small" class="!rounded-md" @click="handleEditJSON()">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
<!-- 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>
<div class="flex items-right justify-end gap-2">
<!-- Use Connection URL -->
<NcButton type="ghost" size="small" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</NcButton>
</div>
</a-collapse-panel>
</a-collapse>
</template>
</div>
<a-form-item class="flex justify-end !mt-5">
<div class="flex justify-end gap-2">
<NcButton type="primary" size="small" class="nc-extdb-btn-test-connection !rounded-md" @click="testConnection">
{{ $t('activity.testDbConn') }}
</NcButton>
<NcButton
size="small"
type="primary"
:disabled="!testSuccess"
class="nc-extdb-btn-submit !rounded-md"
@click="createBase"
>
{{ $t('general.submit') }}
</NcButton>
</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="500px"
: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>
<!-- connection succesfull modal -->
<GeneralModal v-model:visible="isConnSuccess" class="!w-[25rem]">
<div class="flex flex-col h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ t('msg.info.dbConnected') }}</div>
<div class="flex gap-x-2 mt-5 ml-7 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="isConnSuccess = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" @click="createBase">Ok & Add Base</NcButton>
</div>
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1">
<template #header>
<span>{{ $t('title.advancedParameters') }}</span>
</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>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="certFileInput?.click()">
{{ $t('labels.clientCert') }}
</NcButton>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select .key file -->
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput?.click()">
{{ $t('labels.clientKey') }}
</NcButton>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select CA file -->
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="caFileInput?.click()">
{{ $t('labels.serverCA') }}
</NcButton>
</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" />
<component
:is="iconMap.close"
:style="{ 'font-size': '1.5em', 'color': 'red' }"
@click="removeParam(index)"
/>
</div>
</div>
<NcButton size="small" type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center">
<component :is="iconMap.plus" />
</div>
</NcButton>
</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="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }} </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="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }} </a-select-option>
</a-select>
</a-form-item>
<div class="flex justify-end">
<NcButton type="primary" size="small" class="!rounded-md" @click="handleEditJSON()">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
</NcButton>
</div>
</a-collapse-panel>
</a-collapse>
</template>
</div>
<a-form-item class="flex justify-end !mt-5">
<div class="flex justify-end gap-2">
<NcButton
:type="testSuccess ? 'ghost' : 'primary'"
size="small"
class="nc-extdb-btn-test-connection !rounded-md"
:loading="testingConnection"
@click="testConnection"
>
<GeneralIcon v-if="testSuccess" icon="circleCheck" class="text-primary mr-2" />
{{ $t('activity.testDbConn') }}
</NcButton>
<NcButton
size="small"
type="primary"
:disabled="!testSuccess"
:loading="creatingSource"
class="nc-extdb-btn-submit !rounded-md"
@click="createSource"
>
{{ $t('general.submit') }}
</NcButton>
</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="500px"
: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>
</GeneralModal>
</div>
</div>
</GeneralModal>
</template>
<style lang="scss" scoped>
@ -663,7 +719,7 @@ watch(
@apply !min-h-0;
}
.create-base {
.create-source {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { BaseType } from 'nocodb-sdk'
import type { SourceType } from 'nocodb-sdk'
import { Form, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports'
@ -8,6 +8,7 @@ import {
ClientType,
ProjectIdInj,
SSLUsage,
baseTitleValidator,
clientTypes,
computed,
extractSdkResponseErrorMsg,
@ -16,7 +17,6 @@ import {
getTestDatabaseName,
iconMap,
onMounted,
projectTitleValidator,
readFile,
ref,
storeToRefs,
@ -27,22 +27,26 @@ import {
} from '#imports'
const props = defineProps<{
baseId: string
sourceId: string
}>()
const emit = defineEmits(['baseUpdated', 'close'])
const projectStore = useProject()
const projectsStore = useProjects()
const { project } = storeToRefs(projectStore)
const baseStore = useBase()
const basesStore = useBases()
const { base } = storeToRefs(baseStore)
const _projectId = inject(ProjectIdInj, undefined)
const projectId = computed(() => _projectId?.value ?? project.value?.id)
const baseId = computed(() => _projectId?.value ?? base.value?.id)
const { refreshCommandPalette } = useCommandPalette()
const useForm = Form.useForm
const testSuccess = ref(false)
const testingConnection = ref(false)
const form = ref<typeof Form>()
const { api } = useApi()
@ -51,6 +55,8 @@ const { $e } = useNuxtApp()
const { t } = useI18n()
const editingSource = ref(false)
const formState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
@ -75,7 +81,7 @@ const customFormState = ref<ProjectCreateForm>({
const validators = computed(() => {
return {
'title': [projectTitleValidator],
'title': [baseTitleValidator],
'extraParameters': [extraParameterValidator],
'dataSource.client': [fieldRequiredValidator()],
...(formState.value.dataSource.client === ClientType.SQLITE
@ -210,13 +216,13 @@ const editBase = async () => {
}
try {
if (!project.value?.id) return
if (!base.value?.id) return
const connection = getConnectionConfig()
const config = { ...formState.value.dataSource, connection }
await api.base.update(project.value?.id, props.baseId, {
await api.source.update(base.value?.id, props.sourceId, {
alias: formState.value.title,
type: formState.value.dataSource.client,
config,
@ -224,18 +230,18 @@ const editBase = async () => {
inflection_table: formState.value.inflection.inflectionTable,
})
$e('a:base:edit:extdb')
$e('a:source:edit:extdb')
await projectsStore.loadProject(projectId.value!, true)
await basesStore.loadProject(baseId.value!, true)
emit('baseUpdated')
emit('close')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
refreshCommandPalette()
}
}
const isConnSuccess = ref(false)
const testConnection = async () => {
try {
await validate()
@ -244,9 +250,11 @@ const testConnection = async () => {
return
}
$e('a:base:edit:extdb:test-connection', [])
$e('a:source:edit:extdb:test-connection', [])
try {
testingConnection.value = true
if (formState.value.dataSource.client === ClientType.SQLITE) {
testSuccess.value = true
} else {
@ -263,7 +271,6 @@ const testConnection = async () => {
if (result.code === 0) {
testSuccess.value = true
isConnSuccess.value = true
} else {
testSuccess.value = false
@ -275,6 +282,8 @@ const testConnection = async () => {
message.error(await extractSdkResponseErrorMsg(e))
}
testingConnection.value = false
}
const handleImportURL = async () => {
@ -310,12 +319,12 @@ watch(
{ deep: true },
)
// load base config
// load source config
onMounted(async () => {
if (project.value?.id) {
if (base.value?.id) {
const definedParameters = ['host', 'port', 'user', 'password', 'database']
const activeBase = (await api.base.read(project.value?.id, props.baseId)) as BaseType
const activeBase = (await api.source.read(base.value?.id, props.sourceId)) as SourceType
const tempParameters = Object.entries(activeBase.config.connection)
.filter(([key]) => !definedParameters.includes(key))
@ -337,24 +346,17 @@ onMounted(async () => {
</script>
<template>
<div class="edit-base bg-white relative flex flex-col justify-start gap-2 w-full p-2">
<h1 class="prose-2xl font-bold self-start">Edit Base</h1>
<a-form
ref="form"
:model="formState"
name="external-project-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<div class="edit-source bg-white relative flex flex-col justify-start gap-2 w-full p-2">
<h1 class="prose-2xl font-bold self-start">{{ $t('activity.editSource') }}</h1>
<a-form ref="form" :model="formState" name="external-base-create-form" layout="horizontal" no-style :label-col="{ span: 8 }">
<div
class="nc-scrollbar-md"
:style="{
maxHeight: '60vh',
}"
>
<a-form-item label="Base Name" v-bind="validateInfos.title">
<a-form-item label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
@ -427,7 +429,7 @@ onMounted(async () => {
</a-form-item>
<!-- Use Connection URL -->
<div class="flex justify-end gap-2">
<NcButton size="small" type="primary" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
<NcButton size="small" type="ghost" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</NcButton>
</div>
@ -519,7 +521,7 @@ onMounted(async () => {
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-option v-for="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }}</a-select-option>
</a-select>
</a-form-item>
@ -528,7 +530,7 @@ onMounted(async () => {
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-option v-for="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }}</a-select-option>
</a-select>
</a-form-item>
@ -545,15 +547,23 @@ onMounted(async () => {
<a-form-item class="flex justify-end !mt-5">
<div class="flex justify-end gap-2">
<NcButton type="secondary" size="small" class="nc-extdb-btn-test-connection !rounded-md" @click="testConnection">
<NcButton
:type="testSuccess ? 'ghost' : 'primary'"
size="small"
class="nc-extdb-btn-test-connection !rounded-md"
:loading="testingConnection"
@click="testConnection"
>
<GeneralIcon v-if="testSuccess" icon="circleCheck" class="text-primary mr-2" />
{{ $t('activity.testDbConn') }}
</NcButton>
<NcButton
class="nc-extdb-btn-submit !rounded-md"
size="small"
type="primary"
:disabled="!testSuccess"
class="nc-extdb-btn-submit !rounded-md"
:loading="editingSource"
@click="editBase"
>
{{ $t('general.submit') }}
@ -562,14 +572,14 @@ onMounted(async () => {
</a-form-item>
<div class="w-full flex items-center mt-2 text-[#e65100]">
<component :is="iconMap.warning" class="mr-2 mb-5.9" />
<div>Please make sure database you are trying to connect is valid! This operation can cause schema loss!!</div>
<div>{{ $t('msg.warning.dbValid') }}</div>
</div>
</a-form>
<a-modal
v-model:visible="configEditDlg"
:title="$t('activity.editConnJson')"
width="600px"
width="500px"
wrap-class-name="nc-modal-edit-connection-json"
@ok="handleOk"
>
@ -589,17 +599,6 @@ onMounted(async () => {
<a-input v-model:value="importURL" />
</a-modal>
</div>
<!-- connection succesfull modal -->
<GeneralModal v-model:visible="isConnSuccess" class="!w-97">
<div class="flex flex-col h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ t('msg.info.dbConnected') }}</div>
<div class="flex gap-x-2 mt-5 ml-7 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="isConnSuccess = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" @click="editBase">Ok & Edit Base</NcButton>
</div>
</div>
</GeneralModal>
</template>
<style lang="scss" scoped>
@ -623,7 +622,7 @@ onMounted(async () => {
@apply !min-h-0;
}
.edit-base {
.edit-source {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {

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

@ -11,15 +11,14 @@ import {
nextTick,
onMounted,
ref,
storeToRefs,
useNuxtApp,
useProject,
watch,
} from '#imports'
const { modelValue, baseId } = defineProps<{
const { modelValue, baseId, sourceId } = defineProps<{
modelValue: boolean
baseId: string
sourceId: string
}>()
const emit = defineEmits(['update:modelValue'])
@ -28,15 +27,13 @@ const { $api } = useNuxtApp()
const baseURL = $api.instance.defaults.baseURL
const { $state, $jobs } = useNuxtApp()
const { $state, $poller } = useNuxtApp()
const projectStore = useProject()
const baseStore = useBase()
const { refreshCommandPalette } = useCommandPalette()
const { loadTables } = projectStore
const { project } = storeToRefs(projectStore)
const { loadTables } = baseStore
const showGoToDashboardButton = ref(false)
@ -48,6 +45,10 @@ const logRef = ref<typeof AntCard>()
const enableAbort = ref(false)
const goBack = ref(false)
const listeningForUpdates = ref(false)
const syncSource = ref({
id: '',
type: 'Airtable',
@ -81,10 +82,6 @@ const pushProgress = async (message: string, status: JobStatus | 'progress') =>
})
}
const onSubscribe = () => {
step.value = 2
}
const onStatus = async (status: JobStatus, data?: any) => {
if (status === JobStatus.COMPLETED) {
showGoToDashboardButton.value = true
@ -93,6 +90,7 @@ const onStatus = async (status: JobStatus, data?: any) => {
refreshCommandPalette()
// TODO: add tab of the first table
} else if (status === JobStatus.FAILED) {
goBack.value = true
pushProgress(data.error.message, status)
}
}
@ -134,7 +132,7 @@ async function createOrUpdate() {
body: payload,
})
} else {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${baseId}/syncs/${sourceId}`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
@ -146,8 +144,47 @@ async function createOrUpdate() {
}
}
async function listenForUpdates(id?: string) {
if (listeningForUpdates.value) return
listeningForUpdates.value = true
const job = id ? { id } : await $api.jobs.status({ syncId: syncSource.value.id })
if (!job) {
listeningForUpdates.value = false
return
}
$poller.subscribe(
{ id: job.id },
(data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
step.value = 2
if (data.status) {
onStatus(data.status as JobStatus, data.data)
} else {
onLog(data.data as any)
}
} else {
listeningForUpdates.value = false
}
},
)
}
async function loadSyncSrc() {
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, {
const data: any = await $fetch(`/api/v1/db/meta/projects/${baseId}/syncs/${sourceId}`, {
baseURL,
method: 'GET',
headers: { 'xc-auth': $state.token.value as string },
@ -160,7 +197,7 @@ async function loadSyncSrc() {
syncSource.value = migrateSync(srcs[0])
syncSource.value.details.syncSourceUrlOrId =
srcs[0].details.appId && srcs[0].details.appId.length > 0 ? srcs[0].details.syncSourceUrlOrId : srcs[0].details.shareId
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
listenForUpdates()
} else {
syncSource.value = {
id: '',
@ -189,12 +226,12 @@ async function loadSyncSrc() {
async function sync() {
try {
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, {
const jobData: any = await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
})
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
listenForUpdates(jobData.id)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -214,6 +251,9 @@ async function abort() {
headers: { 'xc-auth': $state.token.value as string },
})
step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -221,6 +261,13 @@ async function abort() {
})
}
function cancel() {
step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
}
function migrateSync(src: any) {
if (!src.details?.options) {
src.details.options = {
@ -252,9 +299,8 @@ watch(
onMounted(async () => {
if (syncSource.value.id) {
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
listenForUpdates()
}
await loadSyncSrc()
})
</script>
@ -270,7 +316,7 @@ onMounted(async () => {
>
<div class="px-5">
<!-- Quick Import -->
<div class="mt-5 prose-xl font-weight-bold" @dblclick="enableAbort = true">{{ $t('title.quickImport') }} - AIRTABLE</div>
<div class="mt-5 prose-xl font-weight-bold" @dblclick="enableAbort = true">{{ $t('title.quickImportAirtable') }}</div>
<div v-if="step === 1">
<div class="mb-4">
@ -353,7 +399,7 @@ onMounted(async () => {
<!-- Import Formula Columns -->
<a-tooltip placement="top">
<template #title>
<span>Coming Soon!</span>
<span>{{ $t('title.comingSoon') }}</span>
</template>
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>
{{ $t('labels.importFormulaColumns') }}
@ -420,7 +466,12 @@ onMounted(async () => {
<a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false">
{{ $t('labels.goToDashboard') }}
</a-button>
<a-button v-else-if="enableAbort" class="mt-4" size="large" danger @click="abort()">ABORT</a-button>
<a-button v-else-if="goBack" class="mt-4 uppercase" size="large" danger @click="cancel()">{{
$t('general.cancel')
}}</a-button>
<a-button v-else-if="enableAbort" class="mt-4 uppercase" size="large" danger @click="abort()">{{
$t('general.abort')
}}</a-button>
</div>
</div>
</div>

25
packages/nc-gui/components/dlg/ProjectDelete.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
const props = defineProps<{
visible: boolean
projectId: string
baseId: string
}>()
const emits = defineEmits(['update:visible'])
@ -10,20 +10,22 @@ const visible = useVModel(props, 'visible', emits)
const { closeTab } = useTabs()
const projectsStore = useProjects()
const { deleteProject, navigateToFirstProjectOrHome } = projectsStore
const { projects } = storeToRefs(projectsStore)
const basesStore = useBases()
const { deleteProject, navigateToFirstProjectOrHome } = basesStore
const { bases } = storeToRefs(basesStore)
const { removeFromRecentViews } = useViewsStore()
const { refreshCommandPalette } = useCommandPalette()
const project = computed(() => projects.value.get(props.projectId))
const base = computed(() => bases.value.get(props.baseId))
const isLoading = ref(false)
const onDelete = async () => {
if (!project.value) return
if (!base.value) return
const toBeDeletedProject = JSON.parse(JSON.stringify(project.value))
const toBeDeletedProject = JSON.parse(JSON.stringify(base.value))
isLoading.value = true
try {
@ -34,13 +36,14 @@ const onDelete = async () => {
visible.value = false
if (toBeDeletedProject.id === projectsStore.activeProjectId) {
if (toBeDeletedProject.id === basesStore.activeProjectId) {
await navigateToFirstProjectOrHome()
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
removeFromRecentViews({ baseId: toBeDeletedProject.id! })
}
}
</script>
@ -48,13 +51,13 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="project" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralProjectIcon :type="project.type" class="nc-view-icon px-1.5"></GeneralProjectIcon>
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralProjectIcon :type="base.type" class="nc-view-icon px-1.5 w-10" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ project.title }}
{{ base.title }}
</div>
</div>
</template>

100
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -1,18 +1,28 @@
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import type { ProjectType } from 'nocodb-sdk'
import { useVModel } from '#imports'
import type { BaseType } from 'nocodb-sdk'
import { isEeUI, useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
project: ProjectType
onOk: (jobData: { name: string; id: string }) => Promise<void>
base: BaseType
}>()
const emit = defineEmits(['update:modelValue'])
const { refreshCommandPalette } = useCommandPalette()
const { api } = useApi()
const { $e, $poller } = useNuxtApp()
const basesStore = useBases()
const { loadProjects, createProject: _createProject } = basesStore
const { bases } = storeToRefs(basesStore)
const { navigateToProject } = useGlobal()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
@ -35,17 +45,17 @@ const isLoading = ref(false)
const _duplicate = async () => {
try {
isLoading.value = true
// pick a random color from array and assign to project
const color = projectThemeColors[Math.floor(Math.random() * 1000) % projectThemeColors.length]
// pick a random color from array and assign to base
const color = baseThemeColors[Math.floor(Math.random() * 1000) % baseThemeColors.length]
const tcolor = tinycolor(color)
const complement = tcolor.complement()
const jobData = await api.project.duplicate(props.project.id as string, {
const jobData = await api.base.duplicate(props.base.id as string, {
options: optionsToExclude.value,
project: {
fk_workspace_id: props.project.fk_workspace_id,
type: props.project.type,
base: {
fk_workspace_id: props.base.fk_workspace_id,
type: props.base.type,
color,
meta: JSON.stringify({
theme: {
@ -55,10 +65,50 @@ const _duplicate = async () => {
}),
},
})
props.onOk(jobData as any)
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadProjects('workspace')
const base = bases.value.get(jobData.base_id)
// open project after duplication
if (base) {
await navigateToProject({
workspaceId: isEeUI ? base.fk_workspace_id : undefined,
baseId: base.id,
type: base.type,
})
}
refreshCommandPalette()
isLoading.value = false
dialogShow.value = false
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate project')
await loadProjects('workspace')
refreshCommandPalette()
isLoading.value = false
dialogShow.value = false
}
}
},
)
$e('a:base:duplicate')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
dialogShow.value = false
}
@ -75,27 +125,39 @@ const isEaster = ref(false)
</script>
<template>
<GeneralModal v-if="project" v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate">
<GeneralModal
v-if="base"
v-model:visible="dialogShow"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
class="!w-[30rem]"
wrap-class-name="nc-modal-base-duplicate"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.project') }}
</div>
<div class="mt-4">Are you sure you want to duplicate the `{{ project.title }}` project?</div>
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">Include data</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">Include webhooks</a-checkbox>
<a-checkbox v-model:checked="options.includeData" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews" :disabled="isLoading">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks" :disabled="isLoading">
{{ $t('labels.includeWebhook') }}
</a-checkbox>
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton>
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:base:duplicate']" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>
</GeneralModal>
</template>

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

Loading…
Cancel
Save