Browse Source

Merge branch 'develop' into docs/v2

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

36
.github/workflows/ci-cd.yml

@ -118,19 +118,19 @@ jobs:
db: mysql
shard: 2
playwright-mysql-3:
needs: playwright-mysql-1
if: ${{ always() && ( github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft )}}
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: 3
playwright-mysql-4:
needs: playwright-mysql-2
if: ${{ always() && ( github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft )}}
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
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 }}
@ -145,6 +145,20 @@ jobs:
with:
db: sqlite
shard: 2
playwright-sqlite-3:
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: 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 }}
@ -160,16 +174,16 @@ jobs:
db: pg
shard: 2
playwright-pg-shard-3:
needs: playwright-pg-shard-1
if: ${{ always() && ( github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft )}}
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: 3
playwright-pg-shard-4:
needs: playwright-pg-shard-2
if: ${{ always() && ( github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft ) }}
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: 4
shard: 4

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

@ -13,46 +13,36 @@ on:
jobs:
playwright:
runs-on: [self-hosted, v2]
runs-on: [self-hosted, v3]
timeout-minutes: 100
steps:
- name: Checkout
uses: actions/checkout@v3
- name: pre-requisite checks
- name: Check node,pnpm Installation and set Path
shell: bash
working-directory: scripts/self-hosted-gh-runner
timeout-minutes: 1
run: |
node_version=$(node --version || echo "error")
pnpm_version=$(pnpm -v || echo "error")
echo "node version: $node_version"
echo "pnpm version: $pnpm_version"
if [[ $node_version != v18* ]] || [[ $pnp_version != 8* ]]; then
echo "version mismatch: expected node v18 and pnpm v8"
RUN_PRERQUISITE_STEPS="true"
elif [[ ${FORCE_RUN_PRERQUISITE_STEPS} == "true" ]];
# || [[ ! -f ${PRE_REQ_CHECK_FILE_PATH} ]];
then
echo "FORCE_RUN_PRERQUISITE_STEPS is true"
RUN_PRERQUISITE_STEPS="true"
else
RUN_PRERQUISITE_STEPS="false"
fi
echo "RUN_PRERQUISITE_STEPS=${RUN_PRERQUISITE_STEPS}" >> $GITHUB_ENV
./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.RUN_PRERQUISITE_STEPS == 'true'
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.RUN_PRERQUISITE_STEPS == 'true'
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
echo "STORE_PATH=/root/setup-pnpm/node_modules/.bin/store/v3" >> $GITHUB_ENV
- uses: actions/cache@v3
if: env.RUN_PRERQUISITE_STEPS == 'true'
if: env.IS_NPM_CACHE_DOWNLOAD_REQUIRED == 'true'
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
@ -113,22 +103,28 @@ jobs:
working-directory: ./packages/nocodb
run: |
pnpm run watch:run:playwright:pg &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log &
- name: copy built output and start frontend
if: always()
working-directory: ./
- name: download ui build from artf server
working-directory: ./packages/nc-gui/
run: |
# expects the variables to be available in runner context.
path="gh-artifacts/runs/${GITHUB_RUN_ID}/ui-build/.output"
target_dir="/mnt/${path}"
mkdir -p ${target_dir}
if [[ -d ${target_dir} ]]; then
echo "Directory ${target_dir} exists."
cp -r ${target_dir} ./packages/nc-gui/ || echo "playwright reports directory does not exists" >> ${target_dir}/playwright-report/index.html
else
echo "Error: Directory ${target_dir} does not exists."
exit 1
fi
cd ./packages/nc-gui/
rm -rf .output.zip .output
FILE="$(echo ${GITHUB_REPOSITORY} | sed "s,/,-,g")-${GITHUB_RUN_ID}.zip"
BUILD_FILE_URL=http://65.21.27.147/gh-artifacts/ui-builds/${FILE}
time curl -o output.zip ${BUILD_FILE_URL} -n
unzip -oq output.zip
# path="gh-artifacts/runs/${GITHUB_RUN_ID}/ui-build/.output"
# target_dir="/mnt/${path}"
# if [[ -d ${target_dir} ]]; then
# echo "Directory ${target_dir} exists."
# cp -r ${target_dir} . || echo "playwright reports directory does not exists" >> ${target_dir}/playwright-report/index.html
# else
# echo "Error: Directory ${target_dir} does not exists."
# exit 1
# fi
- name: start frontend
working-directory: ./packages/nc-gui/
run: |
pnpm run ci:start
- name: Install Playwright Browsers
working-directory: ./tests/playwright
@ -183,7 +179,8 @@ jobs:
# - name: Run quick tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
- uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }}
with:
@ -234,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](http://135.181.48.96/'${path}')
[playwright-report](http://135.181.48.96/'${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

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

@ -11,46 +11,36 @@ on:
jobs:
playwright:
runs-on: [self-hosted, aws]
runs-on: [self-hosted, v3]
timeout-minutes: 100
steps:
- name: Checkout
uses: actions/checkout@v3
- name: pre-requisite checks
- name: Check node,pnpm Installation and set Path
shell: bash
working-directory: scripts/self-hosted-gh-runner
timeout-minutes: 1
run: |
node_version=$(node --version || echo "error")
pnpm_version=$(pnpm -v || echo "error")
echo "node version: $node_version"
echo "pnpm version: $pnpm_version"
if [[ $node_version != v18* ]] || [[ $pnp_version != 8* ]]; then
echo "version mismatch: expected node v18 and pnpm v8"
RUN_PRERQUISITE_STEPS="true"
elif [[ ${FORCE_RUN_PRERQUISITE_STEPS} == "true" ]];
# || [[ ! -f ${PRE_REQ_CHECK_FILE_PATH} ]];
then
echo "FORCE_RUN_PRERQUISITE_STEPS is true"
RUN_PRERQUISITE_STEPS="true"
else
RUN_PRERQUISITE_STEPS="false"
fi
echo "RUN_PRERQUISITE_STEPS=${RUN_PRERQUISITE_STEPS}" >> $GITHUB_ENV
./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.RUN_PRERQUISITE_STEPS == 'true'
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.RUN_PRERQUISITE_STEPS == 'true'
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
echo "STORE_PATH=/root/setup-pnpm/node_modules/.bin/store/v3" >> $GITHUB_ENV
- uses: actions/cache@v3
if: env.RUN_PRERQUISITE_STEPS == 'true'
if: env.IS_NPM_CACHE_DOWNLOAD_REQUIRED == 'true'
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
@ -59,22 +49,18 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: install dependencies
run: pnpm bootstrap
- name: run frontend
- name: build frontend (nc-gui)
working-directory: ./packages/nc-gui
run: |
pnpm run build
timeout-minutes: 20
- name: Copy Artifacts to Local Artifacts Dir
if: always()
- name: upload frontend (nc-gui) build to artf server
working-directory: ./
run: |
# expects the variables to be available in runner context.
path="gh-artifacts/runs/${GITHUB_RUN_ID}/ui-build/"
target_dir="/mnt/${path}"
mkdir -p ${target_dir}
# start : add any artifacts to be copied here
cp -r ./packages/nc-gui/.output ${target_dir}/ || echo "playwright reports directory does not exists" >> ${target_dir}/playwright-report/index.html
SUMMARY='[UI BUILD](http://65.21.27.147/'${path}')'
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY
FILE="$(echo ${GITHUB_REPOSITORY} | sed "s,/,-,g")-${GITHUB_RUN_ID}.zip"
cd ./packages/nc-gui/
zip -r ${FILE} .output || echo "UI build directory does not exists" >&2
echo "uploading ${FILE} to http://65.21.27.147/upload/${FILE}"
time curl -T "${FILE}" http://65.21.27.147/upload/${FILE} -n

26
.github/workflows/publish-docs-index-typesense.yml

@ -0,0 +1,26 @@
name: "Publish : Docs search index (Typesense)"
on:
# Triggered manually
workflow_dispatch:
jobs:
doc-indexer:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3 # You MUST checkout your repository first!
- name: Run DocSearch Scraper
uses: celsiusnarhwal/typesense-scraper@v2
with:
# The secret containing your Typesense API key. Required.
api-key: ${{ secrets.TYPESENSE_API_KEY }}
# The hostname or IP address of your Typesense server. Required.
host: ${{ secrets.TYPESENSE_HOST }}
# The port on which your Typesense server is listening. Optional. Default: 8108.
port: 443
# The protocol to use when connecting to your Typesense server. Optional. Default: http.
protocol: https
# The path to your DocSearch config file. Optional. Default: docsearch.config.json.
config: ./packages/noco-docs/typesense-scrape-config.json

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

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

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

@ -0,0 +1,15 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Field Icons">
<mask id="mask0_3_121" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="Bounding box" width="24" height="24" fill="#6a7184"/>
</mask>
<g mask="url(#mask0_3_121)">
<g id="Group 58">
<path id="Vector" d="M15.4615 15.0769L21 15.0769" stroke="#6a7184" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path id="Vector_2" d="M15.4615 9.53845L21 9.53845" stroke="#6a7184" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<rect id="Rectangle 1" width="5.53846" height="16.6154" rx="1" transform="matrix(-1 0 0 1 21 4)" stroke="#6a7184" stroke-width="1.33"/>
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M12.0256 12.3077C12.0256 10.3817 10.4644 8.82049 8.53846 8.82049C6.61254 8.82049 5.05128 10.3817 5.05128 12.3077C5.05128 14.2336 6.61254 15.7948 8.53846 15.7948C10.4644 15.7948 12.0256 14.2336 12.0256 12.3077ZM8.53846 7.48715C11.2008 7.48715 13.359 9.64537 13.359 12.3077C13.359 14.97 11.2008 17.1282 8.53846 17.1282C7.4872 17.1282 6.51454 16.7917 5.72231 16.2205L5.47138 16.4714L3.47138 18.4714C3.21103 18.7318 2.78892 18.7318 2.52858 18.4714C2.26823 18.2111 2.26823 17.789 2.52858 17.5286L4.52858 15.5286L4.75813 15.299C4.10685 14.4771 3.71794 13.4378 3.71794 12.3077C3.71794 9.64537 5.87616 7.48715 8.53846 7.48715Z" fill="#6a7184"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 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

9
packages/nc-gui/assets/nc-icons/owner.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

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

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

@ -0,0 +1,12 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1409_68546)">
<path d="M12.3571 5.96842L4.64282 10.4219" stroke="#1F293A" 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.345705 8.77119)" stroke="#1F293A" stroke-width="1.33"/>
<path d="M4 6.33984L11.7143 10.7933" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1409_68546">
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 712 B

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

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

@ -48,10 +48,19 @@ main {
@apply m-0 h-full w-full bg-white;
}
.nc-input-md {
@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;
@ -177,14 +186,14 @@ a {
// menu item styling
.nc-menu-item {
@apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
@apply cursor-pointer text-sm flex items-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
&.disabled {
@apply text-black text-opacity-25 bg-[#f5f5f5] cursor-not-allowed text-shadow-none box-shadow-none border-[#d9d9d9];
}
}
.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 {
@ -433,9 +442,6 @@ a {
@apply !shadow-none rounded ring-1 ring-red-600;
}
.ant-modal {
@apply !top-[50px];
}
.ant-modal-content {
@apply !p-6;
border-radius: 1rem;
@ -596,6 +602,34 @@ input[type='number'] {
@apply !block !py-1.5;
}
.nc-sidebar-node {
@apply !xs:(min-h-12 max-h-12 hover:bg-gray-50 ml-1.5);
.nc-emoji {
@apply xs:(text-lg);
}
.material-symbols, .nc-icon {
@apply !xs:(text-xl -mt-0.25);
}
.nc-sidebar-node-title {
@apply xs:(text-base);
}
.nc-sidebar-node-btn:not(.nc-sidebar-expand) {
@apply !xs:(hidden)
}
}
.nc-button.ant-btn.nc-sidebar-node-btn {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}
.nc-button.ant-btn.nc-sidebar-node-btn.nc-sidebar-expand {
@apply xs:(opacity-100 hover:bg-gray-50);
.nc-icon {
@apply xs:(visible opacity-100 !text-gray-500)
}
}

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

@ -91,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']
@ -106,7 +107,6 @@ declare module '@vue/runtime-core' {
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircleOutline: typeof import('~icons/mdi/account-circle-outline')['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']
@ -117,11 +117,10 @@ 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']
MdiCircleMedium: typeof import('~icons/mdi/circle-medium')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
@ -129,10 +128,13 @@ 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']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHistory: typeof import('~icons/mdi/history')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
MdiLoading: typeof import('~icons/mdi/loading')['default']
@ -141,6 +143,7 @@ declare module '@vue/runtime-core' {
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMapMarkerOutline: typeof import('~icons/mdi/map-marker-outline')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMessageOutline: typeof import('~icons/mdi/message-outline')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
@ -159,9 +162,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>

28
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">{{ $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">{{ $t('labels.accountDetails') }}</div>
<div class="flex text-gray-500">{{ $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,20 +81,20 @@ 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">{{ $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">{{ $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"
/>
@ -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>

8
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) => {

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

@ -30,20 +30,19 @@ 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>
{{ $t('labels.inviteOnlySignup') }}
</div>
</div>
</template>

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

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { Empty, message } from 'ant-design-vue'
import type { ApiTokenType, RequestParams, UserType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, iconMap, ref, useApi, useCopy, useNuxtApp } from '#imports'
import { message } from 'ant-design-vue'
import type { ApiTokenType, RequestParams } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, ref, useApi, useCopy, useNuxtApp } from '#imports'
const { api, isLoading } = useApi()
@ -12,7 +12,16 @@ const { copy } = useCopy()
const { t } = useI18n()
const tokens = ref<UserType[]>([])
interface IApiTokenInfo extends ApiTokenType {
created_by: string
}
const tokens = ref<IApiTokenInfo[]>([])
const selectedToken = reactive({
isShow: false,
id: '',
})
const currentPage = ref(1)
@ -20,7 +29,11 @@ const showNewTokenModal = ref(false)
const currentLimit = ref(10)
const selectedTokenData = ref<ApiTokenType>({})
const defaultTokenName = t('labels.untitledToken')
const selectedTokenData = ref<ApiTokenType>({
description: defaultTokenName,
})
const searchText = ref<string>('')
@ -28,6 +41,17 @@ const pagination = reactive({
total: 0,
pageSize: 10,
})
const hideOrShowToken = (tokenId: string) => {
if (selectedToken.isShow && selectedToken.id === tokenId) {
selectedToken.isShow = false
selectedToken.id = ''
} else {
selectedToken.isShow = true
selectedToken.id = tokenId
}
}
const loadTokens = async (page = currentPage.value, limit = currentLimit.value) => {
currentPage.value = page
try {
@ -42,7 +66,7 @@ const loadTokens = async (page = currentPage.value, limit = currentLimit.value)
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
tokens.value = response.list as UserType[]
tokens.value = response.list as IApiTokenInfo[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -53,18 +77,17 @@ loadTokens()
const isModalOpen = ref(false)
const tokenDesc = ref('')
const tokenToCopy = ref('')
const openModal = (tk: string, desc: string) => {
isModalOpen.value = true
tokenToCopy.value = tk
tokenDesc.value = desc
}
const isValidTokenName = ref(false)
const deleteToken = async (token: string): Promise<void> => {
try {
await api.orgTokens.delete(token)
// message.success(t('msg.success.tokenDeleted'))
await loadTokens()
if (!tokens.value.length && currentPage.value !== 1) {
currentPage.value--
loadTokens(currentPage.value)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -74,7 +97,15 @@ const deleteToken = async (token: string): Promise<void> => {
tokenDesc.value = ''
}
const validateTokenName = (tokenName: string | undefined) => {
if (!tokenName) return false
return tokenName.length < 255
}
const generateToken = async () => {
isValidTokenName.value = validateTokenName(selectedTokenData.value.description)
if (!isValidTokenName.value) return
try {
await api.orgTokens.create(selectedTokenData.value)
showNewTokenModal.value = false
@ -84,8 +115,10 @@ const generateToken = async () => {
await loadTokens()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
selectedTokenData.value.description = defaultTokenName
$e('a:api-token:generate')
}
$e('a:api-token:generate')
}
const copyToken = async (token: string | undefined) => {
@ -102,132 +135,164 @@ const copyToken = async (token: string | undefined) => {
}
}
const descriptionInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
const triggerDeleteModal = (tokenToDelete: string, tokenDescription: string) => {
tokenToCopy.value = tokenToDelete
tokenDesc.value = tokenDescription
isModalOpen.value = true
}
const selectInputOnMount: VNodeRef = (el) =>
selectedTokenData.value.description === defaultTokenName && (el as HTMLInputElement)?.select()
const errorMessage = computed(() => {
const tokenLength = selectedTokenData.value.description?.length
if (!tokenLength) {
return t('msg.info.tokenNameNotEmpty')
} else if (tokenLength > 255) {
return t('msg.info.tokenNameMaxLength')
}
})
const handleCancel = () => {
showNewTokenModal.value = false
isValidTokenName.value = false
}
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<div class="max-w-[900px] mx-auto p-4" data-testid="nc-token-list">
<div class="text-xl my-4 text-left font-weight-bold">{{ $t('title.tokenManagement') }}</div>
<div class="py-2 flex gap-4 items-center">
<div class="flex-grow"></div>
<component :is="iconMap.reload" class="cursor-pointer" @click="() => loadTokens()" />
<a-button
<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">{{ $t('title.apiTokens') }}</h6>
<NcButton
:disabled="showNewTokenModal"
class="!rounded-md"
data-testid="nc-token-create"
size="middle"
type="primary"
@click="showNewTokenModal = true"
>
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<span class="hidden md:block">
{{ $t('title.addNewToken') }}
</div>
</a-button>
</span>
<span class="flex items-center justify-center md:hidden">
<component :is="iconMap.plus" />
</span>
</NcButton>
</div>
<a-table
:row-key="(record) => record.id"
:data-source="tokens"
:pagination="{ position: ['bottomCenter'] }"
:loading="isLoading"
size="small"
@change="loadTokens($event.current)"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!-- Created By -->
<a-table-column key="created_by" :title="$t('labels.createdBy')" data-index="created_by">
<template #default="{ text }">
<div v-if="text">
{{ text }}
<span>{{ $t('msg.apiTokenCreate') }}</span>
<div class="w-full mt-5 rounded-md h-136 overflow-y-scroll">
<div>
<div class="flex w-full pl-5 bg-gray-50 border-1">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9">{{ $t('title.tokenName') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start">{{ $t('title.creator') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start">{{ $t('labels.token') }}</span>
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start">{{ $t('labels.actions') }}</span>
</div>
<main>
<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">
<a-input
:ref="selectInputOnMount"
v-model:value="selectedTokenData.description"
:default-value="defaultTokenName"
type="text"
class="!rounded-lg !py-1"
placeholder="Token Name"
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>
</div>
<div class="flex gap-2 justify-start">
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
type="primary"
size="sm"
:is-loading="isLoading"
data-testid="nc-token-save-btn"
@click="generateToken"
>
{{ $t('general.save') }}
</NcButton>
</div>
</div>
<NcDivider />
</div>
<div v-else class="text-gray-400">N/A</div>
</template>
</a-table-column>
<!-- Description -->
<a-table-column key="description" :title="$t('labels.description')" data-index="description">
<template #default="{ text }">
{{ text }}
</template>
</a-table-column>
<!-- Token -->
<a-table-column key="token" :title="$t('labels.token')" data-index="token">
<template #default="{ text, record }">
<div class="w-[320px]">
<span v-if="record.show">{{ text }}</span>
<span v-else>*******************************************</span>
<div v-if="!tokens.length" class="h-118 justify-center flex items-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('title.noLabels')" />
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="actions" :title="$t('labels.actions')" data-index="token">
<template #default="{ record }">
<div class="flex items-center gap-2">
<a-tooltip placement="bottom">
<template #title>
<span v-if="record.show"> {{ $t('general.hide') }} </span>
<span v-else> {{ $t('general.show') }} </span>
</template>
<a-button type="text" class="!rounded-md nc-toggle-token-visibility" @click="record.show = !record.show">
<template #icon>
<MaterialSymbolsVisibilityOff v-if="record.show" class="flex mx-auto h-[1.1rem]" />
<MaterialSymbolsVisibility v-else class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> {{ $t('general.copy') }}</template>
<a-button type="text" class="!rounded-md" @click="copyToken(record.token)">
<template #icon>
<component :is="iconMap.copy" class="flex mx-auto h-[1rem]" />
</template>
</a-button>
</a-tooltip>
<a-dropdown
:trigger="['click']"
class="flex"
placement="bottomRight"
overlay-class-name="nc-dropdown-api-token-mgmt"
>
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert class="nc-token-menu" />
</div>
</a-button>
</div>
<template #overlay>
<a-menu data-testid="nc-token-row-action-icon">
<a-menu-item>
<div
class="flex flex-row items-center py-3 h-[2rem] nc-delete-token"
@click="openModal(record.token, record.description)"
>
<component :is="iconMap.delete" class="flex" />
<div class="text-sm pl-2">{{ $t('general.remove') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<div
v-for="el of tokens"
:key="el.id"
data-testid="nc-token-list"
class="flex border-1 pl-5 py-3 justify-between token"
>
<span class="text-black font-bold text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" length="20">
{{ el.description }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" length="20">
{{ el.created_by }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-3/9">
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" length="29">
{{ el.token }}
</GeneralTruncateText>
<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>{{ $t('labels.showOrHide') }}</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>{{ $t('general.copy') }}</template>
<component :is="iconMap.copy" class="hover::cursor-pointer" @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"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div>
</span>
</div>
</template>
</a-table-column>
</a-table>
</main>
</div>
</div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-15">
<a-pagination
v-model:current="currentPage"
:total="pagination.total"
show-less-items
@change="loadTokens(currentPage, currentLimit)"
/>
</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">
@ -242,57 +307,5 @@ const descriptionInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</span>
</template>
</GeneralDeleteModal>
<a-modal
v-model:visible="showNewTokenModal"
:class="{ active: showNewTokenModal }"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-generate-token"
>
<div class="relative flex flex-col h-full">
<a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false">
<template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
<!-- Generate Token -->
<div class="flex flex-row w-full -mt-1 mb-3">
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title>
</div>
<!-- Description -->
<a-form
ref="form"
:model="selectedTokenData"
name="basic"
layout="vertical"
class="flex flex-col justify-center space-y-6"
no-style
autocomplete="off"
@finish="generateToken"
>
<a-input
:ref="descriptionInput"
v-model:value="selectedTokenData.description"
data-testid="nc-token-modal-description"
:placeholder="$t('labels.description')"
class="h-9 rounded-md"
/>
<!-- Generate -->
<div class="flex flex-row justify-end">
<a-button size="middle" class="!rounded-md" type="primary" html-type="submit" data-testid="nc-token-modal-save">
{{ $t('general.generate') }}
</a-button>
</div>
</a-form>
</div>
</a-modal>
</div>
</template>
<style scoped></style>

302
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,74 +140,72 @@ 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 class="max-w-195 mx-auto">
<div class="text-2xl my-4 text-left font-weight-bold">{{ $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">
<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 mt-5 border-1 rounded-md h-[613px] max-w-250">
<div class="flex w-full bg-gray-50 border-b-1">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-10">{{ $t('labels.email') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-20">{{ $t('objects.role') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-end pl-42">{{ $t('labels.action') }}</span>
</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-128.25">
<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>
</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>
</div>
<section v-else class="tbody">
<div
v-for="el of users"
:key="el.id"
data-testid="nc-token-list"
class="flex py-3 justify-around px-5 border-b-1"
:class="{
'py-4': el.roles?.includes('super'),
}"
>
<span class="text-3.5 text-start w-1/3 pl-5">
{{ el.email }}
</span>
<span class="text-3.5 text-start w-1/3 pl-18">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold">{{ $t('labels.superAdmin') }}</div>
<a-select
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"
@ -216,88 +229,79 @@ const copyPasswordResetUrl = async (user: User) => {
</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 }}
</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>
<span class="w-1/3 pl-43">
<div
class="flex items-center gap-2"
:class="{
'opacity-0': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">
<MdiDotsVertical
class="border-1 !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)"
/>
<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-500" />
<div>{{ $t('activity.resendInvite') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" />
<div>{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyPasswordResetUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem>
</template>
<NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem 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-7">
<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="capitalize 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>
.tbody div:nth-child(10) {
border-bottom: none;
}
</style>

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

@ -79,7 +79,7 @@ const copyUrl = async () => {
try {
await copy(inviteUrl.value)
// Copied shareable base url to clipboard!
// Copied shareable source url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
} catch (e: any) {
message.error(e.message)
@ -124,7 +124,7 @@ 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">{{ $t('activity.copyInviteToken') }}</div>
</div>
<a-alert class="!mt-2" type="success" show-icon>

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">{{ $t('labels.headerName') }}</div>
</th>
<th>
<div class="text-left font-normal ml-2">Value</div>
<div class="text-left font-normal ml-2">{{ $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>{{ $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">{{ $t('title.parameterName') }}</div>
</th>
<th>
<div class="text-left font-normal ml-2">Value</div>
<div class="text-left font-normal ml-2">{{ $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>{{ $t('activity.addParameter') }}</div>
<component :is="iconMap.plus" class="flex mx-auto" />
</div>
</NcButton>

30
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)
@ -53,7 +53,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,7 +80,7 @@ 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,
@ -89,18 +89,16 @@ useSelectedCellKeyupListener(active, (e) => {
}"
@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 }" @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>

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

@ -1,9 +1,10 @@
<script setup lang="ts">
import NcModal from '../nc/Modal.vue'
import {
Modal as AModal,
ActiveCellInj,
EditModeInj,
IsFormInj,
JsonExpandInj,
ReadonlyInj,
computed,
inject,
@ -41,7 +42,7 @@ const localValueState = ref<string | undefined>()
const error = ref<string | undefined>()
const isExpanded = ref(false)
const isExpanded = inject(JsonExpandInj, ref(false))
const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value,
@ -139,7 +140,7 @@ useSelectedCellKeyupListener(active, (e) => {
</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>
<a-button type="text" size="small" @click="isExpanded = !isExpanded">
@ -148,11 +149,13 @@ useSelectedCellKeyupListener(active, (e) => {
<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>
@ -171,7 +174,7 @@ 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>
</component>

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

@ -23,9 +23,9 @@ import {
onMounted,
reactive,
ref,
useBase,
useEventListener,
useMetas,
useProject,
useRoles,
useSelectedCellKeyupListener,
watch,
@ -81,7 +81,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 +133,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)
@ -247,7 +247,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 +255,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)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
@ -321,6 +321,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,7 +380,7 @@ 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
@ -421,7 +423,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" />

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

@ -20,8 +20,8 @@ import {
inject,
isDrawerOrModalExist,
ref,
useBase,
useEventListener,
useProject,
useRoles,
useSelectedCellKeyupListener,
watch,
@ -71,7 +71,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
@ -175,7 +175,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 +183,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)) {
updatedColMeta.cdf = updatedColMeta.cdf.replace(/''/g, "'")
}
}
@ -217,6 +217,7 @@ const onKeydown = (e: KeyboardEvent) => {
const onSelect = () => {
isOpen.value = false
isEditable.value = false
}
const cellClickHook = inject(CellClickHookInj, null)
@ -286,7 +287,7 @@ const selectedOpt = computed(() => {
v-model:value="vModel"
class="w-full overflow-hidden"
:class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? '(Optional)' : ''"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed"
:bordered="false"
:open="isOpen && editAllowed"
@ -324,7 +325,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/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">

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

@ -50,7 +50,9 @@ 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 +63,7 @@ const {
visibleItems,
onDrop,
isLoading,
open,
open: _open,
FileIcon,
selectedImage,
isReadonly: _isReadonly,
@ -136,7 +138,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 +148,24 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
})
const rowHeight = inject(RowHeightInj, ref())
const open = () => {
if (isMobileMode.value) return (isExpandedForm.value = true)
_open()
}
const openAttachment = (item: any) => {
if (isMobileMode.value) return
_openAttachment(item)
}
const onExpand = () => {
if (isMobileMode.value) return
modalVisible.value = true
}
</script>
<template>
@ -153,9 +173,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"
:class="{ 'justify-center': !active, 'justify-between': active }"
>
<LazyCellAttachmentCarousel />
@ -168,7 +188,7 @@ const rowHeight = inject(RowHeightInj, ref())
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>
@ -182,9 +202,9 @@ const rowHeight = inject(RowHeightInj, ref())
<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>
<template #title>{{ $t('activity.attachmentDrop') }} </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">
<MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/>
@ -192,7 +212,7 @@ const rowHeight = inject(RowHeightInj, ref())
v-if="!visibleItems.length"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"
>
Add file(s)
{{ $t('activity.addFiles') }}
</div>
</div>
</NcTooltip>
@ -206,7 +226,7 @@ const rowHeight = inject(RowHeightInj, ref())
:class="{ 'justify-center': !isExpandedForm && !isGallery }"
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">
@ -220,18 +240,19 @@ const rowHeight = inject(RowHeightInj, ref())
:class="{ 'ml-2': active }"
@click="
() => {
if (isGallery) return
if (isGallery || isMobileMode) return
selectedImage = 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 +273,18 @@ const rowHeight = inject(RowHeightInj, ref())
</div>
<div
v-if="active"
v-if="active || (isForm && visibleItems.length)"
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)"
>
<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,7 +47,7 @@ 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)"
:class="{
'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop,
@ -52,7 +56,7 @@ onUnmounted(() => {
>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase" style="height: var(--sidebar-bottom-height)">
<div v-if="!isSharedBase">
<DashboardSidebarUserInfo />
</div>
</div>

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

@ -4,11 +4,19 @@ const workspaceStore = useWorkspace()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
const { activeViewTitleOrId } = storeToRefs(useViewsStore())
const { activeTableId } = storeToRefs(useTablesStore())
const { isMobileMode } = useGlobal()
const showSidebarBtn = computed(() => !(isMobileMode.value && !activeViewTitleOrId.value && !activeTableId.value))
</script>
<template>
<div
class="flex items-center px-2 nc-sidebar-header py-1.2 w-full border-b-1 border-gray-200 group"
class="flex items-center nc-sidebar-header w-full border-b-1 border-gray-200 group md:(px-2 py-1.2) xs:(px-1 py-1)"
:data-workspace-title="activeWorkspace?.title"
style="height: var(--topbar-height)"
>
@ -18,7 +26,7 @@ const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
<div class="flex flex-grow min-w-1"></div>
<NcTooltip
class="flex opacity-0 group-hover:opacity-100 transition-opacity duration-50"
class="flex"
:class="{
'!opacity-100': !isLeftSidebarOpen,
}"
@ -26,20 +34,20 @@ const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
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
type="text"
size="small"
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !hover:bg-gray-200"
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)"
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
>
<div class="flex items-center text-inherit">
<GeneralIcon v-if="isMobileMode" icon="close" />
<GeneralIcon
v-else
icon="doubleLeftArrow"
class="duration-150 transition-all !text-lg -mt-0.5"
:class="{

13
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,14 +43,15 @@ 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"
class="nc-sidebar-top-button !xs:hidden"
data-testid="nc-sidebar-team-settings-btn"
:centered="false"
:class="{
@ -68,8 +69,8 @@ const navigateToSettings = () => {
v-model:is-open="isCreateProjectOpen"
modal
type="text"
class="nc-sidebar-top-button !hover:bg-gray-200"
data-testid="nc-sidebar-create-project-btn"
class="nc-sidebar-top-button !hover:bg-gray-200 !xs:hidden"
data-testid="nc-sidebar-create-base-btn"
>
<div class="gap-x-2 flex flex-row w-full items-center !font-normal">
<GeneralIcon icon="plus" />

83
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)
@ -19,12 +30,15 @@ const isAuthTokenCopied = ref(false)
const isLoggingOut = ref(false)
const { isMobileMode } = useGlobal()
const logout = async () => {
isLoggingOut.value = true
try {
await signOut(false)
await clearWorkspaces()
// No need as all stores are cleared on signout
// await clearWorkspaces()
await navigateTo('/signin')
} catch (e) {
@ -71,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>
@ -79,44 +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 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"> {{ $t('title.helpCenter') }} </span>
</NcMenuItem>
</a>
</template>
<NcDivider />
<a 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>
</NcMenuItem>
</a>
<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" />
@ -130,21 +146,24 @@ onMounted(() => {
</a-popover>
</template>
<NcDivider />
<NcMenuItem @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>
</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>
<template v-if="!isMobileMode">
<NcDivider />
<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"> {{ $t('title.copiedAuthToken') }} </template>
<template v-else> {{ $t('title.copyAuthToken') }} </template>
</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>
</template>
</NcDropdown>
<div v-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div>
<template v-if="isMobileMode"></template>
<div v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div>
<div v-else-if="isMounted" class="flex flex-row justify-between pt-1 truncate">
<div class="flex flex-wrap mb-1">
<GithubButton

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>

48
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,19 +48,26 @@ function onOpenModal({
'tableId': table.value.id,
'selectedViewId': copyViewId,
groupingFieldColumnId,
'views': views,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette()
await loadViews()
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 })
@ -57,6 +76,7 @@ function onOpenModal({
function closeDialog() {
isOpen.value = false
isDlgOpen.value = false
close(1000)
}
@ -64,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>
@ -86,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 })">
@ -96,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 })">
@ -106,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>

497
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,18 +44,25 @@ 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 basesStore = useBases()
const projectsStore = useProjects()
const { isMobileMode } = useGlobal()
const { loadProject, loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = projectsStore
const { projects } = storeToRefs(projectsStore)
const { loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = basesStore
const { bases } = storeToRefs(basesStore)
const { loadProjectTables } = useTablesStore()
const { activeTable } = storeToRefs(useTablesStore())
const { appInfo, navigateToProject } = useGlobal()
@ -59,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, () => {})
@ -79,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]
@ -94,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()
@ -106,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))
}
@ -126,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'),
)
@ -144,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(),
})
@ -183,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
@ -207,86 +229,82 @@ 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!)
let isSharedBase = false
// if shared base ignore navigation
if (route.value.params.typeOrId === 'base') {
isSharedBase = true
}
const isProjectPopulated = basesStore.isProjectPopulated(base.id!)
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
}
function openErdView(source: SourceType) {
$e('c:project:relation')
async function openProjectErdView(_project: ProjectType) {
if (!_project.id) return
const isOpen = ref(true)
if (!projectsStore.isProjectPopulated(_project.id)) {
await loadProject(_project.id)
}
const { close } = useDialog(resolveComponent('DlgProjectErd'), {
'modelValue': isOpen,
'sourceId': source!.id,
'onUpdate:modelValue': () => closeDialog(),
'baseId': base.value.id,
})
const project = projects.value.get(_project.id)
function closeDialog() {
isOpen.value = false
const base = project?.bases?.[0]
if (!base) return
openErdView(base)
close(1000)
}
}
const reloadTables = async () => {
@ -296,11 +314,11 @@ const reloadTables = async () => {
}
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
})
@ -310,11 +328,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}`)
}
},
{
@ -335,87 +353,109 @@ 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 { $poller } = useNuxtApp()
const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string }) => {
const DlgProjectDuplicateOnOk = async (jobData: { id: string; base_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,
})
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate project')
await loadProjects('workspace')
}
})
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadProjects('workspace')
const base = bases.value.get(jobData.base_id)
// open base after duplication
if (base) {
await navigateToProject({
baseId: base.id,
type: base.type,
})
}
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate base')
await loadProjects('workspace')
}
}
},
)
$e('a:base:duplicate')
}
const tableDelete = () => {
isTableDeleteDialogVisible.value = true
$e('c:table:delete')
}
$e('a:project:duplicate')
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,
'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="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"
@click="onProjectClick(project, true, true)"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75 !xs:visible"
@click="onProjectClick(base, true, true)"
>
<GeneralIcon
icon="triangleFill"
class="absolute top-2.25 left-2 group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90"
:class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90 !xs:visible"
: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>
@ -426,7 +466,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 }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode }"
@click.stop
@keyup.enter="updateProjectTitle"
@keyup.esc="updateProjectTitle"
@ -434,17 +474,18 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
/>
<span
v-else
class="capitalize text-ellipsis overflow-hidden select-none"
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']">
<NcButton
v-e="['c:base:options']"
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }"
data-testid="nc-sidebar-context-menu"
@ -461,32 +502,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" />
@ -494,48 +541,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/meta/bases/${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') }}
@ -545,11 +601,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"
@ -560,67 +617,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 :source-type="source.type" 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 :source-type="source.type" 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>
@ -628,16 +688,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>
@ -648,25 +709,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>
@ -675,10 +737,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>
@ -691,13 +753,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>
@ -705,16 +771,17 @@ 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>
<NcMenuItem v-if="isUIAllowed('tableDelete')" @click="isTableDeleteDialogVisible = true">
<div class="nc-project-option-item text-red-600">
<NcDivider />
<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>
@ -723,7 +790,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<template v-else>
<NcMenuItem @click="reloadTables">
<div class="nc-project-option-item">
<div class="nc-base-option-item">
{{ $t('general.reload') }}
</div>
</NcMenuItem>
@ -732,35 +799,39 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</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"
:base-id="base?.id"
/>
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :project-id="project?.id" />
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
<DlgProjectDuplicate
v-if="selectedProjectToDuplicate"
v-model="isDuplicateDlgOpen"
:project="selectedProjectToDuplicate"
:base="selectedProjectToDuplicate"
:on-ok="DlgProjectDuplicateOnOk"
/>
<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>

60
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,21 +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 { projectTables } = storeToRefs(useTablesStore())
const tables = computed(() => projectTables.value.get(project.value.id!) ?? [])
const { isMobileMode } = useGlobal()
const { baseTables } = storeToRefs(useTablesStore())
const tables = computed(() => baseTables.value.get(base.value.id!) ?? [])
const { $api } = useNuxtApp()
@ -42,13 +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
@ -84,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
@ -103,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,
}),
)
},
@ -122,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>

120
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,45 +9,48 @@ 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 } = useTableNew({
projectId: project.value.id!,
const { openTable: _openTable } = useTableNew({
baseId: base.value.id!,
})
const route = useRoute()
const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal()
const tabStore = useTabs()
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)
@ -75,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)
@ -102,6 +105,22 @@ const onExpand = async () => {
}
}
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 {
isLoading.value = false
isExpanded.value = true
}
}
watch(
() => activeView.value?.id,
() => {
@ -127,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 pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
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="openTable(table)"
@click="onOpenTable"
>
<div class="flex flex-row h-full items-center">
<NcButton type="text" size="xxsmall" class="nc-sidebar-node-btn" @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 hover:bg-"
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="min-w-5.75"></div>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
class="flex items-center nc-table-icon"
@ -165,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,
}"
/>
@ -200,7 +231,7 @@ const isTableOpened = computed(() => {
</div>
<span
class="nc-tbl-title capitalize text-ellipsis overflow-hidden select-none"
class="nc-tbl-title nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:class="{
'text-black !font-medium': isTableOpened,
}"
@ -210,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
@ -229,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') }}
@ -240,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)"
>
@ -251,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"
@ -263,20 +297,28 @@ const isTableOpened = computed(() => {
</template>
</NcDropdown>
<DashboardTreeViewCreateViewBtn v-if="isUIAllowed('viewCreateOrEdit')">
<NcButton type="text" size="xxsmall" class="nc-create-view-btn nc-sidebar-node-btn">
<NcButton
v-e="['c:view:create']"
type="text"
size="xxsmall"
class="nc-create-view-btn nc-sidebar-node-btn"
:class="{
'!md:(visible opacity-100)': openedTableId === table.id,
}"
>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
</DashboardTreeViewCreateViewBtn>
</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>

85
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 { isUIAllowed } = useRoles()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { $e } = useNuxtApp()
const { isMobileMode } = useGlobal()
const isDefaultBase = computed(() => {
const base = project.value?.bases?.find((b) => b.id === table.value.base_id)
if (!base) return false
const { $e } = useNuxtApp()
return _isDefaultBase(base)
})
const { t } = useI18n()
const { viewsByTable, activeView } = storeToRefs(useViewsStore())
const { viewsByTable, activeView, recentViews } = 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
@ -178,6 +173,7 @@ async function onSortEnd(evt: SortableEvent, undo = false) {
const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy()
if (isMobileMode.value) return
sortable = new Sortable(el, {
// handle: '.nc-drag-icon',
@ -190,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 */
@ -210,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,
})
@ -237,6 +238,13 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) {
scope: defineModelScope({ view: activeView.value }),
})
}
// update view name in recent views
recentViews.value = recentViews.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'))
@ -258,17 +266,26 @@ 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!,
})
}
await loadViews({
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,
}
},
})
@ -324,12 +341,15 @@ function onOpenModal({
refreshCommandPalette()
await loadViews()
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,
})
@ -346,29 +366,8 @@ function onOpenModal({
</script>
<template>
<DashboardTreeViewCreateViewBtn
v-if="isUIAllowed('viewCreateOrEdit')"
:overlay-class-name="isDefaultBase ? '!left-18 !min-w-42' : '!left-25 !min-w-42'"
>
<NcButton
type="text"
size="xsmall"
class="!w-full !py-0 !h-7 !text-gray-500 !hover:(bg-transparent font-normal text-brand-500) !font-normal !text-sm"
:centered="false"
>
<GeneralIcon
icon="plus"
class="mr-2"
:class="{
'ml-18.75': isDefaultBase,
'ml-24.25': !isDefaultBase,
}"
/>
<span class="text-sm">New View</span>
</NcButton>
</DashboardTreeViewCreateViewBtn>
<a-menu
v-if="views.length"
ref="menuRef"
:class="{ dragging }"
class="nc-views-menu flex flex-col w-full !border-r-0 !bg-inherit"

62
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
class="!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"
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>
@ -253,7 +234,7 @@ function onRef(el: HTMLElement) {
<div
v-else
class="capitalize text-ellipsis overflow-hidden select-none w-full"
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none w-full"
data-testid="sidebar-view-title"
:class="{
'font-medium': activeView?.id === vModel.id,
@ -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" l @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>

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

@ -16,10 +16,10 @@ import {
ref,
resolveComponent,
storeToRefs,
useBase,
useBases,
useDialog,
useNuxtApp,
useProject,
useProjects,
useRoles,
useTablesStore,
useTabs,
@ -31,52 +31,54 @@ const { isUIAllowed } = useRoles()
const { addTab } = useTabs()
const { $e, $jobs } = useNuxtApp()
const { $e, $poller } = 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 baseCreateDlg = ref(false)
const projectStore = useProject()
const baseStore = useBase()
const { loadTables } = projectStore
const { loadTables } = baseStore
const { tables } = storeToRefs(projectStore)
const { tables, isSharedBase } = storeToRefs(baseStore)
const { t } = useI18n()
const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const { refreshCommandPalette } = useCommandPalette()
const contextMenuTarget = reactive<{ type?: 'project' | 'base' | 'table' | 'main' | 'layout'; value?: any }>({})
const contextMenuTarget = reactive<{ type?: 'base' | 'source' | '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 +89,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 +98,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,27 +111,44 @@ 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()
}
})
$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 loadTables()
refreshCommandPalette()
const newTable = tables.value.find((el) => el.id === data?.data?.result?.id)
if (newTable) addTab({ title: newTable.title, id: newTable.id, type: newTable.type as TabType })
openTable(newTable!)
} else if (data.status === JobStatus.FAILED) {
message.error(t('msg.error.failedToDuplicateTable'))
await loadTables()
}
}
},
)
$e('a:table:duplicate')
},
@ -163,17 +182,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 +205,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
// ALT + D
case 68: {
e.stopPropagation()
projectCreateDlg.value = true
baseCreateDlg.value = true
break
}
}
@ -194,7 +213,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 +253,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 +269,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>

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

@ -5,20 +5,21 @@ import 'splitpanes/dist/splitpanes.css'
const router = useRouter()
const route = router.currentRoute
const { isMobileMode } = storeToRefs(useConfigStore())
const {
isLeftSidebarOpen,
leftSidebarWidthPercent,
leftSideBarSize: sideBarSize,
leftSidebarState: sidebarState,
mobileNormalizedSidebarSize,
} = storeToRefs(useSidebarStore())
const wrapperRef = ref<HTMLDivElement>()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const animationDuration = 250
const viewportWidth = ref(window.innerWidth)
const sidebarWidth = computed(() => (sideBarSize.value.old * viewportWidth.value) / 100)
const currentSidebarSize = computed({
get: () => sideBarSize.value.current,
set: (val) => {
@ -27,6 +28,22 @@ const currentSidebarSize = computed({
},
})
const { handleSidebarOpenOnMobileForNonViews } = useConfigStore()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const mobileNormalizedContentSize = computed(() => {
if (isMobileMode.value) {
return isLeftSidebarOpen.value ? 0 : 100
}
return contentSize.value
})
const sidebarWidth = computed(() =>
isMobileMode.value ? viewportWidth.value : (sideBarSize.value.old * viewportWidth.value) / 100,
)
watch(currentSidebarSize, () => {
leftSidebarWidthPercent.value = currentSidebarSize.value
})
@ -40,18 +57,18 @@ 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)
}
})
function handleMouseMove(e: MouseEvent) {
if (isMobileMode.value) return
if (!wrapperRef.value) return
if (sidebarState.value === 'openEnd') return
@ -89,6 +106,14 @@ watch(route, () => {
isLeftSidebarOpen.value = true
}
})
watch(isMobileMode, () => {
isLeftSidebarOpen.value = !isMobileMode.value
})
onMounted(() => {
handleSidebarOpenOnMobileForNonViews()
})
</script>
<template>
@ -99,11 +124,17 @@ watch(route, () => {
}"
@resize="currentSidebarSize = $event[0].size"
>
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-sidebar-splitpane relative !overflow-visible">
<Pane
min-size="15%"
:size="mobileNormalizedSidebarSize"
max-size="40%"
class="nc-sidebar-splitpane relative !overflow-visible"
>
<div
ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
:class="{
'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen,
'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
}"
@ -114,7 +145,7 @@ watch(route, () => {
<slot name="sidebar" />
</div>
</Pane>
<Pane :size="contentSize">
<Pane :size="mobileNormalizedContentSize">
<slot name="content" />
</Pane>
</Splitpanes>
@ -126,6 +157,10 @@ watch(route, () => {
width: calc(100% + 4px);
}
.mobile.nc-sidebar-wrapper.minimized-height > * {
@apply !h-full;
}
.nc-sidebar-wrapper > * {
transition: all 0.2s ease-in-out;
@apply z-10 absolute;

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

211
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,61 +63,46 @@ 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))
}
}
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))
}
@ -127,24 +112,24 @@ 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))
@ -157,11 +142,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,
@ -302,7 +293,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 +305,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 pl-2">{{ $t('labels.actions') }}</div>
<div class="ds-table-col ds-table-crud"></div>
</div>
</div>
@ -329,8 +320,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 +334,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>
@ -360,12 +351,8 @@ const isEditBaseModalOpen = computed({
@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
<GeneralIcon icon="sync" class="group-hover:text-accent" />
{{ $t('tooltip.metaSync') }}
</div>
</a-button>
<a-button
@ -375,7 +362,7 @@ const isEditBaseModalOpen = computed({
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
Relations
{{ $t('title.relations') }}
</div>
</a-button>
<a-button
@ -385,7 +372,7 @@ const isEditBaseModalOpen = computed({
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
UI ACL
{{ $t('labels.uiAcl') }}
</div>
</a-button>
<a-button
@ -395,7 +382,7 @@ const isEditBaseModalOpen = computed({
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
Audit
{{ $t('title.audit') }}
</div>
</a-button>
</div>
@ -412,32 +399,32 @@ const isEditBaseModalOpen = computed({
</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">
<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-if="source.is_meta || source.is_local">-</div>
<div v-else class="flex items-center gap-1">
{{ base.is_meta || base.is_local ? 'BASE' : base.alias }}
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</div>
</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>
@ -446,54 +433,50 @@ const isEditBaseModalOpen = computed({
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(base.id, DataSourcesSubTab.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" />
Relations
{{ $t('title.relations') }}
</div>
</a-button>
<a-button
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.UIAcl)"
@click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
UI ACL
{{ $t('labels.uiAcl') }}
</div>
</a-button>
<a-button
v-if="!base.is_meta && !base.is_local"
v-if="!source.is_meta && !source.is_local"
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.Metadata)"
@click="baseAction(source.id, DataSourcesSubTab.Metadata)"
>
<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
<GeneralIcon icon="sync" class="group-hover:text-accent" />
{{ $t('tooltip.metaSync') }}
</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"
v-if="!source.is_meta && !source.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)"
@click="baseAction(source.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"
v-if="!source.is_meta && !source.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="openDeleteBase(base)"
@click="openDeleteBase(source)"
>
<GeneralIcon icon="delete" class="text-red-500 -mt-0.5" />
</a-button>
@ -503,48 +486,48 @@ const isEditBaseModalOpen = computed({
</Draggable>
</div>
</div>
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium">
<GeneralModal v-model:visible="isNewBaseModalOpen" closable :mask-closable="false" size="medium">
<div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase
:connection-type="clientType"
@base-created="loadBases(true)"
@source-created="loadBases(true)"
@close="isNewBaseModalOpen = false"
/>
</div>
</GeneralModal>
<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' }"

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>

141
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,27 @@ import {
watch,
} from '#imports'
const { connectionType } = defineProps<{ connectionType: ClientType }>()
const props = defineProps<{ connectionType?: ClientType }>()
const emit = defineEmits(['baseCreated', 'close'])
const emit = defineEmits(['sourceCreated', 'close'])
const projectStore = useProject()
const { loadProject } = useProjects()
const { project } = storeToRefs(projectStore)
const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL)
const baseStore = useBase()
const { loadProject } = useBases()
const { base } = storeToRefs(baseStore)
const { loadProjectTables } = useTablesStore()
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 +56,8 @@ const { $e } = useNuxtApp()
const { t } = useI18n()
const creatingSource = ref(false)
const formState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
@ -116,9 +122,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 +231,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,12 +259,38 @@ 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')
emit('close')
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))
}
@ -271,9 +304,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 +325,6 @@ const testConnection = async () => {
if (result.code === 0) {
testSuccess.value = true
isConnSuccess.value = true
} else {
testSuccess.value = false
@ -302,6 +336,8 @@ const testConnection = async () => {
message.error(await extractSdkResponseErrorMsg(e))
}
testingConnection.value = false
}
const handleImportURL = async () => {
@ -357,7 +393,7 @@ onMounted(async () => {
})
watch(
() => connectionType,
connectionType,
(v) => {
formState.value.dataSource.client = v
onClientChange()
@ -367,37 +403,21 @@ watch(
</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
<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-project-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<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>
@ -470,7 +490,7 @@ watch(
</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">
<NcButton type="ghost" size="small" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</NcButton>
</div>
@ -563,7 +583,7 @@ watch(
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>
@ -572,7 +592,7 @@ watch(
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>
@ -589,7 +609,14 @@ watch(
<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">
<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>
@ -597,8 +624,9 @@ watch(
size="small"
type="primary"
:disabled="!testSuccess"
:loading="creatingSource"
class="nc-extdb-btn-submit !rounded-md"
@click="createBase"
@click="createSource"
>
{{ $t('general.submit') }}
</NcButton>
@ -628,17 +656,6 @@ watch(
>
<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>
</div>
</GeneralModal>
</div>
</template>
@ -663,7 +680,7 @@ watch(
@apply !min-h-0;
}
.create-base {
.create-source {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {

95
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,24 @@ 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 useForm = Form.useForm
const testSuccess = ref(false)
const testingConnection = ref(false)
const form = ref<typeof Form>()
const { api } = useApi()
@ -51,6 +53,8 @@ const { $e } = useNuxtApp()
const { t } = useI18n()
const editingSource = ref(false)
const formState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
@ -75,7 +79,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 +214,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,9 +228,9 @@ 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) {
@ -234,8 +238,6 @@ const editBase = async () => {
}
}
const isConnSuccess = ref(false)
const testConnection = async () => {
try {
await validate()
@ -244,9 +246,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 +267,6 @@ const testConnection = async () => {
if (result.code === 0) {
testSuccess.value = true
isConnSuccess.value = true
} else {
testSuccess.value = false
@ -275,6 +278,8 @@ const testConnection = async () => {
message.error(await extractSdkResponseErrorMsg(e))
}
testingConnection.value = false
}
const handleImportURL = async () => {
@ -310,12 +315,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 +342,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 +425,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 +517,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 +526,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 +543,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 +568,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 +595,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 +618,7 @@ onMounted(async () => {
@apply !min-h-0;
}
.edit-base {
.edit-source {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {

89
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)
}
}
@ -127,14 +125,14 @@ async function createOrUpdate() {
const { id, ...payload } = syncSource.value
if (id !== '') {
await $fetch(`/api/v1/db/meta/syncs/${id}`, {
await $fetch(`/api/v1/meta/syncs/${id}`, {
baseURL,
method: 'PATCH',
headers: { 'xc-auth': $state.token.value as string },
body: payload,
})
} else {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs/${baseId}`, {
syncSource.value = await $fetch(`/api/v1/meta/bases/${baseId}/syncs/${sourceId}`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
@ -146,8 +144,47 @@ async function createOrUpdate() {
}
}
async function listenForUpdates() {
if (listeningForUpdates.value) return
listeningForUpdates.value = true
const job = 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/meta/bases/${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`, {
await $fetch(`/api/v1/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()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -208,7 +245,7 @@ async function abort() {
"This is a highly experimental feature and only marks job as not started, please don't abort the job unless you are sure job is stuck.",
onOk: async () => {
try {
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/abort`, {
await $fetch(`/api/v1/meta/syncs/${syncSource.value.id}/abort`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
@ -252,9 +289,8 @@ watch(
onMounted(async () => {
if (syncSource.value.id) {
$jobs.subscribe({ syncId: syncSource.value.id }, onSubscribe, onStatus, onLog)
listenForUpdates()
}
await loadSyncSrc()
})
</script>
@ -270,7 +306,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 +389,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 +456,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="step = 1">{{
$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"></GeneralProjectIcon>
<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>

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

@ -1,11 +1,11 @@
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import type { ProjectType } from 'nocodb-sdk'
import type { BaseType } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
project: ProjectType
base: BaseType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
@ -35,17 +35,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: {
@ -75,27 +75,29 @@ 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" 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">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">{{ $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 key="submit" v-e="['a:base:duplicate']" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>
</GeneralModal>
</template>

52
packages/nc-gui/components/dlg/ProjectErd.vue

@ -0,0 +1,52 @@
<script lang="ts" setup>
const props = defineProps<{
baseId: string
sourceId: string
modelValue: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const isOpen = useVModel(props, 'modelValue', emit)
const activeSourceId = computed(() => props.sourceId)
const { openedProject: base } = storeToRefs(useBases())
const { baseTables } = storeToRefs(useTablesStore())
const { loadProjectTables } = useTablesStore()
const isLoading = ref(true)
const { getMeta } = useMetas()
const baseId = computed(() => props.baseId || base.value?.id)
onMounted(async () => {
if (baseId.value && baseTables.value.get(baseId.value)) {
return (isLoading.value = false)
}
try {
await loadProjectTables(baseId.value!)
await Promise.all(
baseTables.value.get(baseId.value!)!.map(async (table) => {
await getMeta(table.id!, false, false, baseId.value!)
}),
)
} catch (e) {
console.error(e)
} finally {
isLoading.value = false
}
})
</script>
<template>
<GeneralModal v-model:visible="isOpen" size="large">
<div class="h-[80vh]">
<ErdView v-if="!isLoading" :source-id="activeSourceId" :base-id="baseId" />
</div>
</GeneralModal>
</template>

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

@ -28,9 +28,9 @@ import {
reactive,
ref,
storeToRefs,
useBase,
useGlobal,
useI18n,
useProject,
useVModel,
} from '#imports'
@ -41,11 +41,11 @@ import { useNuxtApp } from '#app'
interface Props {
modelValue: boolean
importType: 'csv' | 'json' | 'excel'
baseId: string
sourceId: string
importDataOnly?: boolean
}
const { importType, importDataOnly = false, baseId, ...rest } = defineProps<Props>()
const { importType, importDataOnly = false, sourceId, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -63,7 +63,7 @@ const { t } = useI18n()
const progressMsg = ref('Parsing Data ...')
const { tables } = storeToRefs(useProject())
const { tables } = storeToRefs(useBase())
const activeKey = ref('uploadTab')
@ -119,7 +119,7 @@ const { validate, validateInfos } = useForm(importState, validators)
const importMeta = computed(() => {
if (IsImportTypeExcel.value) {
return {
header: `${t('title.quickImport')} - EXCEL`,
header: `${t('title.quickImportExcel')}`,
uploadHint: t('msg.info.excelSupport'),
urlInputLabel: t('msg.info.excelURL'),
loadUrlDirective: ['c:quick-import:excel:load-url'],
@ -127,7 +127,7 @@ const importMeta = computed(() => {
}
} else if (isImportTypeCsv.value) {
return {
header: `${t('title.quickImport')} - CSV`,
header: `${t('title.quickImportCSV')}`,
uploadHint: '',
urlInputLabel: t('msg.info.csvURL'),
loadUrlDirective: ['c:quick-import:csv:load-url'],
@ -135,7 +135,7 @@ const importMeta = computed(() => {
}
} else if (isImportTypeJson.value) {
return {
header: `${t('title.quickImport')} - JSON`,
header: `${t('title.quickImportJSON')}`,
uploadHint: '',
acceptTypes: '.json',
}
@ -546,13 +546,13 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
<LazyTemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
:project-template="templateData"
:base-template="templateData"
:import-data="importData"
:import-columns="importColumns"
:import-data-only="importDataOnly"
:quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse"
:base-id="baseId"
:source-id="sourceId"
:import-worker="importWorker"
class="nc-quick-import-template-editor"
@import="handleImport"
@ -598,7 +598,7 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
<template #tab>
<span class="flex items-center gap-2">
<component :is="iconMap.json" />
JSON Editor
{{ $t('title.jsonEditor') }}
</span>
</template>
@ -611,7 +611,7 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
<template #tab>
<span class="flex items-center gap-2">
<component :is="iconMap.link" />
URL
{{ $t('datatype.URL') }}
</span>
</template>
@ -645,7 +645,7 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
<a-form-item v-if="isImportTypeCsv || IsImportTypeExcel" class="!my-2">
<a-checkbox v-model:checked="importState.parserConfig.firstRowAsHeaders">
<span class="caption">Use First Row as Headers</span>
<span class="caption">{{ $t('labels.firstRowAsHeaders') }}</span>
</a-checkbox>
</a-form-item>
@ -665,7 +665,9 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
</div>
</a-spin>
<template #footer>
<a-button v-if="templateEditorModal" key="back" class="!rounded-md" @click="templateEditorModal = false">Back </a-button>
<a-button v-if="templateEditorModal" key="back" class="!rounded-md" @click="templateEditorModal = false"
>{{ $t('general.back') }}
</a-button>
<a-button v-else key="cancel" class="!rounded-md" @click="dialogShow = false">{{ $t('general.cancel') }} </a-button>
@ -676,7 +678,7 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
:disabled="disableFormatJsonButton"
@click="formatJson"
>
Format JSON
{{ $t('labels.formatJson') }}
</a-button>
<a-button

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

@ -6,7 +6,7 @@ import {
nextTick,
onMounted,
ref,
useProject,
useBase,
useTableNew,
useTablesStore,
useTabs,
@ -16,8 +16,8 @@ import {
const props = defineProps<{
modelValue: boolean
sourceId: string
baseId: string
projectId: string
}>()
const emit = defineEmits(['update:modelValue', 'create'])
@ -30,30 +30,30 @@ const inputEl = ref<HTMLInputElement>()
const { addTab } = useTabs()
const { isMysql, isMssql, isPg } = useProject()
const { isMysql, isMssql, isPg } = useBase()
const { loadProjectTables, addTable } = useTablesStore()
const { table, createTable, generateUniqueTitle, tables, project } = useTableNew({
const { table, createTable, generateUniqueTitle, tables, base } = useTableNew({
async onTableCreate(table) {
// await loadProject(props.projectId)
// await loadProject(props.baseId)
await addTab({
id: table.id as string,
title: table.title,
type: TabType.TABLE,
projectId: props.projectId,
// baseId: props.baseId,
baseId: props.baseId,
// sourceId: props.sourceId,
})
addTable(props.projectId, table)
await loadProjectTables(props.projectId, true)
addTable(props.baseId, table)
await loadProjectTables(props.baseId, true)
emit('create', table)
dialogShow.value = false
},
sourceId: props.sourceId,
baseId: props.baseId,
projectId: props.projectId,
})
const useForm = Form.useForm
@ -66,7 +66,7 @@ const validators = computed(() => {
validator: (_: any, value: any) => {
// validate duplicate alias
return new Promise((resolve, reject) => {
if ((tables.value || []).some((t) => t.title === (value || '') && t.base_id === props.baseId)) {
if ((tables.value || []).some((t) => t.title === (value || '') && t.source_id === props.sourceId)) {
return reject(new Error('Duplicate table alias'))
}
return resolve(true)
@ -77,15 +77,15 @@ const validators = computed(() => {
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql(props.baseId)) {
if (isMysql(props.sourceId)) {
tableNameLengthLimit = 64
} else if (isPg(props.baseId)) {
} else if (isPg(props.sourceId)) {
tableNameLengthLimit = 63
} else if (isMssql(props.baseId)) {
} else if (isMssql(props.sourceId)) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) {
const basePrefix = base?.value?.prefix || ''
if ((basePrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
}
resolve()
@ -168,9 +168,9 @@ onMounted(() => {
<template #label="{ value }">
<a-tooltip v-if="value === 'id'" placement="top" class="!flex">
<template #title>
<span>ID column is required, you can rename this later if required.</span>
<span>{{ $t('msg.idColumnRequired') }}</span>
</template>
ID
{{ $t('datatype.ID') }}
</a-tooltip>
<div v-else class="flex">
{{ value }}
@ -184,13 +184,14 @@ onMounted(() => {
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
v-e="['a:table:create']"
type="primary"
:disabled="validateInfos.title.validateStatus === 'error'"
:loading="creating"
@click="_createTable"
>
{{ $t('activity.createTable') }}
<template #loading> Creating Table </template>
<template #loading> {{ $t('title.creatingTable') }} </template>
</NcButton>
</div>
</a-form>

22
packages/nc-gui/components/dlg/TableDelete.vue

@ -5,7 +5,7 @@ import { UITypes, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{
visible: boolean
tableId: string
projectId: string
baseId: string
}>()
const emits = defineEmits(['update:visible'])
@ -17,13 +17,14 @@ const { closeTab } = useTabs()
const { getMeta, removeMeta } = useMetas()
const { loadTables, projectUrl, isXcdbBase } = useProject()
const { loadTables, baseUrl, isXcdbBase } = useBase()
const { refreshCommandPalette } = useCommandPalette()
const { projectTables, activeTable } = storeToRefs(useTablesStore())
const { removeFromRecentViews } = useViewsStore()
const { baseTables, activeTable } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore()
const tables = computed(() => projectTables.value.get(props.projectId) ?? [])
const tables = computed(() => baseTables.value.get(props.baseId) ?? [])
const table = computed(() => tables.value.find((t) => t.id === props.tableId))
@ -42,7 +43,7 @@ const onDelete = async () => {
const meta = (await getMeta(toBeDeletedTable.id as string, true)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c))
if (relationColumns?.length && !isXcdbBase(toBeDeletedTable.base_id)) {
if (relationColumns?.length && !isXcdbBase(toBeDeletedTable.source_id)) {
const refColMsgs = await Promise.all(
relationColumns.map(async (c, i) => {
const refMeta = (await getMeta((c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string)) as TableType
@ -69,17 +70,20 @@ const onDelete = async () => {
await loadTables()
// Remove from recent views
removeFromRecentViews({ baseId: props.baseId, tableId: toBeDeletedTable.id as string })
removeMeta(toBeDeletedTable.id as string)
refreshCommandPalette()
// Deleted table successfully
$e('a:table:delete')
if (oldActiveTableId === toBeDeletedTable.id) {
// Navigate to project if no tables left or open first table
// Navigate to base if no tables left or open first table
if (tables.value.length === 0) {
await navigateTo(
projectUrl({
id: props.projectId,
baseUrl({
id: props.baseId,
type: 'database',
}),
)
@ -106,7 +110,7 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.table')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralTableIcon :meta="table" class="nc-view-icon"></GeneralTableIcon>
<GeneralTableIcon :meta="table" class="nc-view-icon" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

14
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -34,7 +34,7 @@ const isLoading = ref(false)
const _duplicate = async () => {
try {
isLoading.value = true
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, { options: optionsToExclude.value })
const jobData = await api.dbTable.duplicate(props.table.base_id!, props.table.id!, { options: optionsToExclude.value })
props.onOk(jobData as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -70,21 +70,23 @@ const isEaster = ref(false)
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
<div class="mt-4">Are you sure you want to duplicate the `{{ table.title }}` table?</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">{{ $t('labels.includeData') }}a</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">{{ $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" type="primary" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton>
<NcButton key="submit" v-e="['a:table:duplicate']" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>
</GeneralModal>
</template>

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

@ -10,10 +10,10 @@ import {
nextTick,
reactive,
storeToRefs,
useBase,
useCommandPalette,
useMetas,
useNuxtApp,
useProject,
useTablesStore,
useTabs,
useUndoRedo,
@ -25,10 +25,10 @@ import {
interface Props {
modelValue?: boolean
tableMeta: TableType
baseId: string
sourceId: string
}
const { tableMeta, baseId, ...props } = defineProps<Props>()
const { tableMeta, sourceId, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
@ -42,9 +42,9 @@ const { updateTab } = useTabs()
const { loadProjectTables } = useTablesStore()
const projectStore = useProject()
const { loadTables, isMysql, isMssql, isPg } = projectStore
const { tables, project } = storeToRefs(projectStore)
const baseStore = useBase()
const { loadTables, isMysql, isMssql, isPg } = baseStore
const { tables, base } = storeToRefs(baseStore)
const { refreshCommandPalette } = useCommandPalette()
@ -68,15 +68,15 @@ const validators = computed(() => {
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql(baseId)) {
if (isMysql(sourceId)) {
tableNameLengthLimit = 64
} else if (isPg(baseId)) {
} else if (isPg(sourceId)) {
tableNameLengthLimit = 63
} else if (isMssql(baseId)) {
} else if (isMssql(sourceId)) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) {
const basePrefix = base?.value?.prefix || ''
if ((basePrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
}
resolve()
@ -127,14 +127,14 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
loading.value = true
try {
await $api.dbTable.update(tableMeta.id as string, {
project_id: tableMeta.project_id,
base_id: tableMeta.base_id,
table_name: formState.title,
title: formState.title,
})
dialogShow.value = false
await loadProjectTables(tableMeta.project_id!, true)
await loadProjectTables(tableMeta.base_id!, true)
if (!undo) {
addUndo({
@ -168,7 +168,7 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
$e('a:table:rename')
useTitle(`${project.value?.title}: ${newMeta?.title}`)
useTitle(`${base.value?.title}: ${newMeta?.title}`)
dialogShow.value = false
} catch (e: any) {
@ -213,8 +213,8 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
:loading="loading"
@click="() => renameTable()"
>
Rename Table
<template #loading> Renaming Table </template>
{{ $t('title.renameTable') }}
<template #loading> {{ $t('title.renamingTable') }}</template>
</NcButton>
</div>
</div>

73
packages/nc-gui/components/dlg/ViewCreate.vue

@ -2,7 +2,7 @@
import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import { capitalize } from '@vue/runtime-core'
import type { FormType, GalleryType, GridType, KanbanType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { FormType, GalleryType, GridType, KanbanType, MapType, TableType } from 'nocodb-sdk'
import { UITypes, ViewTypes } from 'nocodb-sdk'
import { computed, message, nextTick, onBeforeMount, reactive, ref, useApi, useI18n, useVModel, watch } from '#imports'
@ -13,7 +13,6 @@ interface Props {
selectedViewId?: string
groupingFieldColumnId?: string
geoDataFieldColumnId?: string
views: ViewType[]
tableId: string
}
@ -41,7 +40,9 @@ const emits = defineEmits<Emits>()
const { getMeta } = useMetas()
const { views, selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props)
const { viewsByTable } = storeToRefs(useViewsStore())
const { selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props)
const meta = ref<TableType | undefined>()
@ -57,6 +58,8 @@ const { api } = useApi()
const isViewCreating = ref(false)
const views = computed(() => viewsByTable.value.get(tableId.value) ?? [])
const form = reactive<Form>({
title: props.title || '',
type: props.type,
@ -238,8 +241,46 @@ onMounted(async () => {
<template #header>
<div class="flex flex-row items-center gap-x-1.5">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
{{ $t(`general.${selectedViewId ? 'duplicate' : 'create'}`) }} <span class="capitalize">{{ typeAlias }}</span>
{{ $t('objects.view') }}
<template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateGridView') }}
</template>
<template v-else>
{{ $t('labels.createGridView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.GALLERY">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateGalleryView') }}
</template>
<template v-else>
{{ $t('labels.createGalleryView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.FORM">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateFormView') }}
</template>
<template v-else>
{{ $t('labels.createFormView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.KANBAN">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateKanbanView') }}
</template>
<template v-else>
{{ $t('labels.createKanbanView') }}
</template>
</template>
<template v-else>
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateMapView') }}
</template>
<template v-else>
{{ $t('labels.duplicateView') }}
</template>
</template>
</div>
</template>
<div class="mt-2">
@ -264,9 +305,10 @@ onMounted(async () => {
v-model:value="form.fk_grp_col_id"
class="w-full nc-kanban-grouping-field-select"
:disabled="groupingFieldColumnId || isMetaLoading"
:loading="true"
placeholder="Select a Grouping Field"
not-found-content="No Single Select Field can be found. Please create one first."
:loading="isMetaLoading"
:options="viewSelectFieldOptions"
:placeholder="$t('placeholder.selectGroupField')"
:not-found-content="$t('placeholder.selectGroupFieldNotFound')"
/>
</a-form-item>
<a-form-item
@ -281,8 +323,8 @@ onMounted(async () => {
:options="viewSelectFieldOptions"
:disabled="groupingFieldColumnId || isMetaLoading"
:loading="isMetaLoading"
placeholder="Select a GeoData Field"
not-found-content="No GeoData Field can be found. Please create one first."
:placeholder="$t('placeholder.selectGeoField')"
:not-found-content="$t('placeholder.selectGeoFieldNotFound')"
/>
</a-form-item>
</a-form>
@ -292,9 +334,14 @@ onMounted(async () => {
{{ $t('general.cancel') }}
</NcButton>
<NcButton type="primary" :loading="isViewCreating" @click="onSubmit">
Create View
<template #loading> Creating View </template>
<NcButton
v-e="[form.copy_from_id ? 'a:view:duplicate' : 'a:view:create']"
type="primary"
:loading="isViewCreating"
@click="onSubmit"
>
{{ $t('labels.createView') }}
<template #loading> {{ $t('labels.creatingView') }}</template>
</NcButton>
</div>
</div>

16
packages/nc-gui/components/dlg/share-and-collaborate/Collaborate.vue

@ -4,6 +4,8 @@ const { loadUsers, users } = useManageUsers()
const formRef = ref()
const { t } = useI18n()
const useForm = Form.useForm
const validators = computed(() => {
return {
@ -11,12 +13,16 @@ const validators = computed(() => {
{
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => {
if (!value || value.length === 0) {
callback('Email is required')
callback(t('msg.error.signUpRules.emailReqd'))
return
}
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !validateEmail(e))
if (invalidEmails.length > 0) {
callback(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `)
callback(
`${
invalidEmails.length > 1 ? t('msg.error.signUpRules.invalidEmails') : t('msg.error.signUpRules.invalidEmail')
} ${invalidEmails.join(', ')} `,
)
} else {
callback()
}
@ -60,7 +66,7 @@ watch(
class="!rounded-md !ml-0.5"
validate-trigger="onBlur"
placeholder="Add people by email..."
data-testid="docs-share-dlg-share-project-collaborate-emails"
data-testid="docs-share-dlg-share-base-collaborate-emails"
/>
</a-form-item>
</div>
@ -71,9 +77,9 @@ watch(
v-model:value="invitationUsersData.role"
class="!rounded-md !bg-white"
dropdown-class-name="nc-dropdown-user-role !rounded-md"
data-testid="docs-share-dlg-share-project-collaborate-role"
data-testid="docs-share-dlg-share-base-collaborate-role"
>
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option">
<a-select-option v-for="(role, index) in baseRoles" :key="index" :value="role" class="nc-role-option">
<div
class="flex flex-row h-full justify-start items-center"
:data-testid="`nc-share-invite-user-role-option-${role}`"

4
packages/nc-gui/components/dlg/share-and-collaborate/ManageUsers.vue

@ -73,7 +73,7 @@ const rolesTypes = [
<template>
<div class="flex flex-col mx-4 h-112">
<div class="flex mt-2.5 mb-2.5 border-b-1 border-gray-50 pb-1.5" :style="{ fontWeight: 500 }">Manage Members</div>
<div class="flex mt-2.5 mb-2.5 text-xs" :style="{ fontWeight: 500 }">Project Owner</div>
<div class="flex mt-2.5 mb-2.5 text-xs font-bold">Base Owner</div>
<div v-if="owner" class="flex flex-row px-2 py-2 items-center gap-x-2 border-1 border-gray-100 rounded-md">
<a-avatar></a-avatar>
<div class="flex flex-col justify-center">
@ -91,7 +91,7 @@ const rolesTypes = [
</div>
</div>
<div class="flex flex-col mb-2 pr-0.5 h-96 overflow-y-auto users-list border-b-1 border-gray-100">
<div v-if="nonOwners.length === 0" class="text-xs mt-2">No users have access to this project</div>
<div v-if="nonOwners.length === 0" class="text-xs mt-2">No users have access to this base</div>
<div
v-for="user of nonOwners"
:key="user.id"

53
packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { extractSdkResponseErrorMsg, message, onMounted, storeToRefs, useDashboard, useNuxtApp, useProject } from '#imports'
import { extractSdkResponseErrorMsg, message, onMounted, storeToRefs, useBase, useDashboard, useNuxtApp } from '#imports'
interface ShareBase {
uuid?: string
@ -16,19 +16,21 @@ const { dashboardUrl } = useDashboard()
const { $api, $e } = useNuxtApp()
const base = ref<null | ShareBase>(null)
const sharedBase = ref<null | ShareBase>(null)
const { project } = storeToRefs(useProject())
const { base } = storeToRefs(useBase())
const url = computed(() => (base.value && base.value.uuid ? `${dashboardUrl.value}#/base/${base.value.uuid}` : ''))
const url = computed(() =>
sharedBase.value && sharedBase.value.uuid ? `${dashboardUrl.value}#/base/${sharedBase.value.uuid}` : '',
)
const loadBase = async () => {
try {
if (!project.value.id) return
if (!base.value.id) return
const res = await $api.project.sharedBaseGet(project.value.id)
const res = await $api.base.sharedBaseGet(base.value.id)
base.value = {
sharedBase.value = {
uuid: res.uuid,
url: res.url,
role: res.roles,
@ -40,14 +42,14 @@ const loadBase = async () => {
const createShareBase = async (role = ShareBaseRole.Viewer) => {
try {
if (!project.value.id) return
if (!base.value.id) return
const res = await $api.project.sharedBaseUpdate(project.value.id, {
const res = await $api.base.sharedBaseUpdate(base.value.id, {
roles: role,
})
base.value = res ?? {}
base.value!.role = role
sharedBase.value = res ?? {}
sharedBase.value!.role = role
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -57,10 +59,10 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
const disableSharedBase = async () => {
try {
if (!project.value.id) return
if (!base.value.id) return
await $api.project.sharedBaseDisable(project.value.id)
base.value = null
await $api.base.sharedBaseDisable(base.value.id)
sharedBase.value = null
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -69,12 +71,12 @@ const disableSharedBase = async () => {
}
onMounted(() => {
if (!base.value) {
if (!sharedBase.value) {
loadBase()
}
})
const isSharedBaseEnabled = computed(() => !!base.value?.uuid)
const isSharedBaseEnabled = computed(() => !!sharedBase.value?.uuid)
const isToggleBaseLoading = ref(false)
const isRoleToggleLoading = ref(false)
@ -96,12 +98,12 @@ const toggleSharedBase = async () => {
}
const onRoleToggle = async () => {
if (!base.value) return
if (!sharedBase.value) return
if (isRoleToggleLoading.value) return
isRoleToggleLoading.value = true
try {
if (base.value.role === ShareBaseRole.Viewer) {
if (sharedBase.value.role === ShareBaseRole.Viewer) {
await createShareBase(ShareBaseRole.Editor)
} else {
await createShareBase(ShareBaseRole.Viewer)
@ -118,16 +120,23 @@ const onRoleToggle = async () => {
<div class="flex flex-col py-2 px-3 gap-2 w-full" data-testid="nc-share-base-sub-modal">
<div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md">
<div class="flex flex-row w-full justify-between">
<div class="text-black font-medium">Enable Public Access</div>
<a-switch :checked="isSharedBaseEnabled" :loading="isToggleBaseLoading" class="ml-2" @click="toggleSharedBase" />
<div class="text-black font-medium">{{ $t('activity.enablePublicAccess') }}</div>
<a-switch
v-e="['c:share:base:enable:toggle']"
:checked="isSharedBaseEnabled"
:loading="isToggleBaseLoading"
class="ml-2"
@click="toggleSharedBase"
/>
</div>
<div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100">
<GeneralCopyUrl v-model:url="url" />
<div class="flex flex-row justify-between mt-3 bg-gray-50 px-3 py-2 rounded-md">
<div class="text-black">Editing access</div>
<div class="text-black">{{ $t('activity.editingAccess') }}</div>
<a-switch
v-e="['c:share:base:role:toggle']"
:loading="isRoleToggleLoading"
:checked="base?.role === ShareBaseRole.Editor"
:checked="sharedBase?.role === ShareBaseRole.Editor"
class="ml-2"
@click="onRoleToggle"
/>

32
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -268,8 +268,9 @@ const isPublicShareDisabled = computed(() => {
<div class="flex flex-col py-2 px-3 mb-1">
<div class="flex flex-col w-full mt-2.5 px-3 py-2.5 border-gray-200 border-1 rounded-md gap-y-2">
<div class="flex flex-row w-full justify-between py-0.5">
<div class="flex" :style="{ fontWeight: 500 }">Enable public viewing</div>
<div class="flex" :style="{ fontWeight: 500 }">{{ $t('activity.enabledPublicViewing') }}</div>
<a-switch
v-e="['c:share:view:enable:toggle']"
data-testid="share-view-toggle"
:checked="isPublicShared"
:loading="isUpdating.public"
@ -284,8 +285,9 @@ const isPublicShareDisabled = computed(() => {
</div>
<div class="flex flex-col justify-between mt-1 py-2 px-3 bg-gray-50 rounded-md">
<div class="flex flex-row justify-between">
<div class="flex text-black">Restrict access with password</div>
<div class="flex text-black">{{ $t('activity.restrictAccessWithPassword') }}</div>
<a-switch
v-e="['c:share:view:password:toggle']"
data-testid="share-password-toggle"
:checked="passwordProtected"
:loading="isUpdating.password"
@ -317,9 +319,10 @@ const isPublicShareDisabled = computed(() => {
"
class="flex flex-row justify-between"
>
<div class="flex text-black">Allow Download</div>
<div class="flex text-black">{{ $t('activity.allowDownload') }}</div>
<a-switch
v-model:checked="allowCSVDownload"
v-e="['c:share:view:allow-csv-download:toggle']"
data-testid="share-download-toggle"
:loading="isUpdating.download"
class="public-password-toggle !mt-0.25"
@ -328,23 +331,32 @@ const isPublicShareDisabled = computed(() => {
<div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-row justify-between">
<!-- use RTL orientation in form - todo: i18n -->
<div class="text-black">Survey Mode</div>
<a-switch v-model:checked="surveyMode" data-testid="nc-modal-share-view__surveyMode">
<div class="text-black">{{ $t('activity.surveyMode') }}</div>
<a-switch
v-model:checked="surveyMode"
v-e="['c:share:view:surver-mode:toggle']"
data-testid="nc-modal-share-view__surveyMode"
>
<!-- todo i18n -->
</a-switch>
</div>
<div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-row justify-between">
<div v-if="activeView?.type === ViewTypes.FORM && isEeUI" class="flex flex-row justify-between">
<!-- use RTL orientation in form - todo: i18n -->
<div class="text-black">RTL Orientation</div>
<a-switch v-model:checked="withRTL" data-testid="nc-modal-share-view__RTL">
<div class="text-black">{{ $t('activity.rtlOrientation') }}</div>
<a-switch
v-model:checked="withRTL"
v-e="['c:share:view:rtl-orientation:toggle']"
data-testid="nc-modal-share-view__RTL"
>
<!-- todo i18n -->
</a-switch>
</div>
<div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-col justify-between gap-y-1 bg-gray-50 rounded-md">
<!-- todo: i18n -->
<div class="flex flex-row justify-between">
<div class="text-black">Use Theme</div>
<div class="text-black">{{ $t('activity.useTheme') }}</div>
<a-switch
v-e="['c:share:view:theme:toggle']"
data-testid="share-theme-toggle"
:checked="viewTheme"
:loading="isUpdating.password"
@ -359,7 +371,7 @@ const isPublicShareDisabled = computed(() => {
data-testid="nc-modal-share-view__theme-picker"
class="!p-0 !bg-inherit"
:model-value="activeView?.meta?.theme?.primaryColor"
:colors="projectThemeColors"
:colors="baseThemeColors"
:row-size="9"
:advanced="false"
@input="onChangeTheme"

6
packages/nc-gui/components/dlg/share-and-collaborate/ShareProject.vue

@ -5,14 +5,14 @@ import ShareBase from './ShareBase.vue'
const { formStatus } = storeToRefs(useShare())
onMounted(async () => {
formStatus.value = 'project-collaborate'
formStatus.value = 'base-collaborate'
})
</script>
<template>
<div class="flex flex-col mx-4 mb-4 mt-2">
<a-tabs v-model:activeKey="formStatus">
<a-tab-pane key="project-collaborate">
<a-tab-pane key="base-collaborate">
<template #tab>
<div class="flex flex-row items-center text-xs px-2">
<MdiAccountPlusOutline class="mr-1" />
@ -25,7 +25,7 @@ onMounted(async () => {
<template #tab>
<div class="flex flex-row items-center text-xs px-2">
<MdiEarth class="mr-1" />
<div data-testid="db-share-base">Share Base</div>
<div data-testid="db-share-base">Share Source</div>
</div>
</template>
<ShareBase />

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

Loading…
Cancel
Save