Browse Source

Merge pull request #9373 from nocodb/develop

pull/9374/head 0.255.1
github-actions[bot] 3 months ago committed by GitHub
parent
commit
6bef6cae00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 54
      .github/workflows/release-timely-executables.yml
  2. 1345
      docker-compose/setup-script/noco.sh
  3. 6
      docker-compose/setup-script/tests/expects/install/redis.sh
  4. 6
      docker-compose/setup-script/tests/expects/install/scale.sh
  5. 6
      docker-compose/setup-script/tests/expects/install/watchtower.sh
  6. 6
      packages/nc-gui/assets/nc-icons/align-left.svg
  7. 11
      packages/nc-gui/assets/nc-icons/calendar.svg
  8. 12
      packages/nc-gui/assets/nc-icons/cell-duration.svg
  9. 6
      packages/nc-gui/assets/nc-icons/cell-select.svg
  10. 7
      packages/nc-gui/assets/nc-icons/cell-year.svg
  11. 8
      packages/nc-gui/assets/nc-icons/form.svg
  12. 11
      packages/nc-gui/assets/nc-icons/freshdesk.svg
  13. 5
      packages/nc-gui/assets/nc-icons/gallery.svg
  14. 6
      packages/nc-gui/assets/nc-icons/grid.svg
  15. 6
      packages/nc-gui/assets/nc-icons/kanban.svg
  16. 5
      packages/nc-gui/assets/nc-icons/lang-c.svg
  17. 17
      packages/nc-gui/assets/nc-icons/lang-java.svg
  18. 5
      packages/nc-gui/assets/nc-icons/lang-js.svg
  19. 58
      packages/nc-gui/assets/nc-icons/lang-nc-sdk.svg
  20. 3
      packages/nc-gui/assets/nc-icons/lang-node.svg
  21. 33
      packages/nc-gui/assets/nc-icons/lang-php.svg
  22. 8
      packages/nc-gui/assets/nc-icons/lang-python.svg
  23. 51
      packages/nc-gui/assets/nc-icons/lang-ruby.svg
  24. 38
      packages/nc-gui/assets/nc-icons/lang-shell.svg
  25. 10
      packages/nc-gui/assets/nc-icons/puzzle-outline.svg
  26. 4
      packages/nc-gui/assets/nc-icons/puzzle-solid.svg
  27. 2
      packages/nc-gui/components/account/AppStore.vue
  28. 36
      packages/nc-gui/components/account/Breadcrumb.vue
  29. 2
      packages/nc-gui/components/account/Profile.vue
  30. 2
      packages/nc-gui/components/account/ResetPassword.vue
  31. 129
      packages/nc-gui/components/account/Setup.vue
  32. 2
      packages/nc-gui/components/account/SignupSettings.vue
  33. 2
      packages/nc-gui/components/account/Token.vue
  34. 2
      packages/nc-gui/components/account/UserList.vue
  35. 13
      packages/nc-gui/components/account/setup/AppIcon.vue
  36. 175
      packages/nc-gui/components/account/setup/Config.vue
  37. 149
      packages/nc-gui/components/account/setup/List.vue
  38. 12
      packages/nc-gui/components/cell/DatePicker.vue
  39. 12
      packages/nc-gui/components/cell/DateTimePicker.vue
  40. 26
      packages/nc-gui/components/cell/User.vue
  41. 2
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  42. 8
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  43. 272
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  44. 18
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  45. 44
      packages/nc-gui/components/dashboard/TreeView/index.vue
  46. 31
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  47. 52
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  48. 10
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  49. 15
      packages/nc-gui/components/dlg/SharedBaseDuplicate.vue
  50. 108
      packages/nc-gui/components/dlg/TableCreate.vue
  51. 182
      packages/nc-gui/components/dlg/TableDescriptionUpdate.vue
  52. 113
      packages/nc-gui/components/dlg/ViewCreate.vue
  53. 170
      packages/nc-gui/components/dlg/ViewDescriptionUpdate.vue
  54. 226
      packages/nc-gui/components/extensions/Details.vue
  55. 176
      packages/nc-gui/components/extensions/Extension.vue
  56. 12
      packages/nc-gui/components/extensions/ExtensionMenu.vue
  57. 144
      packages/nc-gui/components/extensions/Market.vue
  58. 231
      packages/nc-gui/components/extensions/Pane.vue
  59. 27
      packages/nc-gui/components/general/IntegrationIcon.vue
  60. 10
      packages/nc-gui/components/general/UserIcon.vue
  61. 4
      packages/nc-gui/components/monaco/formula.ts
  62. 2
      packages/nc-gui/components/nc/Button.vue
  63. 7
      packages/nc-gui/components/nc/DatePicker.vue
  64. 19
      packages/nc-gui/components/nc/DateWeekSelector.vue
  65. 17
      packages/nc-gui/components/nc/List.vue
  66. 4
      packages/nc-gui/components/nc/Modal.vue
  67. 20
      packages/nc-gui/components/nc/MonthYearSelector.vue
  68. 16
      packages/nc-gui/components/nc/PageHeader.vue
  69. 19
      packages/nc-gui/components/nc/TimeSelector.vue
  70. 73
      packages/nc-gui/components/nc/Tooltip.vue
  71. 81
      packages/nc-gui/components/nc/form-builder/SampleModal.vue
  72. 265
      packages/nc-gui/components/nc/form-builder/index.vue
  73. 9
      packages/nc-gui/components/project/AccessSettings.vue
  74. 2
      packages/nc-gui/components/project/AllTables.vue
  75. 12
      packages/nc-gui/components/project/View.vue
  76. 31
      packages/nc-gui/components/smartsheet/Cell.vue
  77. 9
      packages/nc-gui/components/smartsheet/Details.vue
  78. 252
      packages/nc-gui/components/smartsheet/Form.vue
  79. 1
      packages/nc-gui/components/smartsheet/Gallery.vue
  80. 5
      packages/nc-gui/components/smartsheet/Kanban.vue
  81. 52
      packages/nc-gui/components/smartsheet/Topbar.vue
  82. 5
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  83. 17
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  84. 9
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  85. 13
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  86. 11
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  87. 55
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  88. 147
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  89. 4
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  90. 3
      packages/nc-gui/components/smartsheet/column/FormulaInputHelper.vue
  91. 4
      packages/nc-gui/components/smartsheet/column/RichLongTextDefaultValue.vue
  92. 336
      packages/nc-gui/components/smartsheet/details/Api.vue
  93. 32
      packages/nc-gui/components/smartsheet/details/Fields.vue
  94. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  95. 4
      packages/nc-gui/components/smartsheet/form/field-settings.vue
  96. 26
      packages/nc-gui/components/smartsheet/header/Cell.vue
  97. 2
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  98. 126
      packages/nc-gui/components/smartsheet/header/Menu.vue
  99. 17
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  100. 7
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  101. Some files were not shown because too many files have changed in this diff Show More

54
.github/workflows/release-timely-executables.yml

@ -81,7 +81,7 @@ jobs:
- name : Install dependencies and build executables
run: |
# install npm dependendencies
# install npm dependencies
npm i
# Copy sqlite binaries
@ -97,19 +97,49 @@ jobs:
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=x64 --target_libc=musl
./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=arm64 --target_libc=musl
# ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=armv6 --target_libc=unknown
# ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --fallback-to-build --target_arch=armv7 --target_libc=unknown
# clean up code to optimize size
npx modclean --patterns="default:*" --ignore="nc-lib-gui-daily/**,nocodb-daily/**,dayjs/**,express-status-monitor/**,sqlite3/**" --run
# build executables
npm run build
mkdir -p ./mac-dist
mkdir -p ./dist
# build darwin x64 executable
npm uninstall sharp
npm install --cpu=x64 --os=darwin sharp
npx --yes pkg@5.8.1 . --compress GZip -t node18-macos-x64 -o Noco-macos-x64
mv ./Noco-macos-x64 ./mac-dist/
# build darwin arm64 executable
npm uninstall sharp
npm install --cpu=arm64 --os=darwin sharp
npx --yes pkg@5.8.1 . --compress GZip -t node18-macos-arm64 -o Noco-macos-arm64
mv ./Noco-macos-arm64 ./mac-dist/
# build linux x64 executable
npm uninstall sharp
npm install --cpu=x64 --os=linux sharp
npx --yes pkg@5.8.1 . --compress GZip -t node18-linux-x64 -o Noco-linux-x64
mv ./Noco-linux-x64 ./dist/
mkdir ./mac-dist
mv ./dist/Noco-macos-arm64 ./mac-dist/
mv ./dist/Noco-macos-x64 ./mac-dist/
# build linux arm64 executable
npm uninstall sharp
npm install --cpu=arm64 --os=linux sharp
npx --yes pkg@5.8.1 . --compress GZip -t node18-linux-arm64 -o Noco-linux-arm64
mv ./Noco-linux-arm64 ./dist/
# build windows x64 executable
npm uninstall sharp
npm install --cpu=x64 --os=win32 sharp
npx --yes pkg@5.8.1 . --compress GZip -t node18-win-x64 -o Noco-win-x64.exe
mv ./Noco-win-x64.exe ./dist/
# build windows arm64 executable
npm uninstall sharp
npm install --cpu=arm64 --os=win32 sharp
npx --yes pkg@5.8.1 . --compress GZip -t node18-win-arm64 -o Noco-win-arm64.exe
mv ./Noco-win-arm64.exe ./dist/
- name: Upload executables(except mac executables) to release
uses: svenstaro/upload-release-action@v2
@ -144,7 +174,7 @@ jobs:
- uses: actions/upload-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
name: ${{ format('{0}-signed', github.event.inputs.tag || inputs.tag) }}
path: mac-dist
retention-days: 1
@ -155,7 +185,7 @@ jobs:
steps:
- uses: actions/download-artifact@master
with:
name: ${{ github.event.inputs.tag || inputs.tag }}
name: ${{ format('{0}-signed', github.event.inputs.tag || inputs.tag) }}
path: mac-dist
- name: Upload mac executables to release

1345
docker-compose/setup-script/noco.sh

File diff suppressed because it is too large Load Diff

6
docker-compose/setup-script/tests/expects/install/redis.sh

@ -21,6 +21,12 @@ send "\r"
expect "Do you want to enabled Redis for caching*"
send "Y\r"
expect "Do you want to enable Minio for file storage*"
send "\r"
expect "Enter the MinIO domain name*"
send "\r"
expect "Do you want to enabled Watchtower for automatic updates*"
send "\r"

6
docker-compose/setup-script/tests/expects/install/scale.sh

@ -21,6 +21,12 @@ send "\r"
expect "Do you want to enabled Redis for caching*"
send "Y\r"
expect "Do you want to enable Minio for file storage*"
send "Y\r"
expect "Enter the MinIO domain name*"
send "\r"
expect "Do you want to enabled Watchtower for automatic updates*"
send "\r"

6
docker-compose/setup-script/tests/expects/install/watchtower.sh

@ -21,6 +21,12 @@ send "\r"
expect "Do you want to enabled Redis for caching*"
send "\r"
expect "Do you want to enable Minio for file storage*"
send "\r"
expect "Enter the MinIO domain name*"
send "\r"
expect "Do you want to enabled Watchtower for automatic updates*"
send "Y\r"

6
packages/nc-gui/assets/nc-icons/align-left.svg

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 12H2" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 9.33301H2" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3333 6.66699H2" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 4H2" stroke="#1F293A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

11
packages/nc-gui/assets/nc-icons/calendar.svg

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2.66666H3.33333C2.59695 2.66666 2 3.26361 2 3.99999V13.3333C2 14.0697 2.59695 14.6667 3.33333 14.6667H12.6667C13.403 14.6667 14 14.0697 14 13.3333V3.99999C14 3.26361 13.403 2.66666 12.6667 2.66666Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.83331 9.12625V9.22625" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.83331 11.8V11.9" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 9.13V9.23" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 11.8V11.9" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.1667 9.13V9.23" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 6.66666H14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 1.33334V4.00001" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.33331 1.33334V4.00001" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

12
packages/nc-gui/assets/nc-icons/cell-duration.svg

@ -1,7 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9503 20C15.8163 20 18.9503 16.866 18.9503 13C18.9503 9.13401 15.8163 6 11.9503 6C8.08432 6 4.95032 9.13401 4.95032 13C4.95032 16.866 8.08432 20 11.9503 20Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.9503 10V13L13.4503 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 3L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.2929 7.12129C19.6834 6.73077 19.6834 6.0976 19.2929 5.70708C18.9024 5.31655 18.2692 5.31655 17.8787 5.70708L19.2929 7.12129ZM17.8787 5.70708L15.8787 7.70708L17.2929 9.12129L19.2929 7.12129L17.8787 5.70708Z" fill="currentColor"/>
<path d="M13.5 3H10.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 21C16.1421 21 19.5 17.6421 19.5 13.5C19.5 9.35786 16.1421 6 12 6C7.85786 6 4.5 9.35786 4.5 13.5C4.5 17.6421 7.85786 21 12 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.9503 10V13.5L13.4503 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 3L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.2929 7.12129C19.6834 6.73077 19.6834 6.0976 19.2929 5.70708C18.9024 5.31655 18.2692 5.31655 17.8787 5.70708L19.2929 7.12129ZM17.8787 5.70708L15.8787 7.70708L17.2929 9.12129L19.2929 7.12129L17.8787 5.70708Z" fill="currentColor"/>
<path d="M13.5 3H10.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 949 B

After

Width:  |  Height:  |  Size: 928 B

6
packages/nc-gui/assets/nc-icons/cell-select.svg

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="18" height="18" rx="9" stroke="currentColor" stroke-width="2"/>
<rect x="7" y="7" width="10" height="10" rx="5" fill="currentColor" stroke="currentColor" stroke-width="2"/>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect x="3" y="3" width="18" height="18" rx="9" stroke="currentColor" stroke-width="2"/>
<path d="M7.5 10.5L12 15L16.5 10.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 320 B

7
packages/nc-gui/assets/nc-icons/cell-year.svg

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M19 4H5C3.89543 4 3 4.89543 3 6V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V6C21 4.89543 20.1046 4 19 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 12.75L12 15.75M12 15.75V18.75M12 15.75L15 12.75" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 774 B

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

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 2.66666H12C12.3536 2.66666 12.6928 2.80713 12.9428 3.05718C13.1929 3.30723 13.3334 3.64637 13.3334 3.99999V13.3333C13.3334 13.6869 13.1929 14.0261 12.9428 14.2761C12.6928 14.5262 12.3536 14.6667 12 14.6667H4.00002C3.6464 14.6667 3.30726 14.5262 3.05721 14.2761C2.80716 14.0261 2.66669 13.6869 2.66669 13.3333V3.99999C2.66669 3.64637 2.80716 3.30723 3.05721 3.05718C3.30726 2.80713 3.6464 2.66666 4.00002 2.66666H5.33335" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.99998 1.33334H5.99998C5.63179 1.33334 5.33331 1.63182 5.33331 2.00001V3.33334C5.33331 3.70153 5.63179 4.00001 5.99998 4.00001H9.99998C10.3682 4.00001 10.6666 3.70153 10.6666 3.33334V2.00001C10.6666 1.63182 10.3682 1.33334 9.99998 1.33334Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 7L11 7" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 7L5.1 7" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 10L11 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 10L5.1 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

11
packages/nc-gui/assets/nc-icons/freshdesk.svg

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<g clip-path="url(#clip0_840_16415)">
<path d="M15.95 0.000129358H27.968C28.4991 -0.00414152 29.0257 0.0973827 29.5171 0.29878C30.0085 0.500176 30.4549 0.797413 30.8302 1.17315C31.2055 1.54889 31.5022 1.9956 31.7031 2.48724C31.9039 2.97887 32.0049 3.50557 32 4.03663V16.0501C32 24.8611 24.861 32.0001 16.05 32.0001H15.959C13.8639 32.002 11.7889 31.5909 9.8527 30.7904C7.91651 29.9898 6.15707 28.8156 4.67495 27.3347C3.19283 25.8539 2.01709 24.0954 1.21494 22.1599C0.412778 20.2244 -6.64988e-05 18.1498 8.0341e-09 16.0546C8.0341e-09 7.21863 7.127 0.0911294 15.95 0.000129358Z" fill="#25C16F"/>
<path d="M15.95 7.12695C11.9035 7.12695 8.62305 10.407 8.62305 14.454V19.436C8.65205 20.7695 9.72605 21.8435 11.0595 21.8725H13.132V16.1495H10.332V14.5495C10.502 11.5365 12.9955 9.17945 16.014 9.17945C19.0325 9.17945 21.52 11.532 21.69 14.5495V16.1495H18.85V21.8765H20.7225V21.9675C20.7025 23.2045 19.705 24.2025 18.4725 24.2175H16.236C16.054 24.2175 15.854 24.3085 15.854 24.49C15.8584 24.5898 15.9001 24.6845 15.9708 24.7552C16.0415 24.8259 16.1361 24.8675 16.236 24.872H18.486C20.0885 24.862 21.385 23.5655 21.395 21.963V21.781C21.9303 21.6604 22.4081 21.36 22.7488 20.93C23.0895 20.4999 23.2726 19.9661 23.2675 19.4175V14.5495C23.3585 10.4225 20.0855 7.13145 15.9405 7.13145L15.95 7.12695Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_840_16415">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

5
packages/nc-gui/assets/nc-icons/gallery.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 9.99999L10.6666 6.66666L3.33331 14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.66669 6.66666C6.21897 6.66666 6.66669 6.21894 6.66669 5.66666C6.66669 5.11437 6.21897 4.66666 5.66669 4.66666C5.1144 4.66666 4.66669 5.11437 4.66669 5.66666C4.66669 6.21894 5.1144 6.66666 5.66669 6.66666Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 828 B

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

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 9.33334H9.33331V14H14V9.33334Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66667 9.33334H2V14H6.66667V9.33334Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 2H9.33331V6.66667H14V2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66667 2H2V6.66667H6.66667V2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 653 B

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

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 5L5 10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5L8 11" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 5L11 8" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 716 B

5
packages/nc-gui/assets/nc-icons/lang-c.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M20.3253 6.61167L12.5097 2.11327C12.3803 2.03236 12.2023 2 12.0081 2C11.8139 2 11.6359 2.04854 11.5065 2.11327L3.73946 6.62785C3.46438 6.78966 3.2702 7.19419 3.2702 7.50164V16.5146C3.2702 16.6926 3.30257 16.903 3.43202 17.081L20.7136 7.04856C20.6165 6.85438 20.4709 6.70875 20.3253 6.61167Z" fill="#659AD3"/>
<path d="M3.38348 17.0646C3.46439 17.1941 3.57766 17.3073 3.69093 17.3721L11.4903 21.8866C11.6198 21.9675 11.7978 21.9999 11.9919 21.9999C12.1861 21.9999 12.3641 21.9514 12.4935 21.8866L20.2606 17.3721C20.5356 17.2103 20.7298 16.8057 20.7298 16.4983V7.4853C20.7298 7.33967 20.7136 7.17786 20.6327 7.03223L3.38348 17.0646Z" fill="#03599C"/>
<path d="M15.4547 13.9579C14.7751 15.1554 13.4806 15.9644 12.0081 15.9644C9.82365 15.9644 8.04371 14.1845 8.04371 12C8.04371 9.81553 9.82365 8.03559 12.0081 8.03559C13.4806 8.03559 14.7751 8.84465 15.4547 10.0583L17.5583 8.84465C16.458 6.91908 14.3868 5.6084 12.0081 5.6084C8.4806 5.6084 5.61652 8.47249 5.61652 12C5.61652 15.5275 8.4806 18.3916 12.0081 18.3916C14.3706 18.3916 16.4418 17.0971 17.5421 15.1877L15.4547 13.9579Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

17
packages/nc-gui/assets/nc-icons/lang-java.svg

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<g clip-path="url(#clip0_1374_6489)">
<path d="M8.69044 18.5059C8.69044 18.5059 7.77494 19.0383 9.34197 19.2184C11.2404 19.435 12.2106 19.4039 14.3027 19.008C14.3027 19.008 14.8527 19.3528 15.6209 19.6516C10.931 21.6615 5.00682 19.5351 8.69044 18.5059Z" fill="#5382A1"/>
<path d="M8.11733 15.8828C8.11733 15.8828 7.09051 16.6429 8.6587 16.8051C10.6866 17.0143 12.2881 17.0314 15.0594 16.4978C15.0594 16.4978 15.4427 16.8864 16.0454 17.0989C10.3751 18.757 4.05939 17.2296 8.11733 15.8828Z" fill="#5382A1"/>
<path d="M12.9485 11.434C14.1041 12.7644 12.6449 13.9616 12.6449 13.9616C12.6449 13.9616 15.5791 12.4469 14.2316 10.5501C12.973 8.78124 12.0078 7.90236 17.2328 4.87207C17.2328 4.87207 9.03134 6.92042 12.9485 11.434Z" fill="#E76F00"/>
<path d="M19.1512 20.4458C19.1512 20.4458 19.8287 21.004 18.4051 21.4359C15.6981 22.2559 7.13813 22.5036 4.76021 21.4685C3.90541 21.0967 5.5084 20.5806 6.01264 20.4723C6.53852 20.3583 6.83903 20.3795 6.83903 20.3795C5.88841 19.7099 0.69458 21.6945 4.20082 22.2628C13.7629 23.8135 21.6315 21.5646 19.1512 20.4458Z" fill="#5382A1"/>
<path d="M9.13068 13.165C9.13068 13.165 4.77654 14.1992 7.58877 14.5748C8.77618 14.7337 11.1433 14.6978 13.3481 14.513C15.1501 14.361 16.9594 14.0378 16.9594 14.0378C16.9594 14.0378 16.324 14.31 15.8643 14.6238C11.4428 15.7867 2.90119 15.2457 5.36021 14.0563C7.43981 13.051 9.13068 13.165 9.13068 13.165Z" fill="#5382A1"/>
<path d="M16.9415 17.531C21.4362 15.1954 19.358 12.9509 17.9075 13.2533C17.5519 13.3272 17.3934 13.3914 17.3934 13.3914C17.3934 13.3914 17.5254 13.1846 17.7775 13.0951C20.6471 12.0862 22.854 16.0706 16.8511 17.6487C16.8511 17.6487 16.9207 17.5866 16.9415 17.531Z" fill="#5382A1"/>
<path d="M14.2316 -0.0214844C14.2316 -0.0214844 16.7208 2.4686 11.8707 6.29763C7.98132 9.36918 10.9838 11.1205 11.8691 13.1214C9.59877 11.0731 7.93269 9.26994 9.05043 7.59172C10.691 5.12824 15.236 3.93386 14.2316 -0.0214844Z" fill="#E76F00"/>
<path d="M9.57234 23.8597C13.8867 24.1359 20.5119 23.7065 20.6688 21.665C20.6688 21.665 20.3672 22.4389 17.1032 23.0535C13.4208 23.7465 8.8791 23.6656 6.18536 23.2215C6.18536 23.2215 6.73681 23.6779 9.57234 23.8597Z" fill="#5382A1"/>
</g>
<defs>
<clipPath id="clip0_1374_6489">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

5
packages/nc-gui/assets/nc-icons/lang-js.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M2 5C2 3.34315 3.34315 2 5 2H19C20.6569 2 22 3.34315 22 5V19C22 20.6569 20.6569 22 19 22H5C3.34315 22 2 20.6569 2 19V5Z" fill="#F7DF1E"/>
<path d="M6.7847 19.385L6.92374 19.3009C7.769 18.7893 8.80928 19.4293 9.79727 19.4293C10.4766 19.4293 10.9049 19.1635 10.9049 18.1299V12.1341C10.9049 11.5632 11.3677 11.1003 11.9386 11.1003C12.5095 11.1003 12.9724 11.5632 12.9724 12.1341V18.1591C12.9724 20.3004 11.7172 21.2751 9.8859 21.2751C8.23202 21.2751 7.27199 20.4185 6.78467 19.3848" fill="black"/>
<path d="M14.6298 19.9543C14.2745 19.5515 14.4587 18.9531 14.9235 18.6839C15.3991 18.4086 15.9838 18.5936 16.4019 18.9502C16.76 19.2556 17.2098 19.4441 17.8166 19.4441C18.6732 19.4441 19.2195 19.0158 19.2195 18.4251C19.2195 17.7163 18.6584 17.4652 17.7132 17.0518L17.1965 16.8302C15.7048 16.1953 14.7154 15.3978 14.7154 13.7143C14.7154 12.1636 15.8968 10.9822 17.7428 10.9822C18.7465 10.9822 19.5349 11.2492 20.154 11.9081C20.5 12.2764 20.3486 12.85 19.9234 13.1231C19.4618 13.4195 18.8839 13.1901 18.4001 12.9315C18.2095 12.8297 17.995 12.7839 17.7428 12.7839C17.1374 12.7839 16.7534 13.1679 16.7534 13.6699C16.7534 14.2902 17.1374 14.5413 18.0233 14.9253L18.5402 15.1468C20.2975 15.9 21.287 16.6678 21.287 18.3956C21.287 20.2564 19.825 21.2753 17.861 21.2753C16.3937 21.2753 15.3232 20.7405 14.6298 19.9543Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

58
packages/nc-gui/assets/nc-icons/lang-nc-sdk.svg

@ -0,0 +1,58 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_277_5004)">
<path
d="M9.22072 11.3245V15.1062C9.22072 15.7812 8.73554 16.3287 8.13717 16.3276L6.37549 16.324L6.38475 11.9466V11.8051C6.39967 10.7102 7.2908 10.0126 8.1135 10.3032C8.31313 10.3743 8.49218 10.506 8.64036 10.6727L9.22072 11.3245Z"
fill="url(#paint0_linear_277_5004)" />
<path
d="M13.5244 11.6043V7.64771C13.5244 6.97216 14.0101 6.42472 14.6085 6.42626L16.3702 6.42935L16.3609 10.9822V11.1232C16.3455 12.2186 15.4543 12.9163 14.6322 12.6251C14.432 12.5546 14.2535 12.4229 14.1053 12.2562L13.5244 11.6043Z"
fill="url(#paint1_linear_277_5004)" />
<path
d="M16.3581 10.8224V14.125C16.3663 15.7164 14.8969 16.7933 13.7408 16.1203C13.6003 16.0385 13.4737 15.9299 13.3595 15.8069L9.76926 11.9399L9.22079 11.3245L8.64043 10.6727C8.49225 10.506 8.3132 10.3742 8.11357 10.3032C7.29087 10.0125 6.39974 10.7102 6.38482 11.8051V11.7686L6.38379 8.99485V8.53899C6.38379 6.8195 8.09144 5.85223 9.26247 6.91006L13.9723 12.0551C14.0855 12.1791 14.2203 12.2836 14.3741 12.3515C15.258 12.7446 16.3581 11.9826 16.3581 10.8224Z"
fill="url(#paint2_linear_277_5004)" />
<path
d="M16.3581 13.9084V14.125C16.3663 15.7164 14.8969 16.7933 13.7408 16.1203C13.6003 16.0385 13.4737 15.9299 13.3595 15.8069L9.76926 11.9399L9.22079 11.3245L8.64043 10.6727C8.49225 10.506 8.3132 10.3742 8.11357 10.3032C7.29087 10.0125 6.39974 10.7102 6.38482 11.8051V11.7686L6.38379 8.99485V8.95574C6.59885 7.94988 7.74158 7.50946 8.45623 8.28329L14.2841 14.5881C15.0008 15.364 16.1471 14.9184 16.3581 13.9084Z"
fill="url(#paint3_linear_277_5004)" />
<path
d="M16.3183 19.8173C16.3183 19.5133 16.3574 19.219 16.4304 18.9386H4.86736C4.28288 18.9386 3.80387 18.4601 3.80387 17.8746V4.86726C3.80387 4.28278 4.28288 3.80377 4.86736 3.80377H17.8752C18.4602 3.80377 18.9392 4.28278 18.9392 4.86726V16.4514C19.2114 16.383 19.4959 16.3465 19.7891 16.3465C20.12 16.3465 20.4395 16.3933 20.7425 16.4802V4.27094C20.7425 3.02172 19.7207 1.9999 18.471 1.9999H4.27156C3.02181 1.9999 2 3.02172 2 4.27094V18.4714C2 19.7201 3.02181 20.7424 4.27156 20.7424H16.4438C16.3625 20.4476 16.3183 20.1379 16.3183 19.8173Z"
fill="url(#paint4_linear_277_5004)" />
<path
d="M21.9716 19.8175C21.9716 21.0229 20.9945 22 19.789 22C18.5835 22 17.606 21.0229 17.606 19.8175C17.606 18.612 18.5835 17.6344 19.789 17.6344C20.9945 17.6344 21.9716 18.612 21.9716 19.8175Z"
fill="url(#paint5_linear_277_5004)" />
</g>
<defs>
<linearGradient id="paint0_linear_277_5004" x1="7.44309" y1="13.5313" x2="1.78195" y2="16.536"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
<linearGradient id="paint1_linear_277_5004" x1="15.1451" y1="9.40218" x2="21.3313" y2="11.5717"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
<linearGradient id="paint2_linear_277_5004" x1="7.81824" y1="11.6739" x2="18.63" y2="10.8209"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#353495" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
<linearGradient id="paint3_linear_277_5004" x1="3.58332" y1="8.37075" x2="15.5202" y2="14.3831"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
<linearGradient id="paint4_linear_277_5004" x1="5.7662" y1="5.79286" x2="22.8538" y2="22.7987"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
<linearGradient id="paint5_linear_277_5004" x1="22.7068" y1="21.5441" x2="16.9551" y2="18.14"
gradientUnits="userSpaceOnUse">
<stop stop-color="#ED0029" />
<stop offset="1" stop-color="#F43760" />
</linearGradient>
<clipPath id="clip0_277_5004">
<rect width="20" height="20" fill="white" transform="translate(2 2)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

3
packages/nc-gui/assets/nc-icons/lang-node.svg

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12.325 23.2687C12.0044 23.2687 11.7051 23.1832 11.4272 23.0335L8.58407 21.3448C8.15653 21.1096 8.3703 21.0241 8.49856 20.9814C9.07573 20.789 9.18262 20.7462 9.78117 20.4042C9.8453 20.3614 9.93081 20.3828 9.99494 20.4256L12.1754 21.7296C12.2609 21.7723 12.3678 21.7723 12.4319 21.7296L20.9613 16.7915C21.0468 16.7488 21.0895 16.6632 21.0895 16.5564V6.70164C21.0895 6.59475 21.0468 6.50925 20.9613 6.46649L12.4319 1.54982C12.3464 1.50707 12.2395 1.50707 12.1754 1.54982L3.64602 6.46649C3.56051 6.50925 3.51776 6.61613 3.51776 6.70164V16.5564C3.51776 16.6419 3.56051 16.7488 3.64602 16.7915L5.97609 18.1382C7.23732 18.7795 8.02827 18.0314 8.02827 17.2832V7.55671C8.02827 7.42845 8.13515 7.30019 8.28479 7.30019H9.37501C9.50327 7.30019 9.63153 7.40707 9.63153 7.55671V17.2832C9.63153 18.9719 8.71233 19.9553 7.10906 19.9553C6.6174 19.9553 6.23261 19.9553 5.14239 19.4209L2.89783 18.1382C2.34203 17.8176 2 17.219 2 16.5777V6.72301C2 6.08171 2.34203 5.48316 2.89783 5.16251L11.4272 0.224457C11.9616 -0.0748189 12.6884 -0.0748189 13.2228 0.224457L21.7522 5.16251C22.308 5.48316 22.65 6.08171 22.65 6.72301V16.5777C22.65 17.219 22.308 17.8176 21.7522 18.1382L13.2228 23.0763C12.9449 23.2046 12.6243 23.2687 12.325 23.2687ZM14.9544 16.4922C11.2134 16.4922 10.4439 14.7821 10.4439 13.3285C10.4439 13.2002 10.5507 13.0719 10.7004 13.0719H11.812C11.9402 13.0719 12.0471 13.1574 12.0471 13.2857C12.2181 14.4187 12.7098 14.9745 14.9757 14.9745C16.7714 14.9745 17.541 14.5683 17.541 13.6064C17.541 13.0506 17.3272 12.6444 14.5268 12.3665C12.1968 12.1314 10.7431 11.6183 10.7431 9.75852C10.7431 8.027 12.1968 7.00091 14.6337 7.00091C17.3699 7.00091 18.7167 7.94149 18.8877 9.99367C18.8877 10.0578 18.8663 10.1219 18.8236 10.1861C18.7808 10.2288 18.7167 10.2716 18.6526 10.2716H17.541C17.4341 10.2716 17.3272 10.1861 17.3058 10.0792C17.0493 8.90345 16.3866 8.51867 14.6337 8.51867C12.667 8.51867 12.4319 9.20273 12.4319 9.71577C12.4319 10.3357 12.7098 10.5281 15.3605 10.8701C17.9899 11.2121 19.2297 11.7038 19.2297 13.5422C19.2084 15.4234 17.6692 16.4922 14.9544 16.4922Z" fill="#539E43"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

33
packages/nc-gui/assets/nc-icons/lang-php.svg

@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<mask id="mask0_1374_6435" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="4" width="24" height="15">
<path d="M0.47998 11.7592C0.47998 15.6923 5.63777 18.8809 12 18.8809C18.3622 18.8809 23.52 15.6923 23.52 11.7592C23.52 7.82603 18.3622 4.63748 12 4.63748C5.63777 4.63748 0.47998 7.82608 0.47998 11.7592Z" fill="white"/>
</mask>
<g mask="url(#mask0_1374_6435)">
<path d="M0.47998 11.7592C0.47998 15.6923 5.63777 18.8809 12 18.8809C18.3622 18.8809 23.52 15.6923 23.52 11.7592C23.52 7.82603 18.3622 4.63748 12 4.63748C5.63777 4.63748 0.47998 7.82608 0.47998 11.7592Z" fill="url(#paint0_radial_1374_6435)"/>
</g>
<mask id="mask1_1374_6435" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="3" width="24" height="17">
<path d="M0 3.82626H24V19.6914H0V3.82626Z" fill="white"/>
</mask>
<g mask="url(#mask1_1374_6435)">
<path d="M12 18.3525C18.1137 18.3525 23.07 15.4008 23.07 11.7597C23.07 8.11854 18.1137 5.16684 12 5.16684C5.88624 5.16684 0.929993 8.11854 0.929993 11.7597C0.929993 15.4008 5.88624 18.3525 12 18.3525Z" fill="#777BB3"/>
</g>
<mask id="mask2_1374_6435" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="3" width="24" height="17">
<path d="M0 3.82626H24V19.6914H0V3.82626Z" fill="white"/>
</mask>
<g mask="url(#mask2_1374_6435)">
<path d="M6.73895 12.576C7.24166 12.576 7.61695 12.467 7.85437 12.2523C8.08929 12.0398 8.25154 11.6715 8.33658 11.1578C8.41587 10.6776 8.38566 10.3423 8.24683 10.1614C8.10487 9.97659 7.79795 9.88287 7.33462 9.88287H6.53129L6.08587 12.576H6.73891L6.73895 12.576ZM4.11129 15.8935C4.074 15.8935 4.03858 15.8739 4.01487 15.84C4.00315 15.8233 3.99471 15.8037 3.99018 15.7827C3.98564 15.7617 3.98511 15.7398 3.98862 15.7185L5.16895 8.58168C5.17449 8.54821 5.18977 8.51806 5.21216 8.49637C5.23454 8.47469 5.26265 8.46284 5.29166 8.46284H7.83558C8.63508 8.46284 9.23016 8.71746 9.6042 9.22118C9.98024 9.72701 10.0964 10.4342 9.94933 11.3228C9.88949 11.6849 9.78658 12.0211 9.64349 12.3222C9.50016 12.6236 9.31083 12.9025 9.08058 13.1512C8.80491 13.4544 8.49291 13.6743 8.15408 13.8037C7.82062 13.9313 7.39241 13.9961 6.88125 13.9961H5.85116L5.557 15.7747C5.55146 15.8081 5.5362 15.8383 5.51382 15.86C5.49144 15.8817 5.46334 15.8935 5.43433 15.8936H4.11129V15.8935Z" fill="black"/>
<path d="M6.63435 10.0294H7.33468C7.89385 10.0294 8.08814 10.1736 8.15414 10.2595C8.26364 10.4022 8.28431 10.703 8.21393 11.1295C8.13485 11.6069 7.98827 11.9454 7.77818 12.1355C7.5631 12.3301 7.21343 12.4287 6.73902 12.4287H6.23752L6.63435 10.0294ZM7.8356 8.31552H5.29168C5.23366 8.31553 5.17745 8.33926 5.13269 8.38263C5.08792 8.426 5.05738 8.48632 5.04631 8.55326L3.86598 15.6902C3.85895 15.7328 3.86001 15.7766 3.86909 15.8186C3.87816 15.8606 3.89503 15.8997 3.91848 15.9331C3.94193 15.9666 3.97139 15.9935 4.00473 16.012C4.03807 16.0304 4.07448 16.04 4.11135 16.04H5.43427C5.49229 16.04 5.54851 16.0163 5.59329 15.9729C5.63807 15.9296 5.66861 15.8692 5.67968 15.8023L5.95418 14.1426H6.88127C7.40552 14.1426 7.84685 14.0754 8.19293 13.943C8.54877 13.8071 8.87627 13.5767 9.16602 13.2582C9.40568 12.9991 9.60352 12.7082 9.75268 12.3936C9.90185 12.079 10.0098 11.728 10.0722 11.3505C10.227 10.4151 10.1012 9.66597 9.69843 9.12421C9.2996 8.58763 8.67285 8.31557 7.83568 8.31557M5.93431 12.7226H6.73902C7.27235 12.7226 7.66968 12.6045 7.93068 12.3683C8.19168 12.1321 8.36785 11.7379 8.45935 11.1855C8.54685 10.655 8.50702 10.2807 8.33977 10.0626C8.17252 9.84455 7.83727 9.73555 7.33477 9.73555H6.42835L5.93435 12.7225M7.83573 8.60932C8.60102 8.60932 9.15931 8.84534 9.51022 9.31738C9.86114 9.78942 9.96677 10.4484 9.82677 11.2944C9.76914 11.6429 9.67168 11.9615 9.53447 12.2502C9.39727 12.5389 9.21773 12.8032 8.99606 13.0427C8.73189 13.3332 8.43843 13.5402 8.11552 13.6636C7.7926 13.7872 7.38135 13.8488 6.88135 13.8488H5.74818L5.43443 15.7462H4.11152L5.29168 8.60932H7.83564" fill="white"/>
<path d="M12.9827 13.996C12.9454 13.996 12.91 13.9765 12.8863 13.9426C12.8626 13.9087 12.8529 13.8642 12.86 13.8211L13.3821 10.6633C13.4318 10.363 13.4195 10.1474 13.3476 10.0563C13.3036 10.0007 13.1715 9.90729 12.7807 9.90729H11.8348L11.1783 13.8771C11.1728 13.9106 11.1575 13.9407 11.1351 13.9624C11.1127 13.9841 11.0846 13.996 11.0556 13.996H9.7431C9.72467 13.996 9.70646 13.9912 9.68979 13.982C9.67311 13.9727 9.65838 13.9593 9.64666 13.9425C9.63493 13.9258 9.62649 13.9063 9.62195 13.8853C9.61741 13.8643 9.61688 13.8424 9.6204 13.8211L10.8007 6.68422C10.8063 6.65075 10.8215 6.6206 10.8439 6.59891C10.8663 6.57723 10.8944 6.56537 10.9234 6.56538H12.2359C12.2544 6.56538 12.2726 6.57017 12.2893 6.57941C12.3059 6.58865 12.3207 6.60211 12.3324 6.61883C12.3441 6.63554 12.3525 6.6551 12.3571 6.6761C12.3616 6.6971 12.3622 6.71902 12.3586 6.74029L12.0738 8.46283H13.0914C13.8666 8.46283 14.3922 8.62344 14.6985 8.95387C15.011 9.29095 15.1079 9.82988 14.9879 10.556L14.4388 13.8772C14.4332 13.9107 14.418 13.9408 14.3956 13.9625C14.3732 13.9842 14.3451 13.9961 14.3161 13.9961H12.9827L12.9827 13.996Z" fill="black"/>
<path d="M12.2359 6.41816H10.9234C10.8654 6.41816 10.8091 6.44188 10.7644 6.48525C10.7196 6.52863 10.689 6.58895 10.678 6.65589L9.49763 13.7928C9.4906 13.8353 9.49166 13.8791 9.50074 13.9211C9.50982 13.9631 9.52669 14.0022 9.55015 14.0357C9.5736 14.0691 9.60306 14.096 9.63641 14.1145C9.66976 14.133 9.70617 14.1426 9.74304 14.1426H11.0555C11.1136 14.1426 11.1698 14.1189 11.2146 14.0755C11.2593 14.0321 11.2899 13.9718 11.301 13.9048L11.9378 10.0539H12.7807C13.1708 10.0539 13.2527 10.1518 13.256 10.156C13.2796 10.1859 13.3108 10.3245 13.2594 10.6349L12.7373 13.7928C12.7303 13.8353 12.7313 13.8791 12.7404 13.9211C12.7495 13.9631 12.7664 14.0022 12.7898 14.0357C12.8133 14.0691 12.8427 14.096 12.8761 14.1145C12.9094 14.133 12.9458 14.1426 12.9827 14.1426H14.316C14.3741 14.1426 14.4303 14.1189 14.4751 14.0755C14.5198 14.0321 14.5504 13.9718 14.5615 13.9048L15.1106 10.5837C15.2395 9.80415 15.1293 9.21929 14.7831 8.84538C14.4528 8.48891 13.8995 8.31556 13.0915 8.31556H12.2255L12.4814 6.76798C12.4885 6.72544 12.4874 6.6816 12.4783 6.63959C12.4692 6.59759 12.4524 6.55847 12.4289 6.52503C12.4055 6.49159 12.376 6.46466 12.3427 6.44618C12.3093 6.4277 12.2729 6.41811 12.236 6.41811M12.236 6.71191L11.9223 8.60936H13.0915C13.8272 8.60936 14.3347 8.76028 14.614 9.06172C14.8933 9.36315 14.9769 9.85204 14.8653 10.5277L14.3162 13.8488H12.9828L13.5049 10.691C13.5643 10.3318 13.5425 10.0868 13.4393 9.95614C13.3362 9.8255 13.1166 9.76008 12.7808 9.76008H11.7317L11.0556 13.8488H9.74313L10.9235 6.71191H12.236" fill="white"/>
<path d="M17.0646 12.576C17.5674 12.576 17.9426 12.467 18.1801 12.2523C18.415 12.0398 18.5772 11.6716 18.6623 11.1578C18.7416 10.6776 18.7114 10.3423 18.5725 10.1614C18.4306 9.97659 18.1236 9.88287 17.6603 9.88287H16.857L16.4116 12.576H17.0646L17.0646 12.576ZM14.4371 15.8935C14.3998 15.8935 14.3644 15.8739 14.3406 15.84C14.3289 15.8233 14.3205 15.8037 14.3159 15.7827C14.3114 15.7617 14.3108 15.7398 14.3144 15.7185L15.4947 8.58168C15.5002 8.54821 15.5155 8.51804 15.5379 8.49636C15.5603 8.47468 15.5884 8.46282 15.6174 8.46284H18.1613C18.9609 8.46284 19.5559 8.71746 19.93 9.22118C20.306 9.72701 20.4221 10.4341 20.2751 11.3228C20.2152 11.6849 20.1123 12.0211 19.9692 12.3222C19.8259 12.6236 19.6365 12.9025 19.4063 13.1512C19.1306 13.4544 18.8186 13.6743 18.4798 13.8037C18.1463 13.9313 17.7181 13.9961 17.2069 13.9961H16.1767L15.8827 15.7747C15.8772 15.8081 15.8619 15.8383 15.8395 15.86C15.8171 15.8817 15.789 15.8936 15.7599 15.8936H14.437L14.4371 15.8935Z" fill="black"/>
<path d="M16.9601 10.0294H17.6604C18.2196 10.0294 18.4138 10.1736 18.4798 10.2595C18.5894 10.4022 18.6101 10.703 18.5396 11.1294C18.4606 11.6069 18.3139 11.9454 18.1039 12.1355C17.8888 12.3301 17.5391 12.4287 17.0647 12.4287H16.5633L16.9601 10.0294ZM18.1613 8.31552H15.6174C15.5594 8.31553 15.5032 8.33926 15.4584 8.38263C15.4136 8.426 15.3831 8.48632 15.372 8.55326L14.1917 15.6902C14.1847 15.7328 14.1858 15.7766 14.1948 15.8186C14.2039 15.8606 14.2208 15.8997 14.2442 15.9331C14.2677 15.9666 14.2971 15.9935 14.3305 16.012C14.3638 16.0304 14.4002 16.04 14.4371 16.04H15.76C15.818 16.04 15.8743 16.0163 15.919 15.9729C15.9638 15.9296 15.9944 15.8692 16.0054 15.8023L16.2799 14.1426H17.207C17.7312 14.1426 18.1726 14.0754 18.5186 13.943C18.8745 13.8071 19.202 13.5767 19.4918 13.2581C19.7315 12.9991 19.9288 12.7082 20.0784 12.3936C20.228 12.079 20.3355 11.728 20.3979 11.3505C20.5527 10.415 20.4269 9.66592 20.0241 9.12416C19.6253 8.58763 18.9986 8.31557 18.1614 8.31557M16.2601 12.7226H17.0647C17.5981 12.7226 17.9954 12.6045 18.2564 12.3683C18.5174 12.1321 18.6936 11.7379 18.7851 11.1855C18.8726 10.655 18.8328 10.2807 18.6655 10.0626C18.4982 9.84455 18.163 9.73555 17.6605 9.73555H16.7541L16.2601 12.7225M18.1614 8.60932C18.9267 8.60932 19.485 8.84534 19.8359 9.31738C20.1868 9.78942 20.2925 10.4484 20.1525 11.2944C20.0948 11.6429 19.9974 11.9615 19.8602 12.2502C19.723 12.5389 19.5434 12.8032 19.3218 13.0427C19.0576 13.3332 18.7641 13.5402 18.4412 13.6636C18.1183 13.7872 17.7071 13.8488 17.2071 13.8488H16.0739L15.7601 15.7462H14.4372L15.6176 8.60937H18.1615" fill="white"/>
</g>
<defs>
<radialGradient id="paint0_radial_1374_6435" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.39665 7.14128) scale(15.1274 17.7777)">
<stop stop-color="#AEB2D5"/>
<stop offset="0.3" stop-color="#AEB2D5"/>
<stop offset="0.75" stop-color="#484C89"/>
<stop offset="1" stop-color="#484C89"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

8
packages/nc-gui/assets/nc-icons/lang-python.svg

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M2 11.6171C2.00823 11.5902 2.01481 11.5625 2.01851 11.5348C2.03373 11.2506 2.03702 10.9632 2.06788 10.6787C2.11601 9.75736 2.42619 8.86867 2.96263 8.11449C3.20445 7.79018 3.51969 7.52663 3.88301 7.34502C4.24632 7.16341 4.64757 7.06882 5.05449 7.06885H11.7127C11.8422 7.06885 11.8422 7.06885 11.8422 6.94043V6.48181C11.8422 6.38724 11.8143 6.36604 11.725 6.36604H7.19284C7.1155 6.36604 7.08794 6.34443 7.08794 6.26494V4.02444C7.10275 3.69994 7.23933 3.39257 7.47052 3.16224C7.77756 2.8384 8.15987 2.5939 8.58412 2.45006C9.10224 2.26545 9.64092 2.14336 10.1885 2.08642C10.937 2.00173 11.6914 1.97923 12.4437 2.01916C13.2549 2.03424 14.0575 2.18182 14.8198 2.45617C15.3427 2.63092 15.8048 2.94896 16.1527 3.37341C16.4324 3.73005 16.5706 4.17581 16.5414 4.62655C16.5229 5.00568 16.5414 5.38765 16.5414 5.76963V7.88539C16.5414 8.30079 16.5414 8.71946 16.5229 9.13201C16.5199 9.5794 16.38 10.0153 16.1215 10.3822C15.8631 10.7491 15.4984 11.0296 15.0757 11.1866C14.6656 11.353 14.2259 11.4361 13.7827 11.4308H9.06832C8.5698 11.4214 8.07843 11.5494 7.64906 11.8006C7.09997 12.1425 6.7088 12.685 6.56014 13.3105C6.47068 13.6259 6.42596 13.952 6.42726 14.2795V16.3977C6.42726 16.5384 6.42726 16.5355 6.28534 16.5384C5.77317 16.5384 5.261 16.5539 4.74267 16.5384C4.23539 16.5181 3.75154 16.3214 3.37606 15.9827C2.9396 15.5894 2.61419 15.09 2.43195 14.5339C2.24725 14.0284 2.12997 13.5012 2.0831 12.9656C2.05225 12.6232 2.0399 12.2808 2.01851 11.9416C2.0145 11.9036 2.00832 11.8659 2 11.8287V11.6171Z" fill="#3772A4"/>
<path d="M8.36359 4.73391C8.36486 4.91343 8.41927 5.08855 8.51993 5.23719C8.6206 5.38584 8.76302 5.50135 8.92924 5.56916C9.09547 5.63698 9.27805 5.65406 9.45397 5.61825C9.62988 5.58245 9.79126 5.49536 9.91775 5.36797C10.0442 5.24058 10.1302 5.07859 10.1647 4.90242C10.1993 4.72626 10.1809 4.54381 10.1119 4.37807C10.0429 4.21233 9.92642 4.07074 9.77706 3.97113C9.62771 3.87152 9.4522 3.81836 9.27268 3.81836C9.03101 3.82018 8.79986 3.91743 8.62958 4.08892C8.4593 4.26041 8.36369 4.49224 8.36359 4.73391Z" fill="black"/>
<path d="M8.36359 4.72744C8.36359 4.60806 8.3871 4.48984 8.43279 4.37955C8.47847 4.26925 8.54543 4.16904 8.62985 4.08462C8.71427 4.00021 8.81448 3.93324 8.92478 3.88756C9.03507 3.84187 9.15328 3.81836 9.27267 3.81836C9.39205 3.81836 9.51026 3.84187 9.62056 3.88756C9.73085 3.93324 9.83107 4.00021 9.91548 4.08462C9.9999 4.16904 10.0669 4.26925 10.1125 4.37955C10.1582 4.48984 10.1817 4.60806 10.1817 4.72744C10.1817 4.96854 10.086 5.19977 9.91548 5.37025C9.745 5.54074 9.51377 5.63652 9.27267 5.63652C9.03156 5.63652 8.80034 5.54074 8.62985 5.37025C8.45936 5.19977 8.36359 4.96854 8.36359 4.72744Z" fill="white"/>
<path d="M21.9997 12.382C21.9915 12.4089 21.9849 12.4366 21.9812 12.4647C21.966 12.7488 21.9627 13.0362 21.9318 13.3206C21.8836 14.2417 21.5734 15.1305 21.0369 15.8843C20.7953 16.209 20.4801 16.4731 20.1167 16.6552C19.7533 16.8372 19.3518 16.9323 18.9446 16.9326H12.2849C12.1553 16.9326 12.1553 16.9326 12.1553 17.0609V17.5195C12.1553 17.614 12.1832 17.6356 12.2725 17.6356H16.8061C16.8831 17.6356 16.9111 17.6568 16.9111 17.7363V19.9763C16.896 20.3007 16.7596 20.608 16.5284 20.8383C16.2212 21.1621 15.8386 21.4066 15.4141 21.5504C14.8969 21.7347 14.3593 21.8567 13.8127 21.9139C13.0638 21.9987 12.3092 22.0211 11.5566 21.9812C10.7456 21.9663 9.94266 21.8187 9.18032 21.5442C8.65735 21.3695 8.19509 21.0516 7.84715 20.6272C7.5674 20.2706 7.42909 19.825 7.45831 19.3743C7.47682 18.9953 7.45831 18.6134 7.45831 18.2315V16.1162C7.45831 15.7009 7.45831 15.2823 7.47682 14.8698C7.47974 14.4226 7.61965 13.9867 7.87805 13.6199C8.13645 13.2531 8.50117 12.9727 8.92397 12.8157C9.3342 12.6493 9.77399 12.5663 10.2172 12.5715H14.9298C15.4285 12.581 15.9199 12.453 16.3494 12.2019C16.8993 11.8591 17.2906 11.3156 17.4386 10.6889C17.528 10.3737 17.5728 10.0476 17.5715 9.72014V7.60239C17.5715 7.46178 17.5715 7.46504 17.7134 7.46178C18.2257 7.46178 18.738 7.4467 19.2565 7.46178C19.7639 7.48229 20.2479 7.67927 20.6234 8.01812C21.0599 8.41143 21.3854 8.91069 21.5677 9.46663C21.7518 9.97398 21.868 10.503 21.9133 11.0403C21.9442 11.3826 21.9565 11.725 21.9783 12.0641C21.9825 12.1012 21.9886 12.1379 21.9969 12.1741C21.9989 12.2414 21.9997 12.3107 21.9997 12.382Z" fill="#FFDA4B"/>
<path d="M15.6362 19.2659C15.6349 19.0864 15.5805 18.9113 15.4799 18.7626C15.3792 18.614 15.2368 18.4985 15.0706 18.4306C14.9043 18.3628 14.7218 18.3457 14.5458 18.3815C14.3699 18.4173 14.2085 18.5044 14.082 18.6318C13.9556 18.7592 13.8696 18.9212 13.8351 19.0974C13.8005 19.2735 13.8189 19.456 13.8879 19.6217C13.9569 19.7875 14.0734 19.9291 14.2227 20.0287C14.3721 20.1283 14.5476 20.1814 14.7271 20.1814C14.9688 20.1796 15.1999 20.0824 15.3702 19.9109C15.5405 19.7394 15.6361 19.5076 15.6362 19.2659Z" fill="black"/>
<path d="M15.6362 19.2724C15.6362 19.5135 15.5404 19.7447 15.37 19.9152C15.1995 20.0857 14.9682 20.1814 14.7271 20.1814C14.486 20.1814 14.2548 20.0857 14.0843 19.9152C13.9138 19.7447 13.8181 19.5135 13.8181 19.2724C13.8181 19.0313 13.9138 18.8 14.0843 18.6295C14.2548 18.4591 14.486 18.3633 14.7271 18.3633C14.9682 18.3633 15.1995 18.4591 15.37 18.6295C15.5404 18.8 15.6362 19.0313 15.6362 19.2724Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

51
packages/nc-gui/assets/nc-icons/lang-ruby.svg

@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<g clip-path="url(#clip0_1374_6473)">
<path d="M0 0H24V24H0" fill="none"/>
<path d="M7.21875 20.1562L19.2656 19.3594V11.625" fill="#881100"/>
<path d="M19.2656 19.3594L11.5781 18.75L15.375 15.8906L19.2656 19.3594ZM19.2656 19.3594L18.2344 12.2344L20.1562 7.54688" fill="#BB1100"/>
<path d="M3.79688 16.875C3.85938 19.125 5 20.2188 7.21875 20.1562C13.2656 19.7812 19.6875 12.3281 20.1562 7.54688C20.3438 5.42188 19.4844 4.20312 17.5781 3.89062L15.0469 5.29688C15.0469 5.29688 14.5312 14.3906 4.82812 14.8594" fill="#990000"/>
<path d="M3.79688 16.875L4.03125 12.3281L4.92188 14.8594" fill="url(#paint0_linear_1374_6473)"/>
<path d="M4.92188 14.8594L7.21875 20.1562L9.09375 13.9219" fill="url(#paint1_linear_1374_6473)"/>
<path d="M9.09375 13.9219L13.7812 9.51562L15.375 15.9375" fill="url(#paint2_linear_1374_6473)"/>
<path d="M13.7812 9.51562L15.0469 5.29688L19.7812 9.14062" fill="url(#paint3_linear_1374_6473)"/>
<path d="M7.26561 7.3596C6.06107 8.59388 5.13462 9.98027 4.68944 11.2147C4.24426 12.4492 4.31669 13.4309 4.89084 13.9447C5.46499 14.4584 6.494 14.4622 7.75219 13.9552C9.01039 13.4482 10.3951 12.4719 11.6026 11.2402C12.81 10.0086 13.7418 8.62222 14.1934 7.38516C14.6451 6.1481 14.5798 5.16134 14.0118 4.64127C13.4439 4.12119 12.4197 4.11027 11.1638 4.6109C9.90791 5.11152 8.52289 6.08282 7.31249 7.31179" fill="url(#paint4_linear_1374_6473)"/>
<path d="M17.5781 3.89062H13.0781L15.0469 5.29688L17.5781 3.89062ZM17.5781 3.89062L31.6406 -24.2344L17.5781 3.89062Z" fill="url(#paint5_linear_1374_6473)"/>
</g>
<defs>
<linearGradient id="paint0_linear_1374_6473" x1="3.79688" y1="12.3281" x2="6.10872" y2="15.1881" gradientUnits="userSpaceOnUse">
<stop offset="0.2" stop-color="white"/>
<stop offset="0.4" stop-color="#DD5533"/>
<stop offset="0.6" stop-color="#BB1100"/>
</linearGradient>
<linearGradient id="paint1_linear_1374_6473" x1="4.92188" y1="13.9219" x2="6.70081" y2="19.8739" gradientUnits="userSpaceOnUse">
<stop offset="0.2" stop-color="white"/>
<stop offset="0.4" stop-color="#DD5533"/>
<stop offset="0.6" stop-color="#BB1100"/>
</linearGradient>
<linearGradient id="paint2_linear_1374_6473" x1="9.09375" y1="9.51562" x2="10.4046" y2="15.9263" gradientUnits="userSpaceOnUse">
<stop offset="0.2" stop-color="white"/>
<stop offset="0.4" stop-color="#DD5533"/>
<stop offset="0.6" stop-color="#BB1100"/>
</linearGradient>
<linearGradient id="paint3_linear_1374_6473" x1="13.7813" y1="5.29688" x2="14.3863" y2="9.59929" gradientUnits="userSpaceOnUse">
<stop offset="0.2" stop-color="white"/>
<stop offset="0.4" stop-color="#DD5533"/>
<stop offset="0.6" stop-color="#BB1100"/>
</linearGradient>
<linearGradient id="paint4_linear_1374_6473" x1="4.401" y1="14.3327" x2="14.4906" y2="4.24316" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.1" stop-color="#DD3322"/>
<stop offset="0.2" stop-color="#880000"/>
<stop offset="1" stop-color="#DD2200"/>
</linearGradient>
<linearGradient id="paint5_linear_1374_6473" x1="13.0781" y1="5.29688" x2="39.6891" y2="-11.43" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.1" stop-color="#DD3322"/>
<stop offset="0.2" stop-color="#880000"/>
<stop offset="1" stop-color="#DD2200"/>
</linearGradient>
<clipPath id="clip0_1374_6473">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

38
packages/nc-gui/assets/nc-icons/lang-shell.svg

@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="24px"
height="24px">
<defs>
<linearGradient x1="35.753" y1="3.643" x2="11.271" y2="46.048" gradientUnits="userSpaceOnUse" id="color-1">
<stop offset="0" stop-color="white"></stop>
<stop offset="0.26" stop-color="white"></stop>
<stop offset="0.678" stop-color="white"></stop>
<stop offset="1" stop-color="white"></stop>
</linearGradient>
<linearGradient x1="32.281" y1="26.55" x2="23.433" y2="41.876" gradientUnits="userSpaceOnUse" id="color-2">
<stop offset="0" stop-color="white"></stop>
<stop offset="0.26" stop-color="white"></stop>
<stop offset="0.678" stop-color="white"></stop>
<stop offset="1" stop-color="white"></stop>
</linearGradient>
</defs>
<g fill="none" fill-rule="none" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter"
stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none"
font-size="none" text-anchor="none" style="mix-blend-mode: normal">
<g transform="scale(5.33333,5.33333)">
<path
d="M22.903,3.286c0.679,-0.381 1.515,-0.381 2.193,0c3.355,1.883 13.451,7.551 16.807,9.434c0.679,0.38 1.097,1.084 1.097,1.846c0,3.766 0,15.101 0,18.867c0,0.762 -0.418,1.466 -1.097,1.847c-3.355,1.883 -13.451,7.551 -16.807,9.434c-0.679,0.381 -1.515,0.381 -2.193,0c-3.355,-1.883 -13.451,-7.551 -16.807,-9.434c-0.678,-0.381 -1.096,-1.084 -1.096,-1.846c0,-3.766 0,-15.101 0,-18.867c0,-0.762 0.418,-1.466 1.097,-1.847c3.354,-1.883 13.452,-7.551 16.806,-9.434z"
fill="url(#color-1)" fill-rule="evenodd"></path>
<path
d="M23.987,46.221c-1.085,0 -2.171,-0.252 -3.165,-0.757c-2.22,-1.127 -5.118,-2.899 -7.921,-4.613c-1.973,-1.206 -3.836,-2.346 -5.297,-3.157c-2.223,-1.236 -3.604,-3.581 -3.604,-6.122v-14.945c0,-2.59 1.417,-4.955 3.699,-6.173c3.733,-1.989 9.717,-5.234 12.878,-7.01v0c2.11,-1.184 4.733,-1.184 6.844,0c3.576,2.007 10.369,6.064 14.252,8.513c1.457,0.917 2.327,2.496 2.327,4.225v15.818c0,2.4 -0.859,4.048 -2.553,4.895c-0.944,0.531 -2.628,1.576 -4.578,2.787c-3.032,1.882 -6.806,4.225 -9.564,5.705c-1.035,0.555 -2.177,0.834 -3.318,0.834zM21.556,5.188c-3.172,1.782 -9.174,5.038 -12.916,7.032c-1.628,0.868 -2.64,2.556 -2.64,4.407v14.945c0,1.814 0.987,3.49 2.576,4.373c1.498,0.832 3.378,1.981 5.369,3.199c2.77,1.693 5.634,3.445 7.783,4.536c1.458,0.739 3.188,0.717 4.631,-0.056c2.703,-1.451 6.447,-3.775 9.456,-5.643c1.97,-1.223 3.671,-2.279 4.696,-2.854c1.324,-0.663 1.489,-2.018 1.489,-3.127v-15.818c0,-1.037 -0.521,-1.983 -1.392,-2.532c-3.862,-2.435 -10.613,-6.467 -14.165,-8.461c-1.53,-0.858 -3.357,-0.858 -4.887,-0.001z"
fill="#2f3a3e" fill-rule="nonzero"></path>
<path
d="M22.977,41.654l-0.057,-13.438c-0.011,-2.594 1.413,-4.981 3.701,-6.204l12.01,-6.416c1.998,-1.068 4.414,0.38 4.414,2.646v14.73c0,1.041 -0.54,2.008 -1.426,2.554l-14.068,8.668c-1.994,1.23 -4.564,-0.198 -4.574,-2.54z"
fill="#2f3a3e" fill-rule="nonzero"></path>
<path
d="M28.799,26.274c0.123,-0.063 0.225,0.014 0.227,0.176l0.013,1.32c0.552,-0.219 1.032,-0.278 1.467,-0.177c0.095,0.024 0.136,0.153 0.098,0.306l-0.291,1.169c-0.024,0.089 -0.072,0.178 -0.132,0.233c-0.026,0.025 -0.052,0.044 -0.077,0.057c-0.04,0.02 -0.078,0.026 -0.114,0.019c-0.199,-0.045 -0.671,-0.148 -1.413,0.228c-0.778,0.395 -1.051,1.071 -1.046,1.573c0.007,0.601 0.315,0.783 1.377,0.802c1.416,0.023 2.027,0.643 2.042,2.067c0.016,1.402 -0.733,2.905 -1.876,3.826l0.025,1.308c0.001,0.157 -0.1,0.338 -0.225,0.4l-0.775,0.445c-0.123,0.063 -0.225,-0.014 -0.227,-0.172l-0.013,-1.286c-0.664,0.276 -1.334,0.342 -1.763,0.17c-0.082,-0.032 -0.117,-0.152 -0.084,-0.288l0.28,-1.181c0.022,-0.092 0.071,-0.186 0.138,-0.246c0.023,-0.023 0.048,-0.04 0.072,-0.053c0.044,-0.022 0.087,-0.027 0.124,-0.013c0.462,0.155 1.053,0.082 1.622,-0.206c0.722,-0.365 1.206,-1.102 1.198,-1.834c-0.007,-0.664 -0.366,-0.939 -1.241,-0.946c-1.113,0.002 -2.151,-0.216 -2.168,-1.855c-0.014,-1.35 0.688,-2.753 1.799,-3.641l-0.013,-1.319c-0.001,-0.162 0.098,-0.34 0.225,-0.405z"
fill="url(#color-2)" fill-rule="nonzero"></path>
<path
d="M37.226,34.857l-3.704,2.185c-0.109,0.061 -0.244,-0.019 -0.244,-0.143v-1.252c0,-0.113 0.061,-0.217 0.16,-0.273l3.704,-2.185c0.111,-0.061 0.246,0.019 0.246,0.145v1.248c0,0.115 -0.062,0.219 -0.162,0.275"
fill="#3ab14a" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

10
packages/nc-gui/assets/nc-icons/puzzle-outline.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

4
packages/nc-gui/assets/nc-icons/puzzle-solid.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

2
packages/nc-gui/components/account/AppStore.vue

@ -10,7 +10,7 @@
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="nc-content-max-w p-6 h-[calc(100vh_-_100px)] flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div>
<LazyDashboardSettingsAppStore />
</div>

36
packages/nc-gui/components/account/Breadcrumb.vue

@ -4,6 +4,7 @@ const route = useRoute()
interface BreadcrumbType {
title: string
active?: boolean
path?: string
}
const { t } = useI18n()
@ -77,7 +78,28 @@ const breadcrumb = computed<BreadcrumbType[]>(() => {
}
}
if ((route.params.page === undefined && route.params.nestedPage === '') || route.params.nestedPage === 'list') {
if (route.path.startsWith('/account/setup')) {
payload.push({
title: t('labels.setup'),
active: !route.params.nestedPage,
path: '/account/setup',
})
if (route.params.nestedPage) {
payload.push({
title: route.params.nestedPage,
active: !route.params.app,
path: `/account/setup/${route.params.nestedPage}`,
})
}
if (route.params.app) {
payload.push({
title: route.params.app,
active: true,
})
}
} else if ((route.params.page === undefined && route.params.nestedPage === '') || route.params.nestedPage === 'list') {
payload.push(
...[
{
@ -93,16 +115,24 @@ const breadcrumb = computed<BreadcrumbType[]>(() => {
return payload
})
const onClick = async (item: BreadcrumbType) => {
if (item.path && !item.active) {
await navigateTo(item.path)
}
}
</script>
<template>
<div class="nc-breadcrumb">
<template v-for="(item, i) of breadcrumb" :key="i">
<div
class="nc-breadcrumb-item"
class="nc-breadcrumb-item capitalize"
:class="{
active: item.active,
'active': item.active,
'cursor-pointer': item.path && !item.active,
}"
@click="onClick(item)"
>
{{ item.title }}
</div>

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

@ -72,7 +72,7 @@ const onValidate = async (_: any, valid: boolean) => {
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="nc-content-max-w p-6 h-[calc(100vh_-_100px)] flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="flex flex-col w-150 mx-auto">
<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" data-rec="true">{{ $t('labels.accountDetails') }}</div>

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

@ -68,7 +68,7 @@ const resetError = () => {
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="nc-content-max-w p-6 h-[calc(100vh_-_100px)] flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="mx-auto relative flex flex-col justify-start gap-2 w-full md:(bg-white) max-w-[900px]">
<a-form
ref="formValidator"

129
packages/nc-gui/components/account/Setup.vue

@ -0,0 +1,129 @@
<script setup lang="ts">
const { t } = useI18n()
const { loadSetupApps, emailConfigured, storageConfigured, listModalDlg } = useAccountSetupStoreOrThrow()
// const { appInfo } = useGlobal()
const openedCategory = ref<string | null>(null)
const configs = computed(() => [
{
title: t('labels.configLabel', { label: t('labels.email') }),
key: 'email',
description:
'Configure your preferred email service to manage how your application sends alerts, notifications and other essential emails.',
docsLink: 'https://docs.nocodb.com/account-settings/oss-specific-details#configure-email',
buttonClick: () => {
navigateTo(`/account/setup/email${emailConfigured.value ? `/${emailConfigured.value.title}` : ''}`)
},
itemClick: () => {
navigateTo(`/account/setup/email`)
},
configured: emailConfigured.value,
},
{
title: t('labels.configLabel', { label: t('labels.storage') }),
key: 'storage',
description: 'Set up and manage your preferred storage solution for securely handling and storing your application’s data.',
docsLink: 'https://docs.nocodb.com/account-settings/oss-specific-details#configure-storage',
buttonClick: () => {
navigateTo(`/account/setup/storage${storageConfigured.value ? `/${storageConfigured.value.title}` : ''}`)
},
itemClick: () => {
navigateTo(`/account/setup/storage`)
},
configured: storageConfigured.value,
},
// {
// title: t('labels.switchToProd'),
// key: 'switchToProd',
// description: 'Switch to production-ready app database from existing application database.',
// docsLink: 'https://docs.nocodb.com',
// buttonClick: () => {
// // TODO: Implement the logic to switch to production
// },
// isPending: !(appInfo.value as any)?.prodReady,
// },
])
onMounted(async () => {
await loadSetupApps()
})
</script>
<template>
<div class="flex flex-col" data-test-id="nc-setup-main">
<NcPageHeader>
<template #icon>
<div class="flex justify-center items-center h-5 w-5">
<GeneralIcon icon="ncSliders" class="flex-none text-[20px]" />
</div>
</template>
<template #title>
<span data-rec="true">
{{ $t('labels.setup') }}
</span>
</template>
</NcPageHeader>
<div
class="nc-content-max-w flex-1 max-h-[calc(100vh_-_100px)] overflow-y-auto nc-scrollbar-thin flex flex-col items-center gap-6 p-6"
>
<div class="flex flex-col gap-6 w-150">
<div
v-for="config of configs"
class="flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-2 hover:(shadow bg-gray-10)"
:class="{
'cursor-pointer': config.itemClick,
}"
:data-testid="`nc-setup-${config.key}`"
@click="config.itemClick"
>
<div class="flex gap-3 items-center" data-rec="true">
<NcTooltip v-if="!config.configured || config.isPending">
<template #title>
<span>
{{ $t('activity.pending') }}
</span>
</template>
<GeneralIcon icon="ncAlertCircle" class="text-orange-500 -mt-1 w-6 h-6 nc-pending" />
</NcTooltip>
<GeneralIcon v-else icon="circleCheckSolid" class="text-success w-6 h-6 bg-white-500 nc-configured" />
<span class="font-bold text-base"> {{ config.title }}</span>
</div>
<div class="text-gray-600 text-sm">{{ config.description }}</div>
<div class="flex justify-between mt-4">
<NcButton
size="small"
type="text"
:href="config.docsLink"
target="_blank"
class="!flex items-center !no-underline"
rel="noopener noreferer"
@click.stop
>
<div class="flex gap-2 items-center">
Go to docs
<GeneralIcon icon="ncExternalLink" />
</div>
</NcButton>
<NcButton v-if="config.configured" size="small" type="text" @click.stop="config.buttonClick">
<div class="flex gap-2 items-center">
<GeneralIcon icon="ncEdit3" />
{{ $t('general.edit') }}
</div>
</NcButton>
<NcButton v-else size="small" @click.stop="config.buttonClick">{{ $t('general.configure') }}</NcButton>
</div>
</div>
</div>
</div>
<LazyAccountSetupListModal v-if="openedCategory" v-model="listModalDlg" :category="openedCategory" />
</div>
</template>
<style scoped lang="scss"></style>

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

@ -40,7 +40,7 @@ loadSettings()
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="nc-content-max-w p-6 h-[calc(100vh_-_100px)] flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="flex flex-col items-center">
<div class="flex items-center gap-2">
<a-form-item>

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

@ -235,7 +235,7 @@ const handleCancel = () => {
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="nc-content-max-w p-6 h-[calc(100vh_-_100px)] flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="max-w-202 mx-auto h-full w-full" data-testid="nc-token-list">
<div class="flex gap-4 items-baseline justify-between">
<h6 class="text-xl text-left font-bold my-0" data-rec="true">{{ $t('title.apiTokens') }}</h6>

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

@ -222,7 +222,7 @@ const columns = [
</span>
</template>
</NcPageHeader>
<div class="p-6 h-[calc(100vh_-_100px)] border-t-1 border-gray-200 flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="nc-content-max-w p-6 h-[calc(100vh_-_100px)] flex flex-col gap-6 overflow-auto nc-scrollbar-thin">
<div class="h-full">
<div class="max-w-195 mx-auto h-full">
<div class="flex gap-4 items-center justify-between">

13
packages/nc-gui/components/account/setup/AppIcon.vue

@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
app: {
title: string
logo: string
}
}>()
</script>
<template>
<img v-if="app.title !== 'SMTP'" class="object-contain" :alt="app.title" :src="app.logo" />
<GeneralIcon v-else class="text-gray-500" icon="mail" />
</template>

175
packages/nc-gui/components/account/setup/Config.vue

@ -0,0 +1,175 @@
<script setup lang="ts">
import dayjs from 'dayjs'
const props = defineProps<{
id: string
modelValue?: boolean
}>()
const emit = defineEmits(['saved', 'close', 'update:modelValue'])
const vOpen = useVModel(props, 'modelValue', emit)
const {
readPluginDetails,
activePluginFormData: pluginFormData,
activePlugin: plugin,
isLoading,
loadingAction,
testSettings,
saveSettings,
} = useAccountSetupStoreOrThrow()
await readPluginDetails(props.id)
const pluginTypeMap = {
Input: FormBuilderInputType.Input,
Select: FormBuilderInputType.Select,
Checkbox: FormBuilderInputType.Switch,
LongText: FormBuilderInputType.Input,
Password: FormBuilderInputType.Password,
}
const { formState, validate, validateInfos } = useProvideFormBuilderHelper({
formSchema: [
...plugin.value.formDetails.items.flatMap((item, i) => [
{
type: pluginTypeMap[item.type] || FormBuilderInputType.Input,
label: item.label,
placeholder: item.placeholder,
model: item.key,
required: item.required,
helpText: item.help_text,
width: '48',
border: false,
showHintAsTooltip: true,
},
...(i % 2
? []
: [
{
type: FormBuilderInputType.Space,
width: '4',
},
]),
]),
],
initialState: pluginFormData,
})
const doAction = async (action: Action) => {
try {
switch (action) {
case Action.Save:
await validate()
pluginFormData.value = formState.value
await saveSettings()
vOpen.value = false
break
case Action.Test:
await validate()
pluginFormData.value = formState.value
await testSettings()
break
}
} catch (e: any) {
console.log(e)
} finally {
loadingAction.value = null
}
}
const isValid = computed(() => {
return Object.values(validateInfos || {}).every((info) => info.validateStatus !== 'error')
})
const docLinks = computed(() => {
return [
{
title: 'Application Setup',
url: `https://docs.nocodb.com/account-settings/oss-specific-details#configure-${plugin.value?.category?.toLowerCase()}`,
},
...(plugin.value?.formDetails?.docs || []),
]
})
</script>
<template>
<div class="flex flex-col h-full h-[calc(100vh_-_40px)]" data-testid="nc-setup-config">
<NcPageHeader>
<template #title>
<div class="flex gap-3 items-center">
<AccountSetupAppIcon :app="plugin" class="h-8 w-8" />
<span data-rec="true">
{{ plugin.title }}
</span>
</div>
</template>
</NcPageHeader>
<div class="h-full flex h-[calc(100%_-_48px)]">
<div class="nc-config-left-panel nc-scrollbar-thin relative h-full flex flex-col">
<div class="w-full flex items-center gap-3 border-gray-200 py-6 px-6">
<span class="font-semibold text-base">{{ $t('labels.configuration') }}</span>
<div class="flex-grow" />
<div class="flex gap-2">
<NcButton
v-for="(action, i) in plugin.formDetails.actions"
:key="i"
:loading="loadingAction === action.key"
:type="action.key === Action.Save ? 'primary' : 'default'"
size="small"
:disabled="!!loadingAction || !isValid"
:data-testid="`nc-setup-config-action-${action.key?.toLowerCase()}`"
@click="doAction(action.key)"
>
{{ action.label }}
</NcButton>
</div>
</div>
<div class="h-[calc(100%_-_48px)] flex py-4 flex-col p-6 overflow-auto">
<div v-if="isLoading || !plugin" class="flex flex-row w-full justify-center items-center h-52">
<a-spin size="large" />
</div>
<div v-else class="flex">
<NcFormBuilder class="w-229 px-2 mx-auto" />
</div>
</div>
</div>
<div class="nc-config-right-panel">
<div class="flex-grow flex flex-col gap-3">
<div class="text-gray-500 text-capitalize">{{ $t('labels.documentation') }}</div>
<a
v-for="doc of docLinks"
:key="doc.title"
:href="doc.url"
target="_blank"
rel="noopener noreferrer"
class="!no-underline !text-current flex gap-2 items-center"
>
<GeneralIcon icon="bookOpen" class="text-gray-500" />
{{ doc.title }}
</a>
<NcDivider />
<div class="text-gray-500 text-capitalize">{{ $t('labels.modifiedOn') }}</div>
<div class="">
{{ dayjs(plugin.created_at).format('DD MMM YYYY HH:mm') }}
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.nc-config-left-panel {
@apply w-full flex-1 flex justify-stretch;
}
.nc-config-right-panel {
@apply p-5 w-[320px] border-l-1 border-gray-200 flex flex-col gap-4 bg-gray-50 rounded-br-2xl;
}
</style>

149
packages/nc-gui/components/account/setup/List.vue

@ -0,0 +1,149 @@
<script setup lang="ts">
const props = defineProps<{
category: string
}>()
const { categorizeApps, resetPlugin: _resetPlugin, showPluginUninstallModal, activePlugin } = useAccountSetupStoreOrThrow()
const apps = computed(() => categorizeApps.value?.[props.category?.toLowerCase()] || [])
const configuredApp = computed(() => apps.value.find((app: any) => app.active))
const showResetActiveAppMsg = ref(false)
const switchingTo = ref(null)
const showResetPluginModal = async (app: any, resetActiveAppMsg = false) => {
showResetActiveAppMsg.value = resetActiveAppMsg
showPluginUninstallModal.value = true
activePlugin.value = app
}
const selectApp = (app: any) => {
const activeApp = app !== configuredApp.value && configuredApp.value
if (activeApp) {
switchingTo.value = app
return showResetPluginModal(activeApp, true)
}
navigateTo(`/account/setup/${props.category}/${app.title}`)
}
const resetPlugin = async () => {
await _resetPlugin(activePlugin.value)
if (showResetActiveAppMsg.value) {
await selectApp(switchingTo.value)
switchingTo.value = null
showResetActiveAppMsg.value = false
}
}
const closeResetModal = () => {
activePlugin.value = null
switchingTo.value = null
showResetActiveAppMsg.value = false
showPluginUninstallModal.value = false
}
</script>
<template>
<div class="flex flex-col" data-testid="nc-setup-list">
<NcPageHeader>
<template #title>
<span data-rec="true">
{{ category }}
</span>
</template>
</NcPageHeader>
<div class="h-[calc(100%_-_58px)] flex">
<div class="w-full">
<div class="w-950px px-4 mt-3 mx-auto text-lg font-weight-bold">{{ category }} Services</div>
<div class="container">
<div
v-for="app in apps"
:key="app.title"
class="item group"
:data-testid="`nc-setup-list-item-${app.title}`"
@click="selectApp(app)"
>
<AccountSetupAppIcon :app="app" class="icon" />
<span class="title">{{ app.title }}</span>
<div class="flex-grow" />
<GeneralIcon
v-if="app.active"
icon="delete"
class="text-error min-w-6 h-6 bg-white-500 !hidden !group-hover:!inline cursor-pointer"
/>
<GeneralIcon
v-if="app === configuredApp"
icon="circleCheckSolid"
class="text-success min-w-5 h-5 bg-white-500 nc-configured"
/>
<NcDropdown :trigger="['click']" overlay-class-name="!rounded-md" @click.stop>
<GeneralIcon
v-if="app.active"
icon="threeDotVertical"
class="min-w-5 h-5 bg-white-500 text-gray-500 hover:text-current nc-setup-plugin-menu"
/>
<template #overlay>
<NcMenu class="min-w-20">
<NcMenuItem data-testid="nc-config-reset" @click.stop="showResetPluginModal(app)">
<span> {{ $t('general.reset') }} </span>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</div>
</div>
</div>
<a-modal
v-model:visible="showPluginUninstallModal"
:closable="false"
width="448px"
centered
:footer="null"
wrap-class-name="nc-modal-plugin-reset-conform"
>
<div class="flex flex-col h-full">
<div v-if="showResetActiveAppMsg" class="text-base font-weight-bold">
Switch to {{ switchingTo && switchingTo.title }}
</div>
<div v-else class="text-base font-weight-bold">Reset {{ activePlugin && activePlugin.title }} Configuration</div>
<div class="flex flex-row mt-2 w-full">
<template v-if="showResetActiveAppMsg">
Switching to {{ switchingTo && switchingTo.title }} will reset your {{ activePlugin && activePlugin.title }}
settings. Continue?
</template>
<template v-else>Resetting will erase your current configuration.</template>
</div>
<div class="flex mt-6 justify-end space-x-2">
<NcButton size="small" type="secondary" @click="closeResetModal"> {{ $t('general.cancel') }}</NcButton>
<NcButton size="small" type="danger" data-testid="nc-reset-confirm-btn" @click="resetPlugin">
{{ showResetActiveAppMsg ? `${$t('general.reset')} & ${$t('general.switch')}` : $t('general.reset') }}
</NcButton>
</div>
</div>
</a-modal>
</div>
</template>
<style scoped lang="scss">
.container {
@apply p-4 w-950px gap-5 mx-auto my-2 grid grid-cols-3;
.item {
@apply text-base w-296px max-w-296px flex gap-3 border-1 border-gray-200 py-4 px-5 rounded-xl items-center cursor-pointer hover:(shadow bg-gray-50);
.icon {
@apply !w-8 !h-8 object-contain;
}
.title {
@apply font-weight-bold;
}
}
}
</style>

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

@ -5,11 +5,12 @@ import { isDateMonthFormat, isSystemColumn } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
isPk?: boolean
showCurrentDateOption?: boolean | 'disabled'
}
const { modelValue, isPk } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'currentDate'])
const { t } = useI18n()
@ -287,6 +288,11 @@ function handleSelectDate(value?: dayjs.Dayjs) {
localState.value = value
open.value = false
}
const currentDate = ($event) => {
emit('currentDate', $event)
open.value = false
}
</script>
<template>
@ -336,6 +342,8 @@ function handleSelectDate(value?: dayjs.Dayjs) {
:is-open="isOpen"
type="month"
size="medium"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<NcDatePicker
v-else
@ -344,7 +352,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
:selected-date="localState"
type="date"
size="medium"
:show-current-date-option="showCurrentDateOption"
@update:selected-date="handleSelectDate"
@current-date="currentDate"
/>
</div>
</template>

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

@ -5,12 +5,13 @@ import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
isPk?: boolean
showCurrentDateOption?: boolean | 'disabled'
isUpdatedFromCopyNPaste?: Record<string, boolean>
}
const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'currentDate'])
const timeFormatsObj = {
[timeFormats[0]]: 'hh:mm A',
@ -425,6 +426,11 @@ const cellValue = computed(
localState.value?.format(parseProp(column.value.meta).is12hrFormat ? timeFormatsObj[timeFormat.value] : timeFormat.value) ??
'',
)
const currentDate = ($event) => {
open.value = false
emit('currentDate', $event)
}
</script>
<template>
@ -510,7 +516,9 @@ const cellValue = computed(
:is-open="isOpen"
type="date"
size="medium"
:show-current-date-option="showCurrentDateOption"
@update:selected-date="handleSelectDate"
@current-date="currentDate"
/>
<template v-else>
@ -520,7 +528,9 @@ const cellValue = computed(
is-min-granularity-picker
:is12hr-format="!!parseProp(column.meta).is12hrFormat"
:is-open="isOpen"
:show-current-date-option="showCurrentDateOption"
@update:selected-date="handleSelectTime"
@current-date="currentDate"
/>
</template>
</div>

26
packages/nc-gui/components/cell/User.vue

@ -32,8 +32,12 @@ const activeCell = inject(ActiveCellInj, ref(false))
const basesStore = useBases()
const baseStore = useBase()
const { basesUser } = storeToRefs(basesStore)
const { idUserMap } = storeToRefs(baseStore)
const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : []))
// use both ActiveCellInj or EditModeInj to determine the active state
@ -303,6 +307,11 @@ const filterOption = (input: string, option: any) => {
return searchVal.toLowerCase().includes(input.toLowerCase())
}
}
// check if user is part of the base
const isCollaborator = (userIdOrEmail) => {
return !idUserMap.value?.[userIdOrEmail]?.deleted
}
</script>
<template>
@ -347,6 +356,7 @@ const filterOption = (input: string, option: any) => {
:name="op.display_name?.trim() ? op.display_name?.trim() : ''"
:email="op.email"
class="!text-[0.65rem]"
:disabled="!isCollaborator(op.id)"
/>
</div>
<NcTooltip class="truncate max-w-full" show-on-truncate-only>
@ -360,6 +370,9 @@ const filterOption = (input: string, option: any) => {
whiteSpace: 'nowrap',
display: 'inline',
}"
:class="{
'text-gray-600': !isCollaborator(op.id || op.email),
}"
>
{{ op.display_name?.trim() || op.email }}
</span>
@ -408,6 +421,7 @@ const filterOption = (input: string, option: any) => {
>
<div class="flex-none">
<GeneralUserIcon
:disabled="!isCollaborator(selectedOpt.value)"
size="auto"
:name="!selectedOpt.label?.includes('@') ? selectedOpt.label.trim() : ''"
:email="selectedOpt.label"
@ -425,6 +439,9 @@ const filterOption = (input: string, option: any) => {
whiteSpace: 'nowrap',
display: 'inline',
}"
:class="{
'text-gray-600': !isCollaborator(selectedOpt.value),
}"
>
{{ selectedOpt.label }}
</span>
@ -540,9 +557,16 @@ const filterOption = (input: string, option: any) => {
:name="!label?.includes('@') ? label.trim() : ''"
:email="label"
class="!text-[0.65rem]"
:disabled="!isCollaborator(val)"
/>
</div>
{{ label }}
<span
:class="{
'text-gray-600': !isCollaborator(val),
}"
>
{{ label }}
</span>
</span>
</a-tag>
</template>

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

@ -54,7 +54,7 @@ const navigateToIntegrations = () => {
</template>
<template v-else-if="!isSharedBase">
<div class="xs:hidden flex flex-col p-1 mt-0.25 mb-0.5 truncate">
<DashboardSidebarTopSectionHeader />
<!-- <DashboardSidebarTopSectionHeader /> -->
<NcButton
v-if="isUIAllowed('workspaceSettings') || isUIAllowed('workspaceCollaborators')"

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

@ -15,6 +15,8 @@ const isLoggingOut = ref(false)
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const logout = async () => {
isLoggingOut.value = true
try {
@ -51,6 +53,10 @@ const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
const accountUrl = computed(() => {
return isUIAllowed('superAdminSetup') && !isEeUI ? '/account/setup' : '/account/profile'
})
</script>
<template>
@ -180,7 +186,7 @@ onMounted(() => {
<DashboardSidebarEEMenuOption v-if="isEeUI" />
<nuxt-link v-e="['c:user:settings']" class="!no-underline" to="/account/profile">
<nuxt-link v-e="['c:user:settings']" class="!no-underline" :to="accountUrl">
<NcMenuItem> <GeneralIcon icon="ncSettings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem>
</nuxt-link>
</template>

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

@ -45,7 +45,12 @@ const { copy } = useCopy()
const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table)
const { setMenuContext, openRenameTableDialog: _openRenameTableDialog, duplicateTable: _duplicateTable } = inject(TreeViewInj)!
const {
setMenuContext,
openRenameTableDialog: _openRenameTableDialog,
openTableDescriptionDialog: _openTableDescriptionDialog,
duplicateTable: _duplicateTable,
} = inject(TreeViewInj)!
const { loadViews: _loadViews, navigateToView } = useViewsStore()
const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore())
@ -204,6 +209,11 @@ const openRenameTableDialog = (table: SidebarTableNode, sourceId: string) => {
_openRenameTableDialog(table, !!sourceId)
}
const openTableDescriptionDialog = (table: SidebarTableNode) => {
isOptionsOpen.value = false
_openTableDescriptionDialog(table)
}
const deleteTable = () => {
isOptionsOpen.value = false
isTableDeleteDialogVisible.value = true
@ -343,124 +353,164 @@ const source = computed(() => {
{{ table.title }}
</span>
</NcTooltip>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']" @click.stop>
<NcButton
v-e="['c:table:option']"
class="nc-sidebar-node-btn nc-tbl-context-menu text-gray-700 hover:text-gray-800"
:class="{
'!opacity-100 !inline-block': isOptionsOpen,
}"
data-testid="nc-sidebar-table-context-menu"
type="text"
size="xxsmall"
@click.stop
>
<MdiDotsHorizontal class="!text-current" />
</NcButton>
<template #overlay>
<NcMenu class="!min-w-62.5" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click.stop="onTableIdCopy"
>
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.tableIdColon', {
tableId: table?.id,
})
}}
</div>
<NcButton class="!group-hover:bg-gray-100" size="xsmall" type="secondary">
<GeneralIcon v-if="isTableIdCopied" class="max-h-4 min-w-4" icon="check" />
<GeneralIcon v-else class="max-h-4 min-w-4" else icon="copy" />
</NcButton>
</div>
</NcTooltip>
<template
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole, source }) ||
isUIAllowed('tableDelete', { roles: baseRole, source }))
"
>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole, source })"
:data-testid="`sidebar-table-rename-${table.title}`"
class="nc-table-rename"
@click="openRenameTableDialog(table, source.id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table').toLowerCase() }}
<div class="flex items-center">
<NcTooltip v-if="table.description?.length" placement="bottom">
<template #title>
{{ table.description }}
</template>
<NcButton type="text" class="!hover:bg-transparent" size="xsmall">
<GeneralIcon icon="info" class="!w-3.5 !h-3.5 nc-info-icon group-hover:opacity-100 text-gray-600 opacity-0" />
</NcButton>
</NcTooltip>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']" @click.stop>
<NcButton
v-e="['c:table:option']"
class="nc-sidebar-node-btn nc-tbl-context-menu text-gray-700 hover:text-gray-800"
:class="{
'!opacity-100 !inline-block': isOptionsOpen,
}"
data-testid="nc-sidebar-table-context-menu"
type="text"
size="xxsmall"
@click.stop
>
<MdiDotsHorizontal class="!text-current" />
</NcButton>
<template #overlay>
<NcMenu class="!min-w-62.5" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click.stop="onTableIdCopy"
>
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.tableIdColon', {
tableId: table?.id,
})
}}
</div>
<NcButton class="!group-hover:bg-gray-100" size="xsmall" type="secondary">
<GeneralIcon v-if="isTableIdCopied" class="max-h-4 min-w-4" icon="check" />
<GeneralIcon v-else class="max-h-4 min-w-4" else icon="copy" />
</NcButton>
</div>
</NcMenuItem>
</NcTooltip>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate', {
source,
}) &&
base.sources?.[sourceIndex] &&
(source.is_meta || source.is_local)
isUIAllowed('tableDescriptionEdit', { roles: baseRole, source }) &&
!isUIAllowed('tableRename', { roles: baseRole, source })
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
:data-testid="`sidebar-table-description-${table.title}`"
class="nc-table-description"
@click="openTableDescriptionDialog(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.table').toLowerCase() }}
<div v-e="['c:table:update-description']" class="flex gap-2 items-center">
<!-- <GeneralIcon icon="ncAlignLeft" class="text-gray-700" /> -->
<GeneralIcon icon="ncAlignLeft" class="text-gray-700" />
{{ $t('labels.editDescription') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem class="!text-gray-700" @click="onDuplicate">
<GeneralIcon class="nc-view-copy-icon" icon="duplicate" />
{{
$t('general.duplicateEntity', {
entity: $t('title.defaultView').toLowerCase(),
})
}}
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole, source })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50 nc-table-delete"
@click="deleteTable"
<template
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole, source }) ||
isUIAllowed('tableDelete', { roles: baseRole, source }))
"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table').toLowerCase() }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand text-gray-700 hover:text-gray-800"
:class="{
'!opacity-100 !visible': isOptionsOpen,
}"
@click.stop="onExpand"
>
<GeneralIcon
icon="chevronRight"
class="nc-sidebar-source-node-btns cursor-pointer transform transition-transform duration-200 !text-current text-[20px]"
:class="{ '!rotate-90': isExpanded }"
/>
</NcButton>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole, source })"
:data-testid="`sidebar-table-rename-${table.title}`"
class="nc-table-rename"
@click="openRenameTableDialog(table, source.id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table').toLowerCase() }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDescriptionEdit', { roles: baseRole, source })"
:data-testid="`sidebar-table-description-${table.title}`"
class="nc-table-description"
@click="openTableDescriptionDialog(table)"
>
<div v-e="['c:table:update-description']" class="flex gap-2 items-center">
<!-- <GeneralIcon icon="ncAlignLeft" class="text-gray-700" /> -->
<GeneralIcon icon="ncAlignLeft" class="text-gray-700" />
{{ $t('labels.editDescription') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate', {
source,
}) &&
base.sources?.[sourceIndex] &&
(source.is_meta || source.is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.table').toLowerCase() }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem class="!text-gray-700" @click="onDuplicate">
<GeneralIcon class="nc-view-copy-icon" icon="duplicate" />
{{
$t('general.duplicateEntity', {
entity: $t('title.defaultView').toLowerCase(),
})
}}
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole, source })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50 nc-table-delete"
@click="deleteTable"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table').toLowerCase() }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand text-gray-700 hover:text-gray-800"
:class="{
'!opacity-100 !visible': isOptionsOpen,
}"
@click.stop="onExpand"
>
<GeneralIcon
icon="chevronRight"
class="nc-sidebar-source-node-btns cursor-pointer transform transition-transform duration-200 !text-current text-[20px]"
:class="{ '!rotate-90': isExpanded }"
/>
</NcButton>
</div>
</div>
</div>
<DlgTableDelete
@ -480,6 +530,8 @@ const source = computed(() => {
}
.nc-tree-item svg {
@apply text-primary text-opacity-60;
&:not(.nc-info-icon) {
@apply text-primary text-opacity-60;
}
}
</style>

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

@ -63,6 +63,8 @@ const isDefaultBase = computed(() => {
return _isDefaultBase(source)
})
const { openViewDescriptionDialog: _openViewDescriptionDialog } = inject(TreeViewInj)!
const input = ref<HTMLInputElement>()
const isDropdownOpen = ref(false)
@ -193,6 +195,12 @@ async function onRename() {
onStopEdit()
}
const openViewDescriptionDialog = (view: ViewType) => {
isDropdownOpen.value = false
_openViewDescriptionDialog(view)
}
/** Cancel renaming view */
function onCancel() {
if (!isEditing.value) return
@ -281,6 +289,15 @@ watch(isDropdownOpen, async () => {
</NcTooltip>
<template v-if="!isEditing && !isLocked">
<NcTooltip v-if="vModel.description?.length" placement="bottom">
<template #title>
{{ vModel.description }}
</template>
<NcButton type="text" class="!hover:bg-transparent" size="xsmall">
<GeneralIcon icon="info" class="!w-3.5 !h-3.5 nc-info-icon group-hover:opacity-100 text-gray-600 opacity-0" />
</NcButton>
</NcTooltip>
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
<NcButton
v-e="['c:view:option']"
@ -305,6 +322,7 @@ watch(isDropdownOpen, async () => {
@close-modal="isDropdownOpen = false"
@rename="onRenameMenuClick"
@delete="onDelete"
@description-update="openViewDescriptionDialog(vModel)"
/>
</template>
</NcDropdown>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import type { TableType } from 'nocodb-sdk'
import type { TableType, ViewType } from 'nocodb-sdk'
import ProjectWrapper from './ProjectWrapper.vue'
const { isUIAllowed } = useRoles()
@ -36,6 +36,46 @@ const setMenuContext = (type: 'base' | 'source' | 'table' | 'main' | 'layout', v
contextMenuTarget.value = value
}
function openViewDescriptionDialog(view: ViewType) {
if (!view || !view.id) return
$e('c:view:description')
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgViewDescriptionUpdate'), {
'modelValue': isOpen,
'view': view,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openTableDescriptionDialog(table: TableType) {
if (!table || !table.id) return
$e('c:table:description')
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgTableDescriptionUpdate'), {
'modelValue': isOpen,
'tableMeta': table,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openRenameTableDialog(table: TableType, _ = false) {
if (!table || !table.source_id) return
@ -159,6 +199,8 @@ provide(TreeViewInj, {
setMenuContext,
duplicateTable,
openRenameTableDialog,
openViewDescriptionDialog,
openTableDescriptionDialog,
contextMenuTarget,
})

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

@ -15,11 +15,16 @@ const fetchPluginApps = async () => {
try {
const plugins = (await $api.plugin.list()).list ?? []
apps.value = plugins.map((p) => ({
...p,
tags: p.tags ? p.tags.split(',') : [],
parsedInput: p.input && JSON.parse(p.input as string),
}))
// filter out email and storage plugins
apps.value = plugins
.filter((p) => {
return !['email', 'storage'].includes(p.category.toLowerCase())
})
.map((p) => ({
...p,
tags: p.tags ? p.tags.split(',') : [],
parsedInput: p.input && JSON.parse(p.input as string),
}))
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -106,6 +111,22 @@ onMounted(async () => {
</div>
</a-modal>
<div class="mb-5">
<a-alert type="warning" border="">
<template #message>
<div class="flex flex-row items-center gap-3">
<GeneralIcon icon="ncAlertCircle" class="text-orange-500 w-6 h-6" />
<span class="font-weight-bold">App Store Deprecation</span>
</div>
</template>
<template #description>
<span class="text-gray-500 ml-9">
App store will soon be removed. Email & Storage plugins are now available in Accounts/Setup page. Rest of the plugins
here will be moved to integrations.
</span>
</template>
</a-alert>
</div>
<div class="flex flex-wrap w-full gap-5">
<a-card
v-for="(app, i) in apps"

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { Form, message } from 'ant-design-vue'
import { validateAndExtractSSLProp } from 'nocodb-sdk'
import { type IntegrationType, validateAndExtractSSLProp } from 'nocodb-sdk'
import {
ClientType,
type DatabricksConnection,
@ -233,8 +233,8 @@ const createSource = async () => {
emit('sourceCreated')
vOpen.value = false
creatingSource.value = false
} else if (status === JobStatus.FAILED) {
message.error('Failed to create base')
} else if (data.status === JobStatus.FAILED) {
message.error(data?.data?.error?.message || 'Failed to create base')
creatingSource.value = false
}
}
@ -358,8 +358,8 @@ const allowDataWrite = computed({
const changeIntegration = (triggerTestConnection = false) => {
if (formState.value.fk_integration_id && selectedIntegration.value) {
formState.value.dataSource = {
client: selectedIntegration.value.sub_type,
connection: {
client: selectedIntegration.value.sub_type,
database: selectedIntegrationDb.value,
},
searchPath: selectedIntegration.value.config?.searchPath,
@ -419,6 +419,22 @@ function handleAutoScroll(scroll: boolean, className: string) {
}
const filterIntegrationCategory = (c: IntegrationCategoryItemType) => [IntegrationCategoryType.DATABASE].includes(c.value)
const isIntgrationDisabled = (integration: IntegrationType = {}) => {
switch (integration.sub_type) {
case ClientType.SQLITE:
return {
isDisabled: integration?.source_count && integration.source_count > 0,
msg: 'Sqlite support only 1 database per connection',
}
default:
return {
isDisabled: false,
msg: '',
}
}
}
</script>
<template>
@ -516,16 +532,32 @@ const filterIntegrationCategory = (c: IntegrationCategoryItemType) => [Integrati
dropdown-match-select-width
@change="changeIntegration()"
>
<a-select-option v-for="integration in integrations" :key="integration.id" :value="integration.id">
<a-select-option
v-for="integration in integrations"
:key="integration.id"
:value="integration.id"
:disabled="isIntgrationDisabled(integration).isDisabled"
>
<div class="w-full flex gap-2 items-center" :data-testid="integration.title">
<GeneralBaseLogo
<GeneralIntegrationIcon
v-if="integration?.sub_type"
:source-type="integration.sub_type"
class="flex-none h-4 w-4"
:type="integration.sub_type"
:style="{
filter: isIntgrationDisabled(integration).isDisabled
? 'grayscale(100%) brightness(115%)'
: undefined,
}"
/>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<NcTooltip
class="flex-1 truncate"
:show-on-truncate-only="!isIntgrationDisabled(integration).isDisabled"
>
<template #title>
{{ integration.title }}
{{
isIntgrationDisabled(integration).isDisabled
? isIntgrationDisabled(integration).msg
: integration.title
}}
</template>
{{ integration.title }}
</NcTooltip>

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

@ -413,10 +413,12 @@ function handleAutoScroll(scroll: boolean, className: string) {
>
<a-select-option v-for="integration in integrations" :key="integration.id" :value="integration.id">
<div class="w-full flex gap-2 items-center" :data-testid="integration.title">
<GeneralBaseLogo
v-if="integration.type"
:source-type="integration.sub_type"
class="flex-none h-4 w-4"
<GeneralIntegrationIcon
v-if="integration?.sub_type"
:type="integration.sub_type"
:style="{
filter: 'grayscale(100%) brightness(115%)',
}"
/>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>

15
packages/nc-gui/components/dlg/SharedBaseDuplicate.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ProjectTypes } from 'nocodb-sdk'
import { ProjectTypes, WorkspaceUserRoles } from 'nocodb-sdk'
const props = defineProps<{
modelValue: boolean
@ -96,6 +96,12 @@ const _duplicate = async () => {
dialogShow.value = false
}
}
const filteredWorkspaces = computed(() => {
return workspacesList.value
?.filter((ws) => ws.roles === WorkspaceUserRoles.OWNER || ws.roles === WorkspaceUserRoles.CREATOR)
.map((w) => ({ label: `${w.title[0].toUpperCase()}${w.title.slice(1)}`, value: w.id }))
})
</script>
<template>
@ -105,12 +111,7 @@ const _duplicate = async () => {
<template v-if="isEeUI">
<div class="my-4">Select workspace to duplicate shared base to:</div>
<NcSelect
v-model:value="selectedWorkspace"
class="w-full"
:options="workspacesList.map((w) => ({ label: `${w.title[0].toUpperCase()}${w.title.slice(1)}`, value: w.id }))"
placeholder="Select Workspace"
/>
<NcSelect v-model:value="selectedWorkspace" class="w-full" :options="filteredWorkspaces" placeholder="Select Workspace" />
</template>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>

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

@ -45,6 +45,13 @@ const { table, createTable, generateUniqueTitle, tables, base } = useTableNew({
const useForm = Form.useForm
const enableDescription = ref(false)
const removeDescription = () => {
table.description = ''
enableDescription.value = false
}
const validators = computed(() => {
return {
title: [
@ -111,6 +118,17 @@ const _createTable = async () => {
}
}
const toggleDescription = () => {
if (enableDescription.value) {
enableDescription.value = false
} else {
enableDescription.value = true
setTimeout(() => {
inputEl.value?.focus()
}, 100)
}
}
onMounted(() => {
generateUniqueTitle()
nextTick(() => {
@ -129,24 +147,27 @@ onMounted(() => {
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2 text-base text-gray-800">
<GeneralIcon icon="table" class="!text-gray-600 w-5 h-5" />
{{ $t('activity.createTable') }}
<div class="flex justify-between w-full items-center">
<div class="flex flex-row items-center gap-x-2 text-base font-semibold text-gray-800">
<GeneralIcon icon="table" class="!text-gray-600 w-5 h-5" />
{{ $t('activity.createTable') }}
</div>
<a href="https://docs.nocodb.com/tables/create-table" target="_blank" class="text-[13px]">
{{ $t('title.docs') }}
</a>
</div>
</template>
<div class="flex flex-col mt-1">
<a-form
layout="vertical"
:model="table"
name="create-new-table-form"
class="flex flex-col gap-5"
@keydown.enter="_createTable"
@keydown.esc="dialogShow = false"
>
<div>
<a-form-item
v-bind="validateInfos.title"
:class="{ '!mb-1': isSnowflake(props.sourceId), '!mb-0': !isSnowflake(props.sourceId) }"
>
<div class="flex flex-col gap-5">
<a-form-item v-bind="validateInfos.title">
<a-input
ref="inputEl"
v-model:value="table.title"
@ -156,6 +177,31 @@ onMounted(() => {
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>
<a-form-item
v-if="enableDescription"
v-bind="validateInfos.description"
:class="{ '!mb-1': isSnowflake(props.sourceId), '!mb-0': !isSnowflake(props.sourceId) }"
>
<div class="flex gap-3 text-gray-800 h-7 mb-1 items-center justify-between">
<span class="text-[13px]">
{{ $t('labels.description') }}
</span>
<NcButton type="text" class="!h-6 !w-5" size="xsmall" @click="removeDescription">
<GeneralIcon icon="delete" class="text-gray-700 w-3.5 h-3.5" />
</NcButton>
</div>
<a-textarea
ref="inputEl"
v-model:value="table.description"
class="nc-input-sm nc-input-text-area nc-input-shadow px-3 !text-gray-800 max-h-[150px] min-h-[100px]"
hide-details
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterTableDescription')"
/>
</a-form-item>
<template v-if="isSnowflake(props.sourceId)">
<a-checkbox v-model:checked="table.is_hybrid" class="!flex flex-row items-center"> Hybrid Table </a-checkbox>
</template>
@ -188,20 +234,32 @@ onMounted(() => {
</a-row>
</div>
</div>
<div class="flex flex-row justify-end gap-x-2">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
v-e="['a:table:create']"
type="primary"
size="small"
:disabled="validateInfos.title.validateStatus === 'error'"
:loading="creating"
@click="_createTable"
>
{{ $t('activity.createTable') }}
<template #loading> {{ $t('title.creatingTable') }} </template>
<div class="flex flex-row justify-between gap-x-2">
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="toggleDescription">
<div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" />
<span class="first-letter:capitalize">
{{ $t('labels.addDescription').toLowerCase() }}
</span>
</div>
</NcButton>
<div v-else></div>
<div class="flex gap-2 items-center">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
v-e="['a:table:create']"
type="primary"
size="small"
:disabled="validateInfos.title.validateStatus === 'error'"
:loading="creating"
@click="_createTable"
>
{{ $t('activity.createTable') }}
<template #loading> {{ $t('title.creatingTable') }} </template>
</NcButton>
</div>
</div>
</a-form>
</div>
@ -209,6 +267,14 @@ onMounted(() => {
</template>
<style scoped lang="scss">
.ant-form-item {
@apply mb-0;
}
.nc-input-text-area {
padding-block: 8px !important;
}
.nc-table-advanced-options {
max-height: 0;
transition: 0.3s max-height;

182
packages/nc-gui/components/dlg/TableDescriptionUpdate.vue

@ -0,0 +1,182 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import type { ComponentPublicInstance } from '@vue/runtime-core'
interface Props {
modelValue?: boolean
tableMeta: TableType
sourceId: string
}
const { tableMeta, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
const { $e, $api } = useNuxtApp()
const { setMeta } = useMetas()
const dialogShow = useVModel(props, 'modelValue', emit)
const { loadProjectTables } = useTablesStore()
const baseStore = useBase()
const { loadTables } = baseStore
const { addUndo, defineProjectScope } = useUndoRedo()
const inputEl = ref<HTMLTextAreaElement>()
const loading = ref(false)
const useForm = Form.useForm
const formState = reactive({
description: '',
})
const validators = computed(() => {
return {
description: [
{
validator: (_: any, _value: any) => {
return new Promise<void>((resolve, _reject) => {
resolve()
})
},
},
],
}
})
const { validateInfos } = useForm(formState, validators)
watchEffect(
() => {
if (tableMeta?.description) formState.description = `${tableMeta.description}`
nextTick(() => {
const input = inputEl.value?.$el as HTMLInputElement
if (input) {
input.setSelectionRange(0, formState.description.length)
input.focus()
}
})
},
{ flush: 'post' },
)
const updateDescription = async (undo = false) => {
if (!tableMeta) return
if (formState.description) {
formState.description = formState.description.trim()
}
loading.value = true
try {
await $api.dbTable.update(tableMeta.id as string, {
base_id: tableMeta.base_id,
description: formState.description,
})
dialogShow.value = false
await loadProjectTables(tableMeta.base_id!, true)
if (!undo) {
addUndo({
redo: {
fn: (t: string) => {
formState.description = t
updateDescription(true, true)
},
args: [formState.description],
},
undo: {
fn: (t: string) => {
formState.description = t
updateDescription(true, true)
},
args: [tableMeta.description],
},
scope: defineProjectScope({ model: tableMeta }),
})
}
await loadTables()
// update metas
const newMeta = await $api.dbTable.read(tableMeta.id as string)
await setMeta(newMeta)
$e('a:table:description:update')
dialogShow.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false
}
</script>
<template>
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false">
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="table" class="w-6 h-6 text-gray-700" />
<span class="text-gray-900 font-bold">
{{ tableMeta?.title ?? tableMeta?.table_name }}
</span>
</div>
</template>
<div class="mt-1">
<a-form layout="vertical" :model="formState" name="create-new-table-form">
<a-form-item :label="$t('labels.description')" v-bind="validateInfos.description">
<a-textarea
ref="inputEl"
v-model:value="formState.description"
class="nc-input-sm !py-2 nc-text-area nc-input-shadow"
hide-details
size="small"
:placeholder="$t('msg.info.enterTableDescription')"
@keydown.enter.exact="() => updateDescription()"
/>
</a-form-item>
</a-form>
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
type="primary"
size="small"
:disabled="
validateInfos?.description?.validateStatus === 'error' || formState.description?.trim() === tableMeta?.description
"
:loading="loading"
@click="() => updateDescription()"
>
{{ $t('general.save') }}
</NcButton>
</div>
</div>
</NcModal>
</template>
<style scoped lang="scss">
.nc-text-area {
@apply !py-2 min-h-[120px] max-h-[200px];
}
:deep(.ant-form-item-label > label) {
@apply !text-md font-base !leading-[20px] text-gray-800 flex;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
@apply content-[''] m-0;
}
}
</style>

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

@ -23,6 +23,7 @@ interface Props {
selectedViewId?: string
groupingFieldColumnId?: string
geoDataFieldColumnId?: string
description?: string
tableId: string
calendarRange?: Array<{
fk_from_column_id: string
@ -40,6 +41,7 @@ interface Emits {
interface Form {
title: string
type: ViewTypes
description?: string
copy_from_id: string | null
// for kanban view only
fk_grp_col_id: string | null
@ -103,6 +105,7 @@ const form = reactive<Form>({
fk_geo_data_col_id: null,
calendar_range: props.calendarRange || [],
fk_cover_image_col_id: null,
description: props.description || '',
})
const viewSelectFieldOptions = ref<SelectProps['options']>([])
@ -243,14 +246,35 @@ const addCalendarRange = async () => {
}
*/
const enableDescription = ref(false)
const removeDescription = () => {
form.description = ''
enableDescription.value = false
}
const toggleDescription = () => {
if (enableDescription.value) {
enableDescription.value = false
} else {
enableDescription.value = true
setTimeout(() => {
inputEl.value?.focus()
}, 100)
}
}
const isMetaLoading = ref(false)
onMounted(async () => {
if (form.copy_from_id) {
enableDescription.value = true
}
if ([ViewTypes.GALLERY, ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(props.type)) {
isMetaLoading.value = true
try {
meta.value = (await getMeta(tableId.value))!
if (props.type === ViewTypes.MAP) {
viewSelectFieldOptions.value = meta
.value!.columns!.filter((el) => el.uidt === UITypes.GeoData)
@ -417,6 +441,15 @@ onMounted(async () => {
}
}
})
const isCalendarReadonly = (calendarRange?: Array<{ fk_from_column_id: string; fk_to_column_id: string | null }>) => {
if (!calendarRange) return false
return calendarRange.some((range) => {
console.log(range)
const column = viewSelectFieldOptions.value?.find((c) => c.value === range?.fk_from_column_id)
return !column || ![UITypes.DateTime, UITypes.Date].includes(column.uidt)
})
}
</script>
<template>
@ -696,6 +729,19 @@ onMounted(async () => {
Add another date field
</NcButton> -->
</div>
<div
v-if="isCalendarReadonly(form.calendar_range)"
class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full"
>
<div class="text-gray-500 flex gap-4">
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="info" />
<div class="flex flex-col gap-1">
<h2 class="font-semibold text-sm mb-0 text-gray-800">Calendar is readonly</h2>
<span class="text-gray-500 font-default text-sm"> {{ $t('msg.info.calendarReadOnly') }}</span>
</div>
</div>
</div>
</template>
</a-form>
<div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
@ -708,28 +754,65 @@ onMounted(async () => {
</div>
</div>
<div class="flex flex-row w-full justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="vModel = false">
{{ $t('general.cancel') }}
</NcButton>
<a-form-item v-if="enableDescription">
<div class="flex gap-3 text-gray-800 h-7 mt-4 mb-1 items-center justify-between">
<span class="text-[13px]">
{{ $t('labels.description') }}
</span>
<NcButton
v-e="[form.copy_from_id ? 'a:view:duplicate' : 'a:view:create']"
:disabled="!isNecessaryColumnsPresent"
:loading="isViewCreating"
type="primary"
size="small"
@click="onSubmit"
>
{{ $t('labels.createView') }}
<template #loading> {{ $t('labels.creatingView') }}</template>
<NcButton type="text" class="!h-6 !w-5" size="xsmall" @click="removeDescription">
<GeneralIcon icon="delete" class="text-gray-700 w-3.5 h-3.5" />
</NcButton>
</div>
<a-textarea
ref="inputEl"
v-model:value="form.description"
class="nc-input-sm nc-input-text-area nc-input-shadow px-3 !text-gray-800 max-h-[150px] min-h-[100px]"
hide-details
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterViewDescription')"
/>
</a-form-item>
<div class="flex flex-row w-full justify-between gap-x-2 mt-5">
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="toggleDescription">
<div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" />
<span class="first-letter:capitalize">
{{ $t('labels.addDescription') }}
</span>
</div>
</NcButton>
<div v-else></div>
<div class="flex gap-2 items-center">
<NcButton type="secondary" size="small" @click="vModel = false">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
v-e="[form.copy_from_id ? 'a:view:duplicate' : 'a:view:create']"
:disabled="!isNecessaryColumnsPresent"
:loading="isViewCreating"
type="primary"
size="small"
@click="onSubmit"
>
{{ $t('labels.createView') }}
<template #loading> {{ $t('labels.creatingView') }}</template>
</NcButton>
</div>
</div>
</div>
</NcModal>
</template>
<style lang="scss" scoped>
.nc-input-text-area {
padding-block: 8px !important;
}
.ant-form-item-required {
@apply !text-gray-800 font-medium;
&:before {

170
packages/nc-gui/components/dlg/ViewDescriptionUpdate.vue

@ -0,0 +1,170 @@
<script setup lang="ts">
import type { ViewType } from 'nocodb-sdk'
import type { ComponentPublicInstance } from '@vue/runtime-core'
interface Props {
modelValue?: boolean
view: ViewType
sourceId?: string
}
const { view, ...props } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
const { $e, $api } = useNuxtApp()
const dialogShow = useVModel(props, 'modelValue', emit)
const { loadViews } = useViewsStore()
const { addUndo, defineProjectScope } = useUndoRedo()
const inputEl = ref<ComponentPublicInstance>()
const loading = ref(false)
const useForm = Form.useForm
const formState = reactive({
description: '',
})
const validators = computed(() => {
return {
description: [
{
validator: (_: any, _value: any) => {
return new Promise<void>((resolve, _reject) => {
resolve()
})
},
},
],
}
})
const { validateInfos } = useForm(formState, validators)
watchEffect(
() => {
if (view?.description) formState.description = `${view.description}`
nextTick(() => {
const input = inputEl.value?.$el as HTMLInputElement
if (input) {
input.setSelectionRange(0, formState.description.length)
input.focus()
}
})
},
{ flush: 'post' },
)
const updateDescription = async (undo = false) => {
if (!view) return
if (formState.description) {
formState.description = formState.description.trim()
}
loading.value = true
try {
await $api.dbView.update(view.id as string, {
description: formState.description,
})
dialogShow.value = false
if (!undo) {
addUndo({
redo: {
fn: (t: string) => {
formState.description = t
updateDescription(true, true)
},
args: [formState.description],
},
undo: {
fn: (t: string) => {
formState.description = t
updateDescription(true, true)
},
args: [view.description],
},
scope: defineProjectScope({ view }),
})
}
await loadViews({ tableId: view.fk_model_id, ignoreLoading: true, force: true })
$e('a:view:description:update')
dialogShow.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false
}
</script>
<template>
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false">
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralViewIcon :meta="view" class="mt-0.5 !text-2xl" />
<span class="text-gray-900 font-semibold">
{{ view?.title }}
</span>
</div>
</template>
<div class="mt-1">
<a-form layout="vertical" :model="formState" name="create-new-table-form">
<a-form-item :label="$t('labels.description')" v-bind="validateInfos.description">
<a-textarea
ref="inputEl"
v-model:value="formState.description"
class="nc-input-sm !py-2 nc-text-area !text-gray-800 nc-input-shadow"
hide-details
size="small"
:placeholder="$t('msg.info.enterTableDescription')"
@keydown.enter.exact="() => updateDescription()"
/>
</a-form-item>
</a-form>
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
type="primary"
size="small"
:disabled="
validateInfos?.description?.validateStatus === 'error' || formState.description?.trim() === view?.description
"
:loading="loading"
@click="() => updateDescription()"
>
{{ $t('general.save') }}
</NcButton>
</div>
</div>
</NcModal>
</template>
<style scoped lang="scss">
.nc-text-area {
@apply !py-2 min-h-[120px] max-h-[200px];
}
:deep(.ant-form-item-label > label) {
@apply !leading-[20px] font-base !text-md text-gray-800 flex;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
@apply content-[''] m-0;
}
}
</style>

226
packages/nc-gui/components/extensions/Details.vue

@ -13,7 +13,7 @@ const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emit)
const { availableExtensions, addExtension, getExtensionIcon, isMarketVisible } = useExtensions()
const { availableExtensions, descriptionContent, addExtension, getExtensionAssetsUrl, isMarketVisible } = useExtensions()
const onBack = () => {
vModel.value = false
@ -29,57 +29,88 @@ const activeExtension = computed(() => {
return availableExtensions.value.find((ext) => ext.id === props.extensionId)
})
const detailsBody = activeExtension.value?.description ? marked.parse(activeExtension.value.description) : '<p></p>'
// Create a custom renderer
const renderer = new marked.Renderer()
// Override the image function to modify the URL
renderer.image = function (href: string, title: string | null, text: string) {
// Modify the URL here
const newUrl = getExtensionAssetsUrl(href)
return `<img src="${newUrl}" alt="${text}" title="${title || ''}">`
}
// Apply the custom renderer to marked
marked.use({ renderer })
const getModifiedContent = (content = '') => {
// Modify raw <img> tags, supporting both single and double quotes
return content.replace(/<img\s+src=(["'])(.*?)\1(.*?)>/g, (match, quote, src, rest) => {
const newSrc = getExtensionAssetsUrl(src)
return `<img src=${quote}${newSrc}${quote}${rest}>`
})
}
const detailsBody = computed(() => {
if (descriptionContent.value[props.extensionId]) {
return marked.parse(getModifiedContent(descriptionContent.value[props.extensionId]))
} else if (activeExtension.value?.description) {
return marked.parse(getModifiedContent(activeExtension.value.description))
}
return '<p></p>'
})
</script>
<template>
<NcModal
v-model:visible="vModel"
:body-style="{ 'max-height': '864px', 'height': '85vh' }"
:class="{ active: vModel }"
:closable="from === 'extension'"
:footer="null"
:width="1154"
size="medium"
wrap-class-name="nc-modal-extension-market"
wrap-class-name="nc-modal-extension-details"
>
<div v-if="activeExtension" class="flex flex-col w-full h-full">
<div v-if="from === 'market'" class="flex-none h-8 flex items-center mb-4">
<NcButton size="xsmall" type="text" class="!bg-gray-200/75 !hover:bg-gray-200 !rounded-full" @click="onBack">
<div class="flex items-center gap-2 px-2">
<GeneralIcon icon="ncArrowLeft" />
<span>Back</span>
</div>
<div class="flex items-center gap-3 p-4 border-b-1 border-gray-200">
<NcButton v-if="from === 'market'" size="small" type="text" @click="onBack">
<GeneralIcon icon="arrowLeft" />
</NcButton>
</div>
<div v-else class="h-8"></div>
<div class="extension-details">
<div class="extension-details-left nc-scrollbar-thin">
<div class="flex gap-6">
<img :src="getExtensionIcon(activeExtension.iconUrl)" alt="icon" class="h-[80px] w-[80px] object-contain" />
<div class="flex flex-col gap-3">
<div class="font-weight-700 text-2xl">{{ activeExtension.title }}</div>
<img :src="getExtensionAssetsUrl(activeExtension.iconUrl)" alt="icon" class="h-[50px] w-[50px] object-contain" />
<div class="flex-1 flex flex-col">
<div class="font-semibold text-xl truncate">{{ activeExtension.title }}</div>
<div class="text-small leading-[18px] text-gray-500 truncate">{{ activeExtension.subTitle }}</div>
</div>
<div class="self-start flex items-center gap-2.5">
<NcButton size="small" class="w-full" @click="onAddExtension(activeExtension)">
<div class="flex items-center justify-center gap-1 -ml-3px">
<GeneralIcon icon="plus" /> {{ $t('general.install') }}
</div>
</div>
</NcButton>
<NcButton size="small" type="text" @click="vModel = false">
<GeneralIcon icon="close" class="text-gray-600" />
</NcButton>
</div>
</div>
<div class="text-base text-gray-600" v-html="detailsBody"></div>
<div class="extension-details">
<div class="extension-details-left">
<div class="nc-extension-details-body" v-html="detailsBody"></div>
</div>
<div class="extension-details-right">
<NcButton class="w-full" @click="onAddExtension(activeExtension)">
<div class="flex items-center justify-center">Add Extension</div>
</NcButton>
<div class="flex flex-col gap-4 nc-scrollbar-thin">
<div class="flex flex-col gap-1">
<div class="extension-details-right-title">Version</div>
<div class="extension-details-right-subtitle">{{ activeExtension.version }}</div>
</div>
<div class="flex flex-col gap-1">
<div v-if="activeExtension.publisherName" class="extension-details-right-title">Publisher</div>
<div class="extension-details-right-subtitle">{{ activeExtension.publisherName }}</div>
</div>
<div v-if="activeExtension.publisherEmail" class="flex flex-col gap-1">
<div class="extension-details-right-section">
<div class="extension-details-right-title">Version</div>
<div class="extension-details-right-subtitle">{{ activeExtension.version }}</div>
</div>
<NcDivider />
<div class="extension-details-right-section">
<div v-if="activeExtension.publisherName" class="extension-details-right-title">Publisher</div>
<div class="extension-details-right-subtitle">{{ activeExtension.publisherName }}</div>
</div>
<template v-if="activeExtension.publisherEmail">
<NcDivider />
<div class="extension-details-right-section">
<div class="extension-details-right-title">Publisher Email</div>
<div class="extension-details-right-subtitle">
<a :href="`mailto:${activeExtension.publisherEmail}`" target="_blank" rel="noopener noreferrer">
@ -87,7 +118,10 @@ const detailsBody = activeExtension.value?.description ? marked.parse(activeExte
</a>
</div>
</div>
<div v-if="activeExtension.publisherUrl" class="flex flex-col gap-1">
</template>
<template v-if="activeExtension.publisherUrl">
<NcDivider />
<div class="extension-details-right-section">
<div class="extension-details-right-title">Publisher Website</div>
<div class="extension-details-right-subtitle">
<a :href="activeExtension.publisherUrl" target="_blank" rel="noopener noreferrer">
@ -95,7 +129,7 @@ const detailsBody = activeExtension.value?.description ? marked.parse(activeExte
</a>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
@ -104,17 +138,21 @@ const detailsBody = activeExtension.value?.description ? marked.parse(activeExte
<style lang="scss" scoped>
.extension-details {
@apply flex w-full h-full gap-8 px-3;
@apply flex w-full h-[calc(100%_-_65px)];
.extension-details-left {
@apply flex flex-col gap-6 w-3/4;
@apply p-6 flex-1 flex flex-col gap-6 nc-scrollbar-thin;
}
.extension-details-right {
@apply w-1/4 flex flex-col gap-4;
@apply p-5 w-[320px] flex flex-col space-y-4 border-l-1 border-gray-200 bg-gray-50 nc-scrollbar-thin;
.extension-details-right-section {
@apply flex flex-col gap-2;
}
.extension-details-right-title {
@apply text-base font-weight-700 text-gray-800;
@apply text-sm font-semibold text-gray-800;
}
.extension-details-right-subtitle {
@apply text-sm font-weight-500 text-gray-600;
@ -122,3 +160,109 @@ const detailsBody = activeExtension.value?.description ? marked.parse(activeExte
}
}
</style>
<style lang="scss">
.nc-modal-extension-details {
.ant-modal-content {
@apply overflow-hidden;
}
.nc-modal {
@apply !p-0;
height: min(calc(100vh - 100px), 1024px);
max-height: min(calc(100vh - 100px), 1024px) !important;
.nc-edit-or-add-integration-left-panel {
@apply w-full p-6 flex-1 flex justify-center;
}
.nc-edit-or-add-integration-right-panel {
@apply p-5 w-[320px] border-l-1 border-gray-200 flex flex-col gap-4 bg-gray-50 rounded-br-2xl;
}
}
.nc-extension-details-body {
p {
@apply !m-0 !leading-5;
}
ul {
li {
@apply ml-4;
list-style-type: disc;
}
}
ol {
@apply !pl-4;
li {
list-style-type: decimal;
}
}
ul,
ol {
@apply !my-0;
}
// Pre tag is the parent wrapper for Code block
pre {
@apply overflow-auto mt-3 bg-gray-100;
border-color: #d0d5dd;
border: 1px;
color: black;
font-family: 'JetBrainsMono', monospace;
padding: 1rem;
border-radius: 0.5rem;
height: fit-content;
code {
@apply !px-0;
}
}
code {
@apply rounded-md px-2 py-1 bg-gray-100;
color: inherit;
font-size: 0.8rem;
}
blockquote {
border-left: 3px solid #d0d5dd;
padding: 0 1em;
color: #666;
margin: 1em 0;
font-style: italic;
}
hr {
@apply !border-gray-300;
border: 0;
border-top: 1px solid #ccc;
margin: 1.5em 0;
}
h1 {
font-weight: 700;
font-size: 1.85rem;
margin-bottom: 0.1rem;
line-height: 36px;
}
h2 {
font-weight: 600;
font-size: 1.55rem;
margin-bottom: 0.1em;
line-height: 30px;
}
h3 {
font-weight: 600;
font-size: 1.15rem;
margin-bottom: 0.1em;
line-height: 24px;
}
}
}
</style>

176
packages/nc-gui/components/extensions/Extension.vue

@ -11,7 +11,7 @@ const {
extensionsLoaded,
availableExtensions,
eventBus,
getExtensionIcon,
getExtensionAssetsUrl,
duplicateExtension,
showExtensionDetails,
} = useExtensions()
@ -56,26 +56,34 @@ const { fullscreen, collapsed } = useProvideExtensionHelper(extension)
const component = ref<any>(null)
const extensionManifest = ref<any>(null)
const extensionManifest = ref<ExtensionManifest | undefined>()
const extensionMinHeight = computed(() => {
switch (extension.value.extensionId) {
case 'nc-data-exporter':
return 'min-h-[300px] h-[300px]'
case 'nc-json-exporter':
return 'min-h-[194px] h-[194px]'
case 'nc-csv-import':
return 'min-h-[180px] h-[180px]'
const fullscreenModalMaxWidth = computed(() => {
const modalMaxWidth = {
xs: 'min(calc(100vw - 32px), 448px)',
sm: 'min(calc(100vw - 32px), 640px)',
md: 'min(calc(100vw - 48px), 900px)',
lg: 'min(calc(100vw - 48px), 1280px)',
}
return extensionManifest.value?.config?.modalMaxWith
? modalMaxWidth[extensionManifest.value?.config?.modalMaxWith] || modalMaxWidth.lg
: modalMaxWidth.lg
})
const expandExtension = () => {
if (!collapsed.value) return
collapsed.value = false
}
onMounted(() => {
until(extensionsLoaded)
.toMatch((v) => v)
.then(() => {
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId)
if (!extensionManifest) {
if (!extensionManifest.value) {
return
}
@ -94,7 +102,11 @@ onMounted(() => {
// close fullscreen on escape key press
useEventListener('keydown', (e) => {
if (e.key === 'Escape') {
// Check if the event target or its closest parent is an input, select, or textarea
const isFormElement = (e?.target as HTMLElement)?.closest('input, select, textarea')
// If the target is not a form element and the key is 'Escape', close fullscreen
if (e.key === 'Escape' && !isFormElement) {
fullscreen.value = false
}
})
@ -132,78 +144,103 @@ eventBus.on((event, payload) => {
<div
class="extension-wrapper"
:class="[
`${!collapsed ? extensionMinHeight : ''}`,
{
'!h-auto': collapsed,
'isOpen': !collapsed,
'mousedown': isMouseDown,
},
]"
:style="
!collapsed
? {
height: extensionManifest?.config?.contentMinHeight,
minHeight: extensionManifest?.config?.contentMinHeight,
}
: {}
"
@mousedown="isMouseDown = true"
@mouseup="isMouseDown = false"
>
<div class="extension-header" :class="{ 'mb-2': !collapsed }">
<div
class="extension-header px-3 py-2"
:class="{
'border-b-1 border-gray-200 h-[49px]': !collapsed,
'collapsed border-transparent h-[48px]': collapsed,
}"
@click="expandExtension"
>
<div class="extension-header-left max-w-[calc(100%_-_100px)]">
<!-- Todo: enable later when we support extension reordering -->
<!-- eslint-disable vue/no-constant-condition -->
<NcButton v-if="false" size="xxsmall" type="text">
<NcButton size="xs" type="text" class="nc-extension-drag-handler !px-1" @click.stop>
<GeneralIcon icon="ncDrag" class="flex-none text-gray-500" />
</NcButton>
<img
v-if="extensionManifest"
:src="getExtensionIcon(extensionManifest.iconUrl)"
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)"
alt="icon"
class="h-6 w-6 object-contain"
class="h-8 w-8 object-contain"
/>
<input
<a-input
v-if="titleEditMode && !fullscreen"
ref="titleInput"
v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none !text-inherit !bg-transparent w-4/5 extension-title"
v-model:value="tempTitle"
type="text"
class="flex-grow !h-8 !px-1 !py-1 !-ml-1 !rounded-lg w-4/5 extension-title"
@click.stop
@keyup.enter="updateExtensionTitle"
@keyup.esc="updateExtensionTitle"
@blur="updateExtensionTitle"
/>
>
</a-input>
<NcTooltip v-else show-on-truncate-only class="truncate">
<template #title>
{{ extension.title }}
</template>
<span class="extension-title" @dblclick="enableEditMode">
<span class="extension-title cursor-pointer" @dblclick.stop="enableEditMode" @click.stop>
{{ extension.title }}
</span>
</NcTooltip>
</div>
<div class="extension-header-right">
<NcButton v-if="!activeError" type="text" size="xxsmall" @click="fullscreen = true">
<GeneralIcon icon="expand" />
</NcButton>
<div class="extension-header-right" @click.stop>
<ExtensionsExtensionMenu
:active-error="activeError"
class="nc-extension-menu"
@rename="enableEditMode"
@duplicate="handleDuplicateExtension(extension.id, true)"
@show-details="showExtensionDetails(extension.extensionId, 'extension')"
@clear-data="extension.clear()"
@delete="extension.delete()"
/>
<NcButton size="xxsmall" type="text" @click="collapsed = !collapsed">
<GeneralIcon :icon="collapsed ? 'arrowUp' : 'arrowDown'" class="flex-none" />
<NcButton v-if="!activeError" type="text" size="xs" class="nc-extension-expand-btn !px-1" @click="fullscreen = true">
<GeneralIcon icon="ncMaximize2" class="h-3.5 w-3.5" />
</NcButton>
<NcButton size="xs" type="text" class="!px-1" @click="collapsed = !collapsed">
<GeneralIcon :icon="collapsed ? 'arrowDown' : 'arrowUp'" class="flex-none" />
</NcButton>
</div>
</div>
<template v-if="activeError">
<div v-show="!collapsed" class="extension-content">
<a-result status="error" title="Extension Error">
<div
v-show="!collapsed"
class="extension-content nc-scrollbar-thin h-[calc(100%_-_50px)] flex items-center justify-center"
:class="{
fullscreen,
}"
>
<a-result status="error" title="Extension Error" class="nc-extension-error">
<template #subTitle>{{ activeError }}</template>
<template #extra>
<NcButton @click="extension.clear()">
<NcButton size="small" @click="extension.clear()">
<div class="flex items-center gap-2">
<GeneralIcon icon="reload" />
Clear Data
</div>
</NcButton>
<NcButton type="danger" @click="extension.delete()">
<NcButton size="small" type="danger" @click="extension.delete()">
<div class="flex items-center gap-2">
<GeneralIcon icon="delete" />
Delete
@ -217,33 +254,45 @@ eventBus.on((event, payload) => {
<Teleport to="body" :disabled="!fullscreen">
<div
ref="extensionModalRef"
:class="{ 'extension-modal': fullscreen, 'h-[calc(100%_-_32px)]': !fullscreen }"
:class="{ 'extension-modal': fullscreen, 'h-[calc(100%_-_50px)]': !fullscreen }"
@click="closeFullscreen"
>
<div :class="{ 'extension-modal-content': fullscreen, 'h-full': !fullscreen }">
<div
:class="{ 'extension-modal-content': fullscreen, 'h-full': !fullscreen }"
:style="
fullscreen
? {
maxWidth: fullscreenModalMaxWidth,
}
: {}
"
>
<div v-if="fullscreen" class="flex items-center justify-between cursor-default">
<div class="flex-1 max-w-[calc(100%_-_96px)] flex items-center gap-2 text-gray-800 font-weight-600">
<div class="flex-1 max-w-[calc(100%_-_96px)] flex items-center gap-2 text-gray-800 font-semibold">
<img
v-if="extensionManifest"
:src="getExtensionIcon(extensionManifest.iconUrl)"
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)"
alt="icon"
class="flex-none w-6 h-6"
class="flex-none w-8 h-8"
/>
<input
<a-input
v-if="titleEditMode"
ref="titleInput"
v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none !text-xl !bg-transparent !font-weight-600"
v-model:value="tempTitle"
type="text"
class="flex-grow !h-8 !px-1 !py-1 !-ml-1 !rounded-lg !text-lg font-semibold extension-title max-w-[420px]"
@click.stop
@keyup.enter="updateExtensionTitle"
@keyup.esc="updateExtensionTitle"
@keyup.enter.stop="updateExtensionTitle"
@keyup.esc.stop="updateExtensionTitle"
@blur="updateExtensionTitle"
/>
<NcTooltip v-else show-on-truncate-only class="extension-title truncate text-xl">
>
</a-input>
<NcTooltip v-else show-on-truncate-only class="extension-title truncate text-lg">
<template #title>
{{ extension.title }}
</template>
<span @dblclick="enableEditMode">
<span class="cursor-pointer" @dblclick="enableEditMode">
{{ extension.title }}
</span>
</NcTooltip>
@ -266,7 +315,7 @@ eventBus.on((event, payload) => {
<div
v-show="fullscreen || !collapsed"
class="extension-content"
:class="{ 'h-[calc(100%-40px)]': fullscreen, 'h-full': !fullscreen }"
:class="{ 'fullscreen h-[calc(100%-40px)]': fullscreen, 'h-full': !fullscreen }"
>
<component :is="component" :key="extension.uiKey" class="h-full" />
</div>
@ -280,7 +329,7 @@ eventBus.on((event, payload) => {
<style scoped lang="scss">
.extension-wrapper {
@apply bg-white rounded-xl px-3 py-[11px] w-full border-1 relative;
@apply bg-white rounded-xl w-full border-1 relative;
&.isOpen {
resize: vertical;
@ -295,12 +344,19 @@ eventBus.on((event, payload) => {
.extension-header {
@apply flex justify-between;
&.collapsed:not(:hover) {
.nc-extension-expand-btn,
.nc-extension-menu {
@apply hidden;
}
}
.extension-header-left {
@apply flex-1 flex items-center gap-2;
}
.extension-header-right {
@apply flex items-center gap-2;
@apply flex items-center gap-1;
}
.extension-title {
@ -310,13 +366,35 @@ eventBus.on((event, payload) => {
.extension-content {
@apply rounded-lg;
&:not(.fullscreen) {
@apply p-3;
}
}
.extension-modal {
@apply absolute top-0 left-0 z-1000 w-full h-full bg-black bg-opacity-50;
.extension-modal-content {
@apply bg-white rounded-2xl w-[90%] max-w-[1154px] h-[90vh] mt-[5vh] mx-auto p-6 flex flex-col gap-3;
@apply bg-white rounded-2xl w-[90%] h-[90vh] mt-[5vh] mx-auto p-6 flex flex-col gap-3;
}
}
:deep(.nc-extension-error.ant-result) {
@apply p-0;
.ant-result-icon {
@apply mb-3;
& > span {
@apply text-[32px];
}
}
.ant-result-title {
@apply text-base text-gray-800 font-semibold;
}
.ant-result-extra {
@apply mt-3;
}
}
</style>

12
packages/nc-gui/components/extensions/ExtensionMenu.vue

@ -10,23 +10,23 @@ const emits = defineEmits(['rename', 'duplicate', 'showDetails', 'clearData', 'd
<template>
<div class="flex items-center">
<NcDropdown :trigger="['click']">
<NcButton type="text" :size="fullscreen ? 'small' : 'xxsmall'">
<NcDropdown :trigger="['click']" placement="bottomRight">
<NcButton type="text" :size="fullscreen ? 'small' : 'xs'" class="!px-1">
<GeneralIcon icon="threeDotVertical" />
</NcButton>
<template #overlay>
<NcMenu>
<template v-if="!activeError">
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="emits('rename')">
<NcMenuItem data-rec="true" @click="emits('rename')">
<GeneralIcon icon="edit" />
Rename
</NcMenuItem>
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="emits('duplicate')">
<NcMenuItem data-rec="true" @click="emits('duplicate')">
<GeneralIcon icon="duplicate" />
Duplicate
</NcMenuItem>
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="emits('showDetails')">
<NcMenuItem data-rec="true" @click="emits('showDetails')">
<GeneralIcon icon="info" />
Details
</NcMenuItem>
@ -34,7 +34,7 @@ const emits = defineEmits(['rename', 'duplicate', 'showDetails', 'clearData', 'd
</template>
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="emits('clearData')">
<GeneralIcon icon="reload" />
Clear Data
Clear data
</NcMenuItem>
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="emits('delete')">
<GeneralIcon icon="delete" />

144
packages/nc-gui/components/extensions/Market.vue

@ -9,7 +9,7 @@ const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emit)
const { availableExtensions, addExtension, getExtensionIcon, showExtensionDetails } = useExtensions()
const { availableExtensions, addExtension, getExtensionAssetsUrl, showExtensionDetails } = useExtensions()
const searchQuery = ref<string>('')
@ -17,7 +17,7 @@ const filteredAvailableExtensions = computed(() =>
(availableExtensions.value || []).filter(
(ext) =>
ext.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
ext.description.toLowerCase().includes(searchQuery.value.toLowerCase()),
ext.subTitle.toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
@ -35,72 +35,77 @@ const onAddExtension = (ext: any) => {
<template>
<NcModal
v-model:visible="vModel"
:body-style="{ 'max-height': '864px', 'height': '85vh' }"
:class="{ active: vModel }"
:closable="true"
:footer="null"
:width="1154"
size="medium"
wrap-class-name="nc-modal-extension-market"
>
<template #header>
<div class="flex items-center gap-2 pb-2">
<GeneralIcon icon="puzzle" class="h-5 w-5 flex-none" />
<div class="font-weight-700 text-base">Extensions Marketplace</div>
<div class="h-full">
<div class="flex items-center gap-3 p-4 border-b-1 border-gray-200">
<GeneralIcon icon="ncPuzzleSolid" class="h-6 w-6 flex-none text-gray-700" />
<div class="flex-1 font-semibold text-xl">Extensions Marketplace</div>
<NcButton size="small" type="text" @click="vModel = false">
<GeneralIcon icon="close" class="text-gray-600" />
</NcButton>
</div>
</template>
<div class="flex flex-col h-[calc(100%_-_41px)]">
<div class="h-full flex flex-col gap-4 flex-1 pt-2">
<div class="flex flex max-w-[470px]">
<a-input
v-model:value="searchQuery"
type="text"
class="!h-10 !px-3 !py-1 !rounded-lg"
placeholder="Search for an extension..."
allow-clear
<div class="flex flex-col h-[calc(100%_-_65px)] px-6 py-4">
<div class="h-full flex flex-col gap-6 flex-1 pt-2">
<div class="flex flex max-w-[470px]">
<a-input
v-model:value="searchQuery"
type="text"
class="nc-input-border-on-value !h-8 !px-3 !py-1 !rounded-lg"
placeholder="Search for an extension..."
allow-clear
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" />
</template>
</a-input>
</div>
<div
class="max-h-[calc(100%_-_40px)] flex flex-wrap gap-3 nc-scrollbar-thin pb-2"
:class="{
'h-full': searchQuery && !filteredAvailableExtensions.length && availableExtensions.length,
}"
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" />
</template>
</a-input>
</div>
<div
class="max-h-[calc(100%_-_40px)] flex flex-wrap gap-3 nc-scrollbar-thin"
:class="{
'h-full': searchQuery && !filteredAvailableExtensions.length && availableExtensions.length,
}"
>
<template v-for="ext of filteredAvailableExtensions" :key="ext.id">
<div class="flex border-1 rounded-xl p-3 w-[360px] cursor-pointer" @click="onExtensionClick(ext.id)">
<div class="h-[60px] w-[60px] overflow-hidden m-auto">
<img :src="getExtensionIcon(ext.iconUrl)" alt="icon" class="w-full h-full object-contain" />
</div>
<div class="flex flex-grow flex-col gap-2 ml-3">
<div class="flex justify-between gap-1">
<div class="font-weight-600">{{ ext.title }}</div>
<NcButton size="xsmall" type="secondary" @click.stop="onAddExtension(ext)">
<div class="flex items-center gap-2 mx-1">
<GeneralIcon icon="plus" />
Add
</div>
</NcButton>
<template v-for="ext of filteredAvailableExtensions" :key="ext.id">
<div
class="nc-market-extension-item flex border-1 rounded-xl p-3 w-[360px] cursor-pointer hover:bg-gray-50 transition-all"
@click="onExtensionClick(ext.id)"
>
<div class="h-[60px] w-[60px] overflow-hidden m-auto">
<img :src="getExtensionAssetsUrl(ext.iconUrl)" alt="icon" class="w-full h-full object-contain" />
</div>
<div class="flex flex-grow flex-col gap-1 ml-3">
<div class="flex justify-between gap-1">
<div class="font-weight-600 text-base">{{ ext.title }}</div>
<NcButton size="xsmall" type="secondary" class="!px-7px" @click.stop="onAddExtension(ext)">
<div class="flex items-center gap-1 -ml-3px text-small">
<GeneralIcon icon="plus" />
{{ $t('general.install') }}
</div>
</NcButton>
</div>
<div class="w-[250px] h-[32px] text-xs text-gray-500 line-clamp-2">{{ ext.subTitle }}</div>
</div>
<div class="w-[250px] h-[32px] text-xs text-gray-500 line-clamp-2">{{ ext.description }}</div>
</div>
</div>
</template>
<div
v-if="searchQuery && !filteredAvailableExtensions.length && availableExtensions.length"
class="w-full h-full flex items-center justify-center"
>
<div class="pb-6 text-gray-500 flex flex-col items-center gap-6 text-center">
<img
src="~assets/img/placeholder/no-search-result-found.png"
class="!w-[164px] flex-none"
alt="No search results found"
/>
</template>
<div
v-if="searchQuery && !filteredAvailableExtensions.length && availableExtensions.length"
class="w-full h-full flex items-center justify-center"
>
<div class="pb-6 text-gray-500 flex flex-col items-center gap-6 text-center">
<img
src="~assets/img/placeholder/no-search-result-found.png"
class="!w-[164px] flex-none"
alt="No search results found"
/>
{{ $t('title.noResultsMatchedYourSearch') }}
{{ $t('title.noResultsMatchedYourSearch') }}
</div>
</div>
</div>
</div>
@ -109,4 +114,27 @@ const onAddExtension = (ext: any) => {
</NcModal>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.nc-market-extension-item {
&:hover {
box-shadow: 0px 4px 8px -2px rgba(0, 0, 0, 0.08), 0px 2px 4px -2px rgba(0, 0, 0, 0.04);
}
}
</style>
<style lang="scss">
.nc-modal-extension-market {
.nc-modal {
@apply !p-0;
height: min(calc(100vh - 100px), 1024px);
max-height: min(calc(100vh - 100px), 1024px) !important;
.nc-edit-or-add-integration-left-panel {
@apply w-full p-6 flex-1 flex justify-center;
}
.nc-edit-or-add-integration-right-panel {
@apply p-5 w-[320px] border-l-1 border-gray-200 flex flex-col gap-4 bg-gray-50 rounded-br-2xl;
}
}
}
</style>

231
packages/nc-gui/components/extensions/Pane.vue

@ -1,6 +1,8 @@
<script setup lang="ts">
import { Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import Draggable from 'vuedraggable'
import type { ExtensionType } from '#imports'
const {
extensionList,
@ -10,13 +12,40 @@ const {
detailsFrom,
isMarketVisible,
extensionPanelSize,
toggleExtensionPanel,
updateExtension,
} = useExtensions()
const { $e } = useNuxtApp()
const isReady = ref(false)
const searchExtensionRef = ref<HTMLInputElement>()
const extensionHeaderRef = ref<HTMLDivElement>()
const searchQuery = ref<string>('')
const showSearchBox = ref(false)
const { width } = useElementSize(extensionHeaderRef)
const isOpenSearchBox = computed(() => {
return !!(searchQuery.value || showSearchBox.value)
})
const handleShowSearchInput = () => {
showSearchBox.value = true
nextTick(() => {
searchExtensionRef.value?.focus()
})
}
const handleCloseSearchbox = () => {
showSearchBox.value = false
searchQuery.value = ''
}
const filteredExtensionList = computed(() =>
(extensionList.value || []).filter((ext) => ext.title.toLowerCase().includes(searchQuery.value.toLowerCase())),
)
@ -33,6 +62,44 @@ const normalizePaneMaxWidth = computed(() => {
}
})
const onMove = async (_event: { moved: { newIndex: number; oldIndex: number; element: ExtensionType } }) => {
let {
moved: { newIndex = 0, oldIndex = 0, element },
} = _event
element = extensionList.value?.find((ext) => ext.id === element.id) || element
if (!element?.id) return
newIndex = extensionList.value.findIndex((ext) => ext.id === filteredExtensionList.value[newIndex].id)
oldIndex = extensionList.value.findIndex((ext) => ext.id === filteredExtensionList.value[oldIndex].id)
let nextOrder: number
// set new order value based on the new order of the items
if (extensionList.value.length - 1 === newIndex) {
// If moving to the end, set nextOrder greater than the maximum order in the list
nextOrder = Math.max(...extensionList.value.map((item) => item?.order ?? 0)) + 1
} else if (newIndex === 0) {
// If moving to the beginning, set nextOrder smaller than the minimum order in the list
nextOrder = Math.min(...extensionList.value.map((item) => item?.order ?? 0)) / 2
} else {
nextOrder =
(parseFloat(String(extensionList.value[newIndex - 1]?.order ?? 0)) +
parseFloat(String(extensionList.value[newIndex + 1]?.order ?? 0))) /
2
}
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : oldIndex
await updateExtension(element.id, {
order: _nextOrder,
})
$e('a:extension:reorder')
}
defineExpose({
onReady: () => {
isReady.value = true
@ -46,6 +113,20 @@ watch(isPanelExpanded, (newValue) => {
}, 300)
}
})
onClickOutside(searchExtensionRef, () => {
if (searchQuery.value) {
return
}
showSearchBox.value = false
})
onMounted(() => {
if (searchQuery.value && !showSearchBox.value) {
showSearchBox.value = true
}
})
</script>
<template>
@ -54,96 +135,112 @@ watch(isPanelExpanded, (newValue) => {
:size="extensionPanelSize"
min-size="10%"
max-size="60%"
class="flex flex-col gap-3 bg-[#F0F3FF]"
class="nc-extension-pane"
:style="{
minWidth: isReady ? '300px' : `${normalizePaneMaxWidth}%`,
maxWidth: `${normalizePaneMaxWidth}%`,
}"
>
<div class="flex justify-between items-center px-4 pt-3">
<div class="flex items-center gap-3 font-weight-700 text-brand-500 text-base">
<GeneralIcon icon="puzzle" class="h-5 w-5" /> Extensions
<div
ref="extensionHeaderRef"
class="h-[var(--toolbar-height)] flex items-center gap-3 px-4 py-2 border-b-1 border-gray-200 bg-white"
>
<div
class="flex items-center gap-3 font-weight-700 text-gray-700 text-base"
:class="{
'flex-1': !isOpenSearchBox,
}"
>
<GeneralIcon icon="ncPuzzleSolid" class="h-5 w-5 text-gray-700 opacity-85" />
<span v-if="!isOpenSearchBox || width >= 507">Extensions</span>
</div>
<NcTooltip class="flex" hide-on-click placement="topRight">
<template #title> Hide extensions </template>
<NcButton
size="xxsmall"
type="text"
class="!text-gray-700 !hover:text-gray-800 !hover:bg-gray-200"
@click="toggleExtensionPanel"
>
<div class="flex items-center justify-center">
<GeneralIcon icon="doubleRightArrow" class="flex-none !text-gray-500/75" />
</div>
</NcButton>
</NcTooltip>
</div>
<template v-if="extensionList.length === 0">
<div class="flex items-center flex-col gap-4 w-full nc-scrollbar-md text-center px-4">
<div class="w-[180px] h-[180px] bg-[#d9d9d9] rounded-3xl mt-[100px]"></div>
<div class="font-weight-700 text-base">No extensions added</div>
<div>Add Extensions from the community extensions marketplace</div>
<NcButton size="small" @click="toggleMarket">
<div class="flex items-center gap-2 font-weight-600">
<GeneralIcon icon="plus" />
Add Extension
</div>
<div
class="flex justify-end"
:class="{
'flex-1': isOpenSearchBox,
}"
>
<NcButton v-if="!isOpenSearchBox" size="xs" type="text" class="!px-1" @click="handleShowSearchInput">
<GeneralIcon icon="search" class="flex-none !text-gray-500" />
</NcButton>
</div>
</template>
<template v-else>
<div class="flex w-full items-center justify-between px-4">
<div class="flex flex-grow items-center mr-2">
<div v-else class="flex flex-grow items-center justify-end !max-w-[300px]">
<a-input
ref="searchExtensionRef"
v-model:value="searchQuery"
type="text"
class="!h-8 !px-3 !py-1 !rounded-lg"
class="nc-input-border-on-value !h-7 !px-3 !py-1 !rounded-lg"
placeholder="Search Extension"
allow-clear
@keydown.esc="handleCloseSearchbox"
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" />
</template>
</a-input>
</div>
<NcButton type="ghost" size="small" class="!text-primary !bg-white children:children:max-w-full" @click="toggleMarket">
<div class="flex items-center gap-1 text-xs max-w-full">
</div>
<NcButton type="secondary" size="xs" @click="toggleMarket">
<div class="flex items-center gap-1 text-xs max-w-full -ml-3px">
<GeneralIcon icon="plus" />
{{ $t('general.install') }}
</div>
</NcButton>
</div>
<template v-if="extensionList.length === 0">
<div class="flex items-center flex-col gap-4 w-full nc-scrollbar-md text-center p-4">
<GeneralIcon icon="ncPuzzleSolid" class="h-12 w-12 flex-none mt-[120px] text-gray-500 !stroke-transparent" />
<div class="font-weight-700 text-base">No extensions added</div>
<div class="text-sm text-gray-700">Add Extensions from the community extensions marketplace</div>
<NcButton size="small" @click="toggleMarket">
<div class="flex items-center gap-1 -ml-3px">
<GeneralIcon icon="plus" />
<NcTooltip
class="max-w-[calc(100%_-_16px)] truncate"
show-on-truncate-only
overlay-class-name="children:-ml-2"
modifier-key=""
>
<template #title> Add Extension </template>
Add Extension
</NcTooltip>
{{ $t('general.install') }}
</div>
</NcButton>
<!-- Todo: add docs link -->
<NcButton size="small" type="secondary">
<div class="flex items-center gap-1.5">
<GeneralIcon icon="externalLink" />
{{ $t('activity.goToDocs') }}
</div>
</NcButton>
</div>
<div
class="nc-extension-list-wrapper flex items-center flex-col gap-3 w-full nc-scrollbar-md"
</template>
<template v-else>
<Draggable
:model-value="filteredExtensionList"
draggable=".nc-extension-item"
item-key="id"
handle=".nc-extension-drag-handler"
ghost-class="ghost"
class="nc-extension-list-wrapper flex items-center flex-col gap-3 w-full nc-scrollbar-md py-4"
:class="{
'h-full': searchQuery && !filteredExtensionList.length && extensionList.length,
}"
@start="(e) => e.target.classList.add('grabbing')"
@end="(e) => e.target.classList.remove('grabbing')"
@change="onMove($event)"
>
<ExtensionsWrapper v-for="ext in filteredExtensionList" :key="ext.id" :extension-id="ext.id" />
<div
v-if="searchQuery && !filteredExtensionList.length && extensionList.length"
class="w-full h-full flex-1 flex items-center justify-center"
>
<div class="pb-6 text-gray-500 flex flex-col items-center gap-6 text-center">
<img
src="~assets/img/placeholder/no-search-result-found.png"
class="!w-[164px] flex-none"
alt="No search results found"
/>
{{ $t('title.noResultsMatchedYourSearch') }}
<template #item="{ element: ext }">
<div class="nc-extension-item w-full">
<ExtensionsWrapper :extension-id="ext.id" />
</div>
</div>
</div>
</template>
<template v-if="searchQuery && !filteredExtensionList.length && extensionList.length" #header>
<div class="w-full h-full flex-1 flex items-center justify-center">
<div class="pb-6 text-gray-500 flex flex-col items-center gap-6 text-center">
<img
src="~assets/img/placeholder/no-search-result-found.png"
class="!w-[164px] flex-none"
alt="No search results found"
/>
{{ $t('title.noResultsMatchedYourSearch') }}
</div>
</div>
</template>
</Draggable>
</template>
<ExtensionsMarket v-if="isMarketVisible" v-model="isMarketVisible" />
<ExtensionsDetails
@ -161,4 +258,10 @@ watch(isPanelExpanded, (newValue) => {
@apply pb-3;
}
}
.nc-extension-pane {
@apply flex flex-col bg-gray-50 rounded-l-xl border-1 border-gray-200 z-30 -mt-1px;
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.16), 0px 8px 8px -4px rgba(0, 0, 0, 0.04);
}
</style>

27
packages/nc-gui/components/general/IntegrationIcon.vue

@ -0,0 +1,27 @@
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
type: keyof typeof allIntegrationsMapByValue
size: 'sx' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
}>(),
{
size: 'sm',
},
)
</script>
<template>
<component
:is="allIntegrationsMapByValue[props.type]?.icon"
v-if="allIntegrationsMapByValue[props.type]?.icon"
class="stroke-transparent flex-none"
:class="{
'w-3.5 h-3.5': size === 'sx',
'w-4 h-4': size === 'sm',
'w-5 h-5': size === 'md',
'w-6 h-6': size === 'lg',
'w-7 h-7': size === 'xl',
'w-8 h-8': size === 'xxl',
}"
/>
</template>

10
packages/nc-gui/components/general/UserIcon.vue

@ -6,11 +6,13 @@ const props = withDefaults(
size?: 'small' | 'medium' | 'base' | 'large' | 'xlarge' | 'auto'
name?: string
email?: string
disabled?: boolean
}>(),
{
size: 'medium',
name: '',
email: '',
disabled: false,
},
)
@ -19,11 +21,19 @@ const { size, email } = toRefs(props)
const displayName = computed(() => props.name?.trim() || '')
const backgroundColor = computed(() => {
if (props.disabled) {
return '#bbbbbb'
}
// in comments we need to generate user icon from email
return displayName.value ? stringToColor(displayName.value) : email.value ? stringToColor(email.value) : '#FFFFFF'
})
const usernameInitials = computed(() => {
if (props.disabled) {
return ''
}
const displayNameSplit = displayName.value?.split(' ').filter((name) => name) ?? []
if (displayNameSplit.length > 0) {

4
packages/nc-gui/components/monaco/formula.ts

@ -62,7 +62,7 @@ const generateLanguageDefinition = (identifiers: string[]) => {
],
[/\d+/, 'number'],
[/[-+/*=<>!]+/, 'operator'],
[/[{}()\[\]]/, '@brackets'],
[/[{}()]/, '@brackets'],
[/[ \t\r\n]+/, 'white'],
],
@ -86,12 +86,10 @@ const generateLanguageDefinition = (identifiers: string[]) => {
const languageConfiguration: languages.LanguageConfiguration = {
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },

2
packages/nc-gui/components/nc/Button.vue

@ -97,7 +97,7 @@ useEventListener(NcButton, 'mousedown', () => {
'justify-center': props.centered,
'justify-start': !props.centered,
}"
class="flex flex-row gap-x-2.5 w-full"
class="flex flex-row gap-x-2.5 nc-btn-inner w-full"
>
<template v-if="iconPosition === 'left'">
<GeneralLoader

7
packages/nc-gui/components/nc/DatePicker.vue

@ -8,6 +8,7 @@ interface Props {
isCellInputField?: boolean
type: 'date' | 'time' | 'year' | 'month'
isOpen: boolean
showCurrentDateOption?: boolean | 'disabled'
}
const props = withDefaults(defineProps<Props>(), {
@ -18,7 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
type: 'date',
isOpen: false,
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek', 'currentDate'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
@ -133,7 +134,9 @@ onMounted(() => {
:is-monday-first="false"
is-cell-input-field
size="medium"
:show-current-date-option="showCurrentDateOption"
@update:picker-type="handleUpdatePickerType"
@current-date="emit('currentDate', $event)"
/>
<NcMonthYearSelector
v-if="['month', 'year'].includes(tempPickerType)"
@ -143,7 +146,9 @@ onMounted(() => {
:is-year-picker="tempPickerType === 'year'"
is-cell-input-field
size="medium"
:show-current-date-option="showCurrentDateOption"
@update:picker-type="handleUpdatePickerType"
@current-date="emit('currentDate', $event)"
/>
</template>

19
packages/nc-gui/components/nc/DateWeekSelector.vue

@ -15,6 +15,7 @@ interface Props {
} | null
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
showCurrentDateOption?: boolean | 'disabled'
}
const props = withDefaults(defineProps<Props>(), {
@ -29,7 +30,7 @@ const props = withDefaults(defineProps<Props>(), {
isCellInputField: false,
pickerType: 'date',
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek', 'update:pickerType'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek', 'update:pickerType', 'currentDate'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
@ -250,10 +251,24 @@ const paginate = (action: 'next' | 'prev') => {
</span>
</span>
</div>
<div v-if="isCellInputField" class="flex items-center justify-center px-2 pb-2 pt-1">
<div v-if="isCellInputField" class="flex items-center justify-center px-2 pb-2 pt-1 gap-2">
<NcButton class="nc-date-picker-now-btn !h-7" size="small" type="secondary" @click="handleSelectDate(dayjs())">
<span class="text-small"> {{ $t('labels.today') }} </span>
</NcButton>
<NcTooltip v-if="showCurrentDateOption" :disabled="showCurrentDateOption !== 'disabled'">
<template #title>
{{ $t('tooltip.currentDateNotAvail') }}
</template>
<NcButton
class="nc-date-picker-current-date-btn !h-7"
size="small"
type="secondary"
:disabled="showCurrentDateOption === 'disabled'"
@click="emit('currentDate')"
>
<span class="text-small"> {{ $t('labels.currentDate') }} </span>
</NcButton>
</NcTooltip>
</div>
</div>
</div>

17
packages/nc-gui/components/nc/List.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { useVirtualList } from '@vueuse/core'
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'
export type RawValueType = string | number
@ -30,12 +29,20 @@ interface Props {
optionLabelKey?: string
/** Whether the list is open or closed */
open?: boolean
/** Whether to close the list after an item is selected */
/**
* Whether to close the list after an item is selected
* @default true
*/
closeOnSelect?: boolean
/** Placeholder text for the search input */
searchInputPlaceholder?: string
/** Whether to show the currently selected option */
showSelectedOption?: boolean
/**
* The height of each item in the list, used for virtual list rendering.
* @default 38
*/
itemHeight?: number
/** Custom filter function for list items */
filterOption?: (input: string, option: ListItem, index: Number) => boolean
}
@ -49,9 +56,11 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
open: false,
closeOnSelect: true,
searchInputPlaceholder: '',
showSelectedOption: true,
optionValueKey: 'value',
optionLabelKey: 'label',
itemHeight: 38,
})
const emits = defineEmits<Emits>()
@ -102,7 +111,7 @@ const {
wrapperProps,
scrollTo,
} = useVirtualList(list, {
itemHeight: 38,
itemHeight: props.itemHeight,
})
/**
@ -253,7 +262,7 @@ watch(
<a-input
ref="inputRef"
v-model:value="searchQuery"
:placeholder="searchInputPlaceholder || $t('placeholder.searchFields')"
:placeholder="searchInputPlaceholder"
class="nc-toolbar-dropdown-search-field-input !pl-2 !pr-1.5"
allow-clear
:bordered="false"

4
packages/nc-gui/components/nc/Modal.vue

@ -8,6 +8,7 @@ const props = withDefaults(
maskClosable?: boolean
showSeparator?: boolean
wrapClassName?: string
closable?: boolean
}>(),
{
size: 'medium',
@ -15,6 +16,7 @@ const props = withDefaults(
maskClosable: true,
showSeparator: true,
wrapClassName: '',
closable: false,
},
)
@ -89,7 +91,7 @@ const slots = useSlots()
:class="{ active: visible }"
:width="width"
:centered="true"
:closable="false"
:closable="closable"
:wrap-class-name="newWrapClassName"
:footer="null"
:mask-closable="maskClosable"

20
packages/nc-gui/components/nc/MonthYearSelector.vue

@ -8,6 +8,7 @@ interface Props {
hideCalendar?: boolean
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
showCurrentDateOption?: boolean | 'disabled'
}
const props = withDefaults(defineProps<Props>(), {
@ -18,7 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
isCellInputField: false,
pickerType: 'date',
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:pickerType'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:pickerType', 'currentDate'])
const pageDate = useVModel(props, 'pageDate', emit)
@ -183,6 +184,23 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</span>
</template>
</div>
<div v-if="showCurrentDateOption" class="flex items-center justify-center px-2 pb-2 pt-1">
<NcTooltip :disabled="showCurrentDateOption !== 'disabled'">
<template #title>
{{ $t('tooltip.currentDateNotAvail') }}
</template>
<NcButton
class="nc-date-picker-now-btn !h-7"
size="small"
type="secondary"
:disabled="showCurrentDateOption === 'disabled'"
@click="emit('currentDate')"
>
<span class="text-small"> {{ $t('labels.currentDate') }} </span>
</NcButton>
</NcTooltip>
</div>
</div>
</div>
</template>

16
packages/nc-gui/components/nc/PageHeader.vue

@ -1,7 +1,19 @@
<script lang="ts" setup></script>
<script lang="ts" setup>
interface Props {
bottomBorder?: boolean
}
withDefaults(defineProps<Props>(), {
bottomBorder: true,
})
</script>
<template>
<div class="nc-page-header">
<div
class="nc-page-header"
:class="{
'border-b-1 border-gray-200': bottomBorder,
}"
>
<div class="flex-1 flex items-start gap-3">
<div v-if="$slots.icon" class="h-7 flex items-center children:flex-none">
<slot name="icon"></slot>

19
packages/nc-gui/components/nc/TimeSelector.vue

@ -7,6 +7,7 @@ interface Props {
isMinGranularityPicker?: boolean
minGranularity?: number
isOpen?: boolean
showCurrentDateOption?: boolean | 'disabled'
}
const props = withDefaults(defineProps<Props>(), {
@ -16,7 +17,7 @@ const props = withDefaults(defineProps<Props>(), {
minGranularity: 30,
isOpen: false,
})
const emit = defineEmits(['update:selectedDate'])
const emit = defineEmits(['update:selectedDate', 'currentDate'])
const pageDate = ref<dayjs.Dayjs>(dayjs())
@ -93,10 +94,24 @@ onMounted(() => {
</div>
</div>
<div v-else></div>
<div class="px-2 py-1 box-border flex items-center justify-center">
<div class="px-2 py-1 box-border flex items-center justify-center gap-2">
<NcButton :tabindex="-1" class="!h-7" size="small" type="secondary" @click="handleSelectTime(dayjs())">
<span class="text-small"> {{ $t('general.now') }} </span>
</NcButton>
<NcTooltip v-if="showCurrentDateOption" :disabled="showCurrentDateOption !== 'disabled'">
<template #title>
{{ $t('tooltip.currentDateNotAvail') }}
</template>
<NcButton
class="nc-date-picker-now-btn !h-7"
size="small"
type="secondary"
:disabled="showCurrentDateOption === 'disabled'"
@click="emit('currentDate')"
>
<span class="text-small"> {{ $t('labels.currentDate') }} </span>
</NcButton>
</NcTooltip>
</div>
</div>
</template>

73
packages/nc-gui/components/nc/Tooltip.vue

@ -35,6 +35,8 @@ const color = computed(() => (props.color ? props.color : 'dark'))
const el = ref()
const element = ref()
const showTooltip = controlledRef(false, {
onBeforeChange: (shouldShow) => {
if (shouldShow && disabled.value) return false
@ -43,6 +45,8 @@ const showTooltip = controlledRef(false, {
const isHovering = useElementHover(() => el.value)
const isOverlayHovering = useElementHover(() => element.value)
const attrs = useAttrs()
const isKeyPressed = ref(false)
@ -74,38 +78,51 @@ onKeyStroke(
{ eventName: 'keyup' },
)
watch([isHovering, () => modifierKey.value, () => disabled.value], ([hovering, key, isDisabled]) => {
if (showOnTruncateOnly?.value) {
const targetElement = el?.value
const isElementTruncated = targetElement && targetElement.scrollWidth > targetElement.clientWidth
if (!isElementTruncated) {
watchDebounced(
[isOverlayHovering, isHovering, () => modifierKey.value, () => disabled.value],
([overlayHovering, hovering, key, isDisabled]) => {
if (showOnTruncateOnly?.value) {
const targetElement = el?.value
const isElementTruncated = targetElement && targetElement.scrollWidth > targetElement.clientWidth
if (!isElementTruncated) {
if (overlayHovering) {
showTooltip.value = true
return
}
showTooltip.value = false
return
}
}
if (overlayHovering) {
showTooltip.value = true
return
}
if ((!hovering || isDisabled) && !props.mouseLeaveDelay) {
showTooltip.value = false
return
}
}
if ((!hovering || isDisabled) && !props.mouseLeaveDelay) {
showTooltip.value = false
return
}
// Show tooltip on mouseover if no modifier key is provided
if (hovering && !key) {
showTooltip.value = true
return
}
// Show tooltip on mouseover if no modifier key is provided
if (hovering && !key) {
showTooltip.value = true
return
}
// While hovering if the modifier key was changed and the key is not pressed, hide tooltip
if (hovering && key && !isKeyPressed.value) {
showTooltip.value = false
return
}
// While hovering if the modifier key was changed and the key is not pressed, hide tooltip
if (hovering && key && !isKeyPressed.value) {
showTooltip.value = false
return
}
// When mouse leaves the element, then re-enters the element while key stays pressed, show the tooltip
if (!showTooltip.value && hovering && key && isKeyPressed.value) {
showTooltip.value = true
}
})
// When mouse leaves the element, then re-enters the element while key stays pressed, show the tooltip
if (!showTooltip.value && hovering && key && isKeyPressed.value) {
showTooltip.value = true
}
},
{
debounce: 100,
},
)
const divStyles = computed(() => ({
style: attrs.style as CSSProperties,
@ -131,7 +148,9 @@ const onClick = () => {
:mouse-leave-delay="mouseLeaveDelay"
>
<template #title>
<slot name="title" />
<div ref="element">
<slot name="title" />
</div>
</template>
<component

81
packages/nc-gui/components/nc/form-builder/SampleModal.vue

@ -0,0 +1,81 @@
<script lang="ts" setup>
const initState = ref({
someDefaultProp: 'value',
})
const { formState, isLoading, submit } = useProvideFormBuilderHelper({
formSchema: [
{
type: FormBuilderInputType.Input,
label: 'Sample Input',
width: 100,
model: 'title',
placeholder: 'Some placeholder',
category: 'General',
required: true,
},
{
type: FormBuilderInputType.Input,
label: 'Input To Nested Path',
width: 50,
model: 'config.sample',
placeholder: 'This is added to config.sample',
category: 'Sample Category',
helpText: 'This is a sample help text',
required: true,
},
{
type: FormBuilderInputType.Space,
width: 50,
category: 'Sample Category',
},
{
type: FormBuilderInputType.Input,
label: 'Multiple Elements in Category',
width: 50,
model: 'config.sample2',
placeholder: 'This is added to config.sample2',
category: 'Sample Category',
required: false,
},
{
type: FormBuilderInputType.Select,
label: 'Sample Select',
width: 100,
model: 'config.select',
category: 'Settings',
options: [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
],
defaultValue: 'option2',
required: true,
},
{
type: FormBuilderInputType.Switch,
label: 'Sample Switch',
width: 100,
model: 'config.switch',
category: 'Misc',
helpText: 'This is a sample switch',
required: false,
border: true,
},
],
onSubmit: async () => {
console.log('submit', formState)
},
initialState: initState,
})
</script>
<template>
<div class="h-full">
<NcFormBuilder />
<div class="mt-10"></div>
<NcButton :loading="isLoading" type="primary" @click="submit">Submit</NcButton>
</div>
</template>
<style lang="scss" scoped></style>

265
packages/nc-gui/components/nc/form-builder/index.vue

@ -0,0 +1,265 @@
<script lang="ts" setup>
const { form, formState, formElementsCategorized, isLoading, validateInfos } = useFormBuilderHelperOrThrow()
const deepReference = (path: string): any => {
return path.split('.').reduce((acc, key) => acc[key], formState.value)
}
const setFormState = (path: string, value: any) => {
// update nested prop in formState
const keys = path.split('.')
const lastKey = keys.pop()
if (!lastKey) return
const target = keys.reduce((acc, key) => {
if (!acc[key]) {
acc[key] = {}
}
return acc[key]
}, formState.value)
target[lastKey] = value
}
</script>
<template>
<div class="nc-form-builder nc-scrollbar-thin relative">
<a-form ref="form" :model="formState" hide-required-mark layout="vertical" class="flex flex-col gap-4">
<template v-for="category in Object.keys(formElementsCategorized)" :key="category">
<div class="nc-form-section">
<div v-if="category !== FORM_BUILDER_NON_CATEGORIZED" class="nc-form-section-title">{{ category }}</div>
<div class="nc-form-section-body">
<div class="flex flex-wrap">
<template v-for="field in formElementsCategorized[category]" :key="field.model">
<div
v-if="field.type === FormBuilderInputType.Space"
:style="`width:${+field.width || 100}%`"
class="w-full"
></div>
<a-form-item
v-else
v-bind="validateInfos[field.model]"
class="nc-form-item"
:style="`width:${+field.width || 100}%`"
:required="false"
:data-testid="`nc-form-input-${field.model}`"
>
<template v-if="![FormBuilderInputType.Switch].includes(field.type)" #label>
<div class="flex items-center gap-1">
<span>{{ field.label }}</span>
<span v-if="field.required" class="text-red-500">*</span>
<NcTooltip v-if="field.helpText && field.showHintAsTooltip">
<template #title>
<div class="text-xs">
{{ field.helpText }}
</div>
</template>
<GeneralIcon icon="info" class="text-gray-500 h-4" />
</NcTooltip>
</div>
</template>
<template v-if="field.type === FormBuilderInputType.Input">
<a-input
autocomplete="off"
class="!w-full"
:value="deepReference(field.model)"
@update:value="setFormState(field.model, $event)"
/>
</template>
<template v-else-if="field.type === FormBuilderInputType.Password">
<a-input-password
readonly
onfocus="this.removeAttribute('readonly');"
onblur="this.setAttribute('readonly', true);"
autocomplete="off"
:value="deepReference(field.model)"
@update:value="setFormState(field.model, $event)"
/>
</template>
<template v-else-if="field.type === FormBuilderInputType.Select">
<NcSelect
:value="deepReference(field.model)"
:options="field.options"
@update:value="setFormState(field.model, $event)"
/>
</template>
<template v-else-if="field.type === FormBuilderInputType.Switch">
<div class="flex flex-col p-2" :class="field.border ? 'border-1 rounded-lg shadow' : ''">
<div class="flex items-center">
<NcSwitch :checked="!!deepReference(field.model)" @update:checked="setFormState(field.model, $event)" />
<span class="ml-[6px] font-bold">{{ field.label }}</span>
<NcTooltip v-if="field.helpText">
<template #title>
<div class="text-xs">
{{ field.helpText }}
</div>
</template>
<GeneralIcon icon="info" class="text-gray-500 h-4 ml-1" />
</NcTooltip>
</div>
<div v-if="field.helpText && !field.showHintAsTooltip" class="w-full mt-1 pl-[35px]">
<div class="text-xs text-gray-500">{{ field.helpText }}</div>
</div>
</div>
</template>
<div
v-if="field.helpText && field.type !== FormBuilderInputType.Switch && !field.showHintAsTooltip"
class="w-full mt-1"
>
<div class="text-xs text-gray-500">{{ field.helpText }}</div>
</div>
</a-form-item>
</template>
</div>
</div>
</div>
</template>
</a-form>
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15">
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000">
<a-spin size="large" />
</div>
</general-overlay>
</div>
</template>
<style lang="scss" scoped>
.nc-form-item {
margin-bottom: 12px;
}
:deep(.ant-collapse-header) {
@apply !-mt-4 !p-0 flex items-center !cursor-default children:first:flex;
}
:deep(.ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header .ant-collapse-arrow) {
@apply !right-0;
}
:deep(.ant-collapse-content-box) {
@apply !px-0 !pb-0 !pt-3;
}
:deep(.ant-form-item-explain-error) {
@apply !text-xs;
}
:deep(.ant-divider) {
@apply m-0;
}
:deep(.ant-form-item-with-help .ant-form-item-explain) {
@apply !min-h-0;
}
:deep(.ant-select .ant-select-selector .ant-select-selection-item) {
@apply font-weight-400;
}
.nc-form-builder {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {
@apply !appearance-none border-solid rounded-md;
}
:deep(.ant-input-password) {
input {
@apply !border-none my-0;
}
}
.nc-form-section {
@apply flex flex-col gap-3;
}
.nc-form-section-title {
@apply text-sm font-bold text-gray-800;
}
.nc-form-section-body {
@apply flex flex-col gap-3;
}
:deep(.ant-form-item-label > label.ant-form-item-required:after) {
@apply content-['*'] inline-block text-inherit text-red-500 ml-1;
}
:deep(.ant-form-item) {
&.ant-form-item-has-error {
&:not(:has(.ant-input-password)) .ant-input {
&:not(:hover):not(:focus):not(:disabled) {
@apply shadow-default;
}
&:hover:not(:focus):not(:disabled) {
@apply shadow-hover;
}
&:focus {
@apply shadow-error ring-0;
}
}
.ant-input-number,
.ant-input-affix-wrapper.ant-input-password {
&:not(:hover):not(:focus-within):not(:disabled) {
@apply shadow-default;
}
&:hover:not(:focus-within):not(:disabled) {
@apply shadow-hover;
}
&:focus-within {
@apply shadow-error ring-0;
}
}
}
&:not(.ant-form-item-has-error) {
&:not(:has(.ant-input-password)) .ant-input {
&:not(:hover):not(:focus):not(:disabled) {
@apply shadow-default border-gray-200;
}
&:hover:not(:focus):not(:disabled) {
@apply border-gray-200 shadow-hover;
}
&:focus {
@apply shadow-selected ring-0;
}
}
.ant-input-number,
.ant-input-affix-wrapper.ant-input-password {
&:not(:hover):not(:focus-within):not(:disabled) {
@apply shadow-default border-gray-200;
}
&:hover:not(:focus-within):not(:disabled) {
@apply border-gray-200 shadow-hover;
}
&:focus-within {
@apply shadow-selected ring-0;
}
}
}
}
:deep(.ant-row:not(.ant-form-item)) {
@apply !-mx-1.5;
& > .ant-col {
@apply !px-1.5;
}
}
:deep(.ant-form-item) {
@apply !mb-6;
}
}
</style>
<style lang="scss"></style>

9
packages/nc-gui/components/project/AccessSettings.vue

@ -274,7 +274,7 @@ const isDeleteOrUpdateAllowed = (user) => {
<GeneralIcon icon="ncSlash1" class="nc-breadcrumb-divider" />
<NuxtLink
:href="`/admin/${orgId}/bases`"
class="!hover:(text-gray-800 underline-gray-600) flex items-center !text-gray-700 !underline-transparent ml-0.75 max-w-1/4"
class="!hover:(text-gray-800 underline-gray-600) flex items-center !text-gray-700 !underline-transparent max-w-1/4"
>
<div class="nc-breadcrumb-item">
{{ $t('objects.projects') }}
@ -299,12 +299,7 @@ const isDeleteOrUpdateAllowed = (user) => {
</NcPageHeader>
</div>
<div
class="h-full flex flex-col items-center gap-6 px-6 pt-6"
:class="{
'border-t-1 border-gray-200': isAdminPanel,
}"
>
<div class="nc-content-max-w h-full flex flex-col items-center gap-6 px-6 pt-6">
<div v-if="!isAdminPanel" class="w-full flex justify-between items-center max-w-350 gap-3">
<a-input
v-model:value="userSearchText"

2
packages/nc-gui/components/project/AllTables.vue

@ -216,7 +216,7 @@ const onCreateBaseClick = () => {
<template #bodyCell="{ column, record }">
<div
v-if="column.key === 'tableName'"
class="w-full flex items-center gap-3 max-w-full text-gray-800 font-semibold"
class="w-full flex items-center gap-3 max-w-full text-gray-800"
data-testid="proj-view-list__item-title"
>
<div class="min-w-6 flex items-center justify-center">

12
packages/nc-gui/components/project/View.vue

@ -112,10 +112,10 @@ watch(
<div class="h-full nc-base-view">
<div
v-if="!isAdminPanel"
class="flex flex-row px-2 py-2 gap-1 justify-between w-full border-b-1 border-gray-200"
class="flex flex-row px-2 py-2 gap-3 justify-between w-full border-b-1 border-gray-200"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
>
<div class="flex flex-row items-center gap-x-3">
<div class="flex-1 flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2 px-2">
<GeneralProjectIcon :color="parseProp(currentBase?.meta).iconColor" :type="currentBase?.type" />
@ -127,10 +127,13 @@ watch(
</NcTooltip>
</div>
</div>
<SmartsheetTopbarCmdK v-if="!isSharedBase" />
<LazyGeneralShareProject />
</div>
<div
class="flex nc-base-view-tab container"
class="flex nc-base-view-tab"
:style="{
height: 'calc(100% - var(--topbar-height))',
}"
@ -217,6 +220,9 @@ watch(
:deep(.ant-tabs-tab) {
@apply pt-2 pb-3;
}
:deep(.ant-tabs-content) {
@apply nc-content-max-w;
}
:deep(.ant-tabs-tab .tab-title) {
@apply text-gray-500;
}

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

@ -19,6 +19,8 @@ const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editE
const column = toRef(props, 'column')
const meta = inject(MetaInj, ref())
const active = toRef(props, 'active', false)
const readOnly = toRef(props, 'readOnly', false)
@ -51,11 +53,9 @@ const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useBase())
const sqlUi = ref(
column.value?.source_id && sqlUis.value[column.value?.source_id]
? sqlUis.value[column.value?.source_id]
: Object.values(sqlUis.value)[0],
)
const sourceId = meta.value?.source_id || column.value?.source_id
const sqlUi = ref(sourceId && sqlUis.value[sourceId] ? sqlUis.value[sourceId] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))
@ -123,6 +123,17 @@ const onContextmenu = (e: MouseEvent) => {
e.stopPropagation()
}
}
const showCurrentDateOption = computed(() => {
if (!isEditColumnMenu.value || (!isDate(column.value, abstractType.value) && !isDateTime(column.value, abstractType.value)))
return false
return sqlUi.value?.getCurrentDateDefault?.(column.value) ? true : 'disabled'
})
const currentDate = () => {
vModel.value = sqlUi.value?.getCurrentDateDefault?.(column.value)
}
</script>
<template>
@ -166,13 +177,21 @@ const onContextmenu = (e: MouseEvent) => {
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDatePicker
v-else-if="isDate(column, abstractType)"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker
v-else-if="isDateTime(column, abstractType)"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="isRating(column)" v-model="vModel" />

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

@ -103,11 +103,12 @@ watch(openedSubTab, () => {
@apply flex flex-row items-center gap-x-1.5 pr-0.5;
}
:deep(.ant-tabs-nav) {
:deep(.nc-details-tab > .ant-tabs-nav:first-of-type) {
min-height: calc(var(--toolbar-height) - 1px);
}
:deep(.ant-tabs-tab) {
@apply pt-2 pb-3;
.ant-tabs-tab {
@apply pt-3 pb-3 text-small leading-[18px];
}
}
</style>

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

@ -6,6 +6,8 @@ import 'splitpanes/dist/splitpanes.css'
import {
type AttachmentResType,
type ColumnType,
type LinkToAnotherRecordType,
ProjectRoles,
RelationTypes,
UITypes,
@ -14,6 +16,7 @@ import {
isLinksOrLTAR,
isVirtualCol,
} from 'nocodb-sdk'
import type { ValidateInfo } from 'ant-design-vue/es/form/useForm'
import type { ImageCropperConfig } from '~/lib/types'
provide(IsFormInj, ref(true))
@ -55,6 +58,8 @@ const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { metas, getMeta } = useMetas()
const { base } = storeToRefs(useBase())
const { getPossibleAttachmentSrc } = useAttachment()
@ -87,6 +92,7 @@ const {
validate,
clearValidate,
fieldMappings,
isValidRedirectUrl,
} = useProvideFormViewStore(meta, view, formViewData, updateFormView, isEditable)
const { preFillFormSearchParams } = storeToRefs(useViewsStore())
@ -94,7 +100,7 @@ const { preFillFormSearchParams } = storeToRefs(useViewsStore())
const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook())
reloadEventHook.on(async () => {
await loadFormView()
await Promise.all([loadFormView(), loadReleatedMetas()])
setFormData()
})
@ -184,6 +190,88 @@ const onVisibilityChange = (state: 'showAddColumn' | 'showEditColumn') => {
const getFormLogoSrc = computed(() => getPossibleAttachmentSrc(parseProp(formViewData.value?.logo_url)))
const isOpenRedirectUrlOption = ref(false)
const redirectLinkValidation = ref<ValidateInfo>({
validateStatus: '',
help: undefined,
})
const isOpenRedirectUrl = computed({
get: () => {
return typeof formViewData.value?.redirect_url === 'string'
},
set: (value: boolean) => {
isOpenRedirectUrlOption.value = value
if (value) {
formViewData.value = {
...formViewData.value,
redirect_url: '',
}
} else {
formViewData.value = {
...formViewData.value,
redirect_url: null,
}
redirectLinkValidation.value = {
validateStatus: '',
help: undefined,
}
}
updateView()
},
})
const handleUpdateRedirectUrl = () => {
const validStatus = isValidRedirectUrl()
redirectLinkValidation.value = {
...validStatus,
}
if (validStatus.validateStatus === 'error') {
return
}
updateView()
}
const getPrefillValue = (c: ColumnType, value: any) => {
let preFillValue: any
switch (c.uidt) {
case UITypes.LinkToAnotherRecord:
case UITypes.Links: {
const values = Array.isArray(value) ? value : [value]
const fk_related_model_id = (c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id
if (!fk_related_model_id) return
const rowIds = values
.map((row) => {
return extractPkFromRow(row, metas.value[fk_related_model_id].columns || [])
})
.filter((rowId) => !!rowId)
.join(',')
preFillValue = rowIds || undefined
// if bt/oo then extract object from array
if (c.colOptions?.type === RelationTypes.BELONGS_TO || c.colOptions?.type === RelationTypes.ONE_TO_ONE) {
preFillValue = rowIds[0]
}
break
}
default: {
return value
}
}
return preFillValue
}
const updatePreFillFormSearchParams = useDebounceFn(() => {
if (isLocked.value || !isUIAllowed('dataInsert')) return
@ -192,8 +280,20 @@ const updatePreFillFormSearchParams = useDebounceFn(() => {
const searchParams = new URLSearchParams()
for (const c of visibleColumns.value) {
if (c.title && preFilledData[c.title] && !isVirtualCol(c) && !(UITypes.Attachment === c.uidt)) {
searchParams.append(c.title, preFilledData[c.title])
if (
!c.title ||
!isValidValue(preFilledData[c.title]) ||
(isVirtualCol(c) && !isLinksOrLTAR(c)) ||
isAttachment(c) ||
c.uidt === UITypes.SpecificDBType
) {
continue
}
const preFillValue = getPrefillValue(c, preFilledData[c.title])
if (preFillValue !== undefined) {
searchParams.append(c.title, preFillValue)
}
}
@ -562,6 +662,31 @@ const updateFieldTitle = (value: string) => {
}
}
const handleAutoScrollFormField = (title: string, isSidebar: boolean) => {
const field = document.querySelector(
`${isSidebar ? '.nc-form-field-item-' : '.nc-form-drag-'}${CSS.escape(title?.replaceAll(' ', ''))}`,
)
if (field) {
setTimeout(() => {
field?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 50)
}
}
async function loadReleatedMetas() {
await Promise.all(
(localColumns.value || []).map(async (c: ColumnType) => {
const fk_related_model_id = (c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id
if (isVirtualCol(c) && isLinksOrLTAR(c) && fk_related_model_id) {
await getMeta(fk_related_model_id)
}
return c
}),
)
}
onMounted(async () => {
if (imageCropperData.value.src) {
URL.revokeObjectURL(imageCropperData.value.imageConfig.src)
@ -570,7 +695,9 @@ onMounted(async () => {
preFillFormSearchParams.value = ''
isLoadingFormView.value = true
await loadFormView()
await Promise.all([loadFormView(), loadReleatedMetas()])
setFormData()
isLoadingFormView.value = false
})
@ -600,7 +727,6 @@ watch(
for (const virtualField in state.value) {
formState.value[virtualField] = state.value[virtualField]
}
updatePreFillFormSearchParams()
try {
@ -616,18 +742,6 @@ watch(
},
)
const handleAutoScrollFormField = (title: string, isSidebar: boolean) => {
const field = document.querySelector(
`${isSidebar ? '.nc-form-field-item-' : '.nc-form-drag-'}${CSS.escape(title?.replaceAll(' ', ''))}`,
)
if (field) {
setTimeout(() => {
field?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 50)
}
}
watch(activeField, (newValue, oldValue) => {
if (newValue && autoScrollFormField.value) {
nextTick(() => {
@ -1289,7 +1403,7 @@ useEventListener(
</div>
<!-- Field text -->
<div class="nc-form-field-text p-4 flex flex-col gap-4 border-b border-gray-200">
<div class="text-base font-bold text-gray-600">
<div class="text-sm font-bold text-gray-800">
{{ $t('objects.field') }} {{ $t('general.text').toLowerCase() }}
</div>
@ -1326,8 +1440,8 @@ useEventListener(
<Splitpanes v-if="formViewData" horizontal class="nc-form-settings w-full nc-form-right-splitpane">
<Pane min-size="30" size="50" class="nc-form-right-splitpane-item p-4 flex flex-col space-y-4 !min-h-200px">
<div class="flex flex-wrap justify-between items-center gap-2">
<div class="flex gap-3">
<div class="text-base font-bold text-gray-600">
<div class="flex items-center gap-3">
<div class="text-sm font-bold text-gray-800">
{{ $t('objects.viewType.form') }} {{ $t('objects.fields') }}
</div>
<NcBadge color="border-gray-200">
@ -1519,7 +1633,7 @@ useEventListener(
<Pane min-size="20" size="50" class="nc-form-right-splitpane-item !overflow-y-auto nc-form-scrollbar">
<div class="p-4 flex flex-col space-y-4 border-b border-gray-200">
<!-- Appearance Settings -->
<div class="text-base font-bold text-gray-600">{{ $t('labels.appearanceSettings') }}</div>
<div class="text-sm font-bold text-gray-800">{{ $t('labels.appearanceSettings') }}</div>
<div class="flex flex-col space-y-3">
<div :class="isLocked || !isEditable ? 'pointer-events-none' : ''">
@ -1599,44 +1713,84 @@ useEventListener(
<div class="p-4 flex flex-col space-y-4">
<!-- Post Form Submission Settings -->
<div class="text-base font-bold text-gray-600">
<div class="text-sm font-bold text-gray-800">
{{ $t('msg.info.postFormSubmissionSettings') }}
</div>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<!-- Show "Submit Another Form" button -->
<span>{{ $t('msg.info.submitAnotherForm') }}</span>
<a-switch
v-model:checked="formViewData.submit_another_form"
v-e="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
data-testid="nc-form-checkbox-submit-another-form"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<!-- Redirect to URL -->
<span>{{ $t('labels.redirectToUrl') }}</span>
<a-switch
v-model:checked="isOpenRedirectUrl"
v-e="[`a:form-view:redirect-url`]"
size="small"
class="nc-form-checkbox-redirect-url"
data-testid="nc-form-checkbox-redirect-url"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
</div>
<div v-if="isOpenRedirectUrl" class="flex flex-col gap-2 max-w-[calc(100%_-_40px)]">
<a-form-item class="!my-0" v-bind="redirectLinkValidation">
<a-input
v-model:value="formViewData.redirect_url"
type="text"
class="!h-8 !px-3 !py-1 !rounded-lg"
placeholder="Paste redirect URL here"
data-testid="nc-form-redirect-url-input"
@input="handleUpdateRedirectUrl"
></a-input>
</a-form-item>
<div class="text-small leading-[18px] text-gray-400 pl-3">
Use {record_id} to get ID of the newly created record.
<a
href="https://docs.nocodb.com/views/view-types/form/#redirect-url"
target="_blank"
rel="noopener noreferrer"
class="!no-underline !hover:underline"
>
Learn more
</a>
</div>
</div>
</div>
<template v-if="!isOpenRedirectUrl">
<div class="flex items-center justify-between gap-3">
<!-- Show "Submit Another Form" button -->
<span>{{ $t('msg.info.submitAnotherForm') }}</span>
<a-switch
v-model:checked="formViewData.submit_another_form"
v-e="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
data-testid="nc-form-checkbox-submit-another-form"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
</div>
<div class="flex items-center justify-between gap-3">
<!-- Show a blank form after 5 seconds -->
<span>{{ $t('msg.info.showBlankForm') }}</span>
<a-switch
v-model:checked="formViewData.show_blank_form"
v-e="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
data-testid="nc-form-checkbox-show-blank-form"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
</div>
<div class="flex items-center justify-between gap-3">
<!-- Show a blank form after 5 seconds -->
<span>{{ $t('msg.info.showBlankForm') }}</span>
<a-switch
v-model:checked="formViewData.show_blank_form"
v-e="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
data-testid="nc-form-checkbox-show-blank-form"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
</div>
</template>
<div class="flex items-center justify-between gap-3">
<!-- Email me at <email> -->
<span>
{{ $t('msg.info.emailForm') }}
<span class="text-bold text-gray-600">{{ user?.email }}</span>
<span class="text-bold text-gray-600 underline">{{ user?.email }}</span>
</span>
<a-switch
v-model:checked="emailMe"
@ -1651,7 +1805,7 @@ useEventListener(
</div>
<!-- Show this message -->
<div class="pb-10">
<div v-if="!isOpenRedirectUrl" class="pb-10">
<div class="text-gray-800 mb-2">
{{ $t('msg.info.formDisplayMessage') }}
</div>

1
packages/nc-gui/components/smartsheet/Gallery.vue

@ -143,7 +143,6 @@ const openNewRecordFormHookHandler = async () => {
const handleClick = (col, event) => {
if (isButton(col)) {
event.preventDefault()
event.stopPropagation()
}
}

5
packages/nc-gui/components/smartsheet/Kanban.vue

@ -327,7 +327,6 @@ const handleCollapseStack = async (stackIdx: number) => {
const handleCellClick = (col, event) => {
if (isButton(col)) {
event.preventDefault()
event.stopPropagation()
}
}
@ -1017,9 +1016,9 @@ const handleSubmitRenameOrNewStack = async (loadMeta: boolean, stack?: any, stac
</div>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 truncate">
<div
class="nc-kanban-data-count px-1 rounded bg-gray-200 text-gray-800 text-sm font-weight-500"
class="nc-kanban-data-count px-1 rounded bg-gray-200 text-gray-800 text-sm font-weight-500 truncate"
:style="{ 'word-break': 'keep-all', 'white-space': 'nowrap' }"
>
<!-- Record Count -->

52
packages/nc-gui/components/smartsheet/Topbar.vue

@ -53,31 +53,41 @@ const topbarBreadcrumbItemWidth = computed(() => {
<div class="flex items-center justify-end gap-3 flex-1">
<GeneralApiLoader v-if="!isMobileMode" />
<div
v-if="extensionsEgg"
class="flex items-center px-2 py-1 border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 cursor-pointer font-weight-600 text-sm select-none"
:class="{ 'bg-brand-50 text-brand-500': isPanelExpanded }"
<NcButton
v-if="!isSharedBase && extensionsEgg"
v-e="['c:extension-toggle']"
type="secondary"
size="small"
class="nc-topbar-extension-btn"
:class="{ '!bg-brand-50 !hover:bg-brand-100/70 !text-brand-500': isPanelExpanded }"
data-testid="nc-topbar-extension-btn"
@click="toggleExtensionPanel"
>
<GeneralIcon icon="puzzle" class="w-4 h-4" :class="{ 'border-l-1 border-transparent': isPanelExpanded }" />
<span
class="overflow-hidden trasition-all duration-200"
:class="{ 'w-[0px] invisible': isPanelExpanded, 'ml-2 w-[74px]': !isPanelExpanded }"
>
Extensions
</span>
<div class="flex items-center justify-center min-w-[28.69px]">
<GeneralIcon
icon="ncPuzzleOutline"
class="w-4 h-4 !stroke-transparent"
:class="{ 'border-l-1 border-transparent': isPanelExpanded }"
/>
<span
class="overflow-hidden trasition-all duration-200"
:class="{ 'w-[0px] invisible': isPanelExpanded, 'ml-1 w-[74px]': !isPanelExpanded }"
>
Extensions
</span>
</div>
</NcButton>
<div v-else-if="!isSharedBase && !extensionsEgg" class="w-[15px] h-[15px] cursor-pointer" @dblclick="onEggClick" />
<div v-if="!isSharedBase">
<LazySmartsheetTopbarCmdK />
</div>
<div v-if="(isForm || isGrid || isKanban || isGallery || isMap || isCalendar) && !isPublic && !isMobileMode">
<LazyGeneralShareProject is-view-toolbar />
</div>
<div v-else-if="!extensionsEgg" class="w-[15px] h-[15px] cursor-pointer" @dblclick="onEggClick" />
<LazyGeneralShareProject
v-if="(isForm || isGrid || isKanban || isGallery || isMap || isCalendar) && !isPublic && !isMobileMode"
is-view-toolbar
/>
<LazyGeneralLanguage
v-if="isSharedBase && !appInfo.ee"
class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md"
/>
<div v-if="isSharedBase && !appInfo.ee">
<LazyGeneralLanguage class="cursor-pointer text-lg hover:(text-black bg-gray-200) mr-0 p-1.5 rounded-md" />
</div>
</div>
</template>
</div>

5
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -15,6 +15,7 @@ const {
displayField,
sideBarFilterOption,
showSideMenu,
updateFormat,
} = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
@ -515,7 +516,7 @@ const calculateNewRow = (event: MouseEvent, skipChangeCheck?: boolean) => {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
@ -538,7 +539,7 @@ const calculateNewRow = (event: MouseEvent, skipChangeCheck?: boolean) => {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol.title!] = dayjs(endDate).format(updateFormat.value)
updateProperty.push(toCol.title!)
}

17
packages/nc-gui/components/smartsheet/calendar/MonthView.vue

@ -17,6 +17,7 @@ const {
viewMetaProperties,
showSideMenu,
updateRowProperty,
updateFormat,
} = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
@ -400,10 +401,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeC
...dragRecord.value,
row: {
...dragRecord.value?.row,
[fromCol!.title!]:
calDataType.value === UITypes.Date
? dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ')
: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol!.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
@ -423,10 +421,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeC
endDate = newStartDate.clone()
}
newRow.row[toCol!.title!] =
calDataType.value === UITypes.Date
? dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
: dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol!.title!] = dayjs(endDate).format(updateFormat.value)
updateProperty.push(toCol!.title!)
}
@ -501,7 +496,7 @@ const onResize = (event: MouseEvent) => {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol!.title!]: dayjs(newEndDate).format('YYYY-MM-DD HH:mm:ssZ'),
[toCol!.title!]: dayjs(newEndDate).format(updateFormat.value),
},
}
} else {
@ -517,7 +512,7 @@ const onResize = (event: MouseEvent) => {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol!.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
}
@ -689,7 +684,7 @@ const addRecord = (date: dayjs.Dayjs) => {
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: date.format(updateFormat.value),
},
}
emit('newRecord', newRecord)

9
packages/nc-gui/components/smartsheet/calendar/SideMenu.vue

@ -49,6 +49,7 @@ const {
searchQuery,
sideBarFilterOption,
showSideMenu,
updateFormat,
} = useCalendarViewStoreOrThrow()
const sideBarListRef = ref<VNodeRef | null>(null)
@ -287,13 +288,13 @@ const newRecord = () => {
}
if (activeCalendarView.value === 'day') {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ')
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format(updateFormat.value)
} else if (activeCalendarView.value === 'week') {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDateRange.value.start.format('YYYY-MM-DD HH:mm:ssZ')
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDateRange.value.start.format(updateFormat.value)
} else if (activeCalendarView.value === 'month') {
row[calendarRange.value[0]!.fk_from_col!.title!] = (selectedDate.value ?? selectedMonth.value).format('YYYY-MM-DD HH:mm:ssZ')
row[calendarRange.value[0]!.fk_from_col!.title!] = (selectedDate.value ?? selectedMonth.value).format(updateFormat.value)
} else if (activeCalendarView.value === 'year') {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ')
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format(updateFormat.value)
}
emit('newRecord', { row, oldRow: {}, rowMeta: { new: true } })

13
packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue

@ -14,6 +14,7 @@ const {
displayField,
updateRowProperty,
viewMetaProperties,
updateFormat,
} = useCalendarViewStoreOrThrow()
const maxVisibleDays = computed(() => {
@ -22,6 +23,8 @@ const maxVisibleDays = computed(() => {
const container = ref<null | HTMLElement>(null)
const { $e } = useNuxtApp()
const { width: containerWidth } = useElementSize(container)
const { isUIAllowed } = useRoles()
@ -322,7 +325,7 @@ const onResize = (event: MouseEvent) => {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'),
[toCol.title!]: newEndDate.format(updateFormat.value),
},
}
} else if (resizeDirection.value === 'left') {
@ -340,7 +343,7 @@ const onResize = (event: MouseEvent) => {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
}
@ -399,7 +402,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
@ -423,7 +426,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol.title!] = dayjs(endDate).format(updateFormat.value)
updateProperty.push(toCol.title!)
}
@ -559,7 +562,7 @@ const addRecord = (date: dayjs.Dayjs) => {
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: date.format(updateFormat.value),
},
}
emits('newRecord', newRecord)

11
packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue

@ -17,6 +17,7 @@ const {
updateRowProperty,
sideBarFilterOption,
showSideMenu,
updateFormat,
} = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
@ -655,7 +656,7 @@ const onResize = (event: MouseEvent) => {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'),
[toCol.title!]: newEndDate.format(updateFormat.value),
},
}
} else if (resizeDirection.value === 'left') {
@ -672,7 +673,7 @@ const onResize = (event: MouseEvent) => {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
}
@ -744,7 +745,7 @@ const calculateNewRow = (
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).format(updateFormat.value),
},
}
@ -762,7 +763,7 @@ const calculateNewRow = (
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol.title!] = dayjs(endDate).format(updateFormat.value)
updatedProperty.push(toCol.title!)
}
@ -929,7 +930,7 @@ const addRecord = (date: dayjs.Dayjs) => {
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: date.format(updateFormat.value),
},
}
emits('newRecord', newRecord)

55
packages/nc-gui/components/smartsheet/column/DefaultValue.vue

@ -10,6 +10,8 @@ provide(EditColumnInj, ref(true))
const vModel = useVModel(props, 'value', emits)
const meta = inject(MetaInj, ref())
const isVisibleDefaultValueInput = useVModel(props, 'isVisibleDefaultValueInput', emits)
const rowRef = ref({
@ -41,6 +43,21 @@ watch(
cdfValue.value = newValue
},
)
const { sqlUis } = storeToRefs(useBase())
const sqlUi = computed(() =>
meta.value?.source_id && sqlUis.value[meta.value?.source_id]
? sqlUis.value[meta.value?.source_id]
: Object.values(sqlUis.value)[0],
)
const showCurrentDateOption = computed(() => {
return [UITypes.Date, UITypes.DateTime].includes(vModel.value?.uidt) && sqlUi.value?.getCurrentDateDefault?.(vModel.value)
})
const isCurrentDate = computed(() => {
return showCurrentDateOption.value && cdfValue.value?.toUpperCase?.() === sqlUi.value?.getCurrentDateDefault?.(vModel.value)
})
</script>
<template>
@ -48,13 +65,13 @@ watch(
<NcButton
size="small"
type="text"
class="!text-gray-500 !hover:text-gray-700"
class="!text-gray-700"
data-testid="nc-show-default-value-btn"
@click.stop="isVisibleDefaultValueInput = true"
>
<div class="flex items-center gap-2">
<span>{{ $t('general.set') }} {{ $t('placeholder.defaultValue').toLowerCase() }}</span>
<GeneralIcon icon="plus" class="flex-none h-4 w-4" />
<span>{{ $t('general.set') }} {{ $t('placeholder.defaultValue').toLowerCase() }}</span>
</div>
</NcButton>
</div>
@ -63,27 +80,37 @@ watch(
<div class="w-full flex items-center gap-2 mb-2">
<div class="text-small leading-[18px] flex-1 text-gray-700">{{ $t('placeholder.defaultValue') }}</div>
</div>
<div class="flex flex-row gap-2">
<div class="flex flex-row gap-2 relative">
<div
class="nc-default-value-wrapper border-1 flex items-center w-full px-3 border-gray-300 rounded-lg sm:min-h-[32px] xs:min-h-13 flex items-center focus-within:(border-brand-500 shadow-selected ring-0) transition-all duration-0.3s"
>
<LazySmartsheetCell
:edit-enabled="true"
:model-value="cdfValue"
:column="vModel"
class="!border-none h-auto my-auto"
@update:cdf="updateCdfValue"
@update:edit-enabled="editEnabled = $event"
@click="editEnabled = true"
/>
<div class="relative flex-grow">
<div
v-if="isCurrentDate"
class="absolute pointer-events-none h-full w-full bg-white z-2 top-0 left-0 rounded-full items-center flex bg-white"
>
<div class="-ml-2">
<NcBadge>{{ $t('labels.currentDate') }}</NcBadge>
</div>
</div>
<LazySmartsheetCell
:edit-enabled="true"
:model-value="cdfValue"
:column="vModel"
class="!border-none h-auto my-auto"
@update:cdf="updateCdfValue"
@update:edit-enabled="editEnabled = $event"
@click="editEnabled = true"
/>
</div>
<component
:is="iconMap.close"
v-if="
![UITypes.Year, UITypes.Date, UITypes.Time, UITypes.DateTime, UITypes.SingleSelect, UITypes.MultiSelect].includes(
vModel.uidt,
)
) || isCurrentDate
"
class="w-4 h-4 cursor-pointer rounded-full !text-black-500 text-gray-500 cursor-pointer hover:bg-gray-50"
class="w-4 h-4 cursor-pointer rounded-full z-3 !text-black-500 text-gray-500 cursor-pointer hover:bg-gray-50"
@click="updateCdfValue(null)"
/>
</div>

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

@ -25,6 +25,7 @@ const props = defineProps<{
hideType?: boolean
hideAdditionalOptions?: boolean
fromTableExplorer?: boolean
editDescription?: boolean
readonly?: boolean
}>()
@ -43,6 +44,8 @@ const {
column,
} = useColumnCreateStoreOrThrow()
const editDescription = toRef(props, 'editDescription')
const { getMeta } = useMetas()
const { t } = useI18n()
@ -237,7 +240,19 @@ watchEffect(() => {
advancedOptions.value = false
})
const enableDescription = ref(false)
const descInputEl = ref()
const removeDescription = () => {
formState.value.description = ''
enableDescription.value = false
}
onMounted(() => {
if (column.value?.description?.length || editDescription.value) {
enableDescription.value = true
}
if (!isEdit.value) {
generateNewColumnMeta(true)
} else {
@ -286,11 +301,15 @@ onMounted(() => {
}
}
if (isForm.value && !props.fromTableExplorer) {
if (isForm.value && !props.fromTableExplorer && !enableDescription.value) {
setTimeout(() => {
antInput.value?.focus()
antInput.value?.select()
}, 100)
} else if (enableDescription.value) {
setTimeout(() => {
descInputEl.value?.focus()
}, 100)
}
})
})
@ -346,6 +365,17 @@ const filterOption = (input: string, option: { value: UITypes }) => {
)
}
const triggerDescriptionEnable = () => {
if (enableDescription.value) {
enableDescription.value = false
} else {
enableDescription.value = true
setTimeout(() => {
descInputEl.value?.focus()
}, 100)
}
}
const isFullUpdateAllowed = computed(() => {
if (isMetaReadOnly.value && !readonlyMetaAllowedTypes.includes(formState.value?.uidt) && !isVirtualCol(formState.value)) {
return false
@ -585,41 +615,89 @@ const isFullUpdateAllowed = computed(() => {
</Transition>
</template>
<a-form-item v-if="enableDescription">
<div class="flex gap-3 text-gray-800 h-7 mb-1 items-center justify-between">
<span class="text-[13px]">
{{ $t('labels.description') }}
</span>
<NcButton type="text" class="!h-6 !w-5" size="xsmall" @click="removeDescription">
<GeneralIcon icon="delete" class="text-gray-700 w-3.5 h-3.5" />
</NcButton>
</div>
<a-textarea
ref="descInputEl"
v-model:value="formState.description"
:class="{
'!min-h-[200px]': fromTableExplorer,
'h-[150px] !min-h-[100px]': !fromTableExplorer,
}"
class="nc-input-sm nc-input-text-area nc-input-shadow !text-gray-800 px-3 !max-h-[300px]"
hide-details
data-testid="create-field-description-input"
:placeholder="$t('msg.info.enterFieldDescription')"
/>
</a-form-item>
<template v-if="props.fromTableExplorer">
<a-form-item></a-form-item>
<a-form-item>
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="triggerDescriptionEnable">
<div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" />
<span class="first-letter:capitalize">
{{ $t('labels.addDescription').toLowerCase() }}
</span>
</div>
</NcButton>
</a-form-item>
</template>
<template v-else>
<a-form-item>
<div
class="flex gap-x-2 justify-end"
:class="{
'justify-end': !props.embedMode,
}"
>
<!-- Cancel -->
<NcButton size="small" html-type="button" type="secondary" @click="emit('cancel')">
{{ $t('general.cancel') }}
</NcButton>
<!-- Save -->
<NcButton
html-type="submit"
type="primary"
:loading="saving"
:disabled="!formState.uidt || disableSubmitBtn"
size="small"
:label="submitBtnLabel.label"
:loading-label="submitBtnLabel.loadingLabel"
data-testid="nc-field-modal-submit-btn"
@click.prevent="onSubmit"
<div class="flex items-center justify-between gap-2">
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="triggerDescriptionEnable">
<div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" />
<span class="first-letter:capitalize">
{{ $t('labels.addDescription').toLowerCase() }}
</span>
</div>
</NcButton>
<div v-else></div>
<a-form-item>
<div
class="flex gap-x-2 justify-end"
:class="{
'justify-end': !props.embedMode,
}"
>
{{ submitBtnLabel.label }}
<template #loading>
{{ submitBtnLabel.loadingLabel }}
</template>
</NcButton>
</div>
</a-form-item>
<!-- Cancel -->
<NcButton size="small" html-type="button" type="secondary" @click="emit('cancel')">
{{ $t('general.cancel') }}
</NcButton>
<!-- Save -->
<NcButton
html-type="submit"
type="primary"
:loading="saving"
:disabled="!formState.uidt || disableSubmitBtn"
size="small"
:label="submitBtnLabel.label"
:loading-label="submitBtnLabel.loadingLabel"
data-testid="nc-field-modal-submit-btn"
@click.prevent="onSubmit"
>
{{ submitBtnLabel.label }}
<template #loading>
{{ submitBtnLabel.loadingLabel }}
</template>
</NcButton>
</div>
</a-form-item>
</div>
</template>
</template>
</a-form>
@ -635,6 +713,11 @@ const isFullUpdateAllowed = computed(() => {
</style>
<style lang="scss" scoped>
.nc-input-text-area {
@apply !text-gray-800;
padding-block: 8px !important;
}
.nc-fields-input {
&::placeholder {
@apply font-normal;

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

@ -7,6 +7,7 @@ interface Props {
columnPosition?: Pick<ColumnReqType, 'column_order'>
preload?: Partial<ColumnType>
tableExplorerColumns?: ColumnType[]
editDescription?: boolean
fromTableExplorer?: boolean
isColumnValid?: (value: Partial<ColumnType>) => boolean
}
@ -17,7 +18,7 @@ const emit = defineEmits(['submit', 'cancel', 'mounted'])
const meta = inject(MetaInj, ref())
const { column, preload, tableExplorerColumns, fromTableExplorer, isColumnValid } = toRefs(props)
const { column, preload, tableExplorerColumns, fromTableExplorer, isColumnValid, editDescription } = toRefs(props)
useProvideColumnCreateStore(meta, column, tableExplorerColumns, fromTableExplorer, isColumnValid)
@ -36,6 +37,7 @@ defineExpose({
<SmartsheetColumnEditOrAdd
:preload="preload"
:column-position="props.columnPosition"
:edit-description="editDescription"
:from-table-explorer="props.fromTableExplorer || false"
@submit="emit('submit')"
@cancel="emit('cancel')"

3
packages/nc-gui/components/smartsheet/column/FormulaInputHelper.vue

@ -170,7 +170,8 @@ onMounted(async () => {
languages.setMonarchTokensProvider(
formulaLanguage.name,
formulaLanguage.generateLanguageDefinition(supportedColumns.value.map((c) => c.title!)),
// replace @ with \x01 to avoid conflict with monaco's tokenizer
formulaLanguage.generateLanguageDefinition(supportedColumns.value.map((c) => c.title!.replace(/@/g, '\x01'))),
)
languages.setLanguageConfiguration(formulaLanguage.name, formulaLanguage.languageConfiguration)

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

@ -28,13 +28,13 @@ const cdfValue = computed({
<NcButton
size="small"
type="text"
class="!text-gray-500 !hover:text-gray-700"
class="text-gray-700"
data-testid="nc-show-default-value-btn"
@click.stop="isVisibleDefaultValueInput = true"
>
<div class="flex items-center gap-2">
<span>{{ $t('general.set') }} {{ $t('placeholder.defaultValue').toLowerCase() }}</span>
<GeneralIcon icon="plus" class="flex-none h-4 w-4" />
<span>{{ $t('general.set') }} {{ $t('placeholder.defaultValue').toLowerCase() }}</span>
</div>
</NcButton>
</div>

336
packages/nc-gui/components/smartsheet/details/Api.vue

@ -26,38 +26,49 @@ const indicator = h(LoadingOutlined, {
const { copy } = useCopy()
const isCopied = ref(false)
const langs = [
{
name: 'shell',
clients: ['curl', 'wget'],
icon: iconMap.langShell,
},
{
name: 'javascript',
clients: ['axios', 'fetch', 'jquery', 'xhr'],
icon: iconMap.langJavascript,
},
{
name: 'node',
clients: ['axios', 'fetch', 'request', 'native', 'unirest'],
icon: iconMap.langNode,
},
{
name: 'nocodb-sdk',
name: 'NocoDB-SDK',
clients: ['javascript', 'node'],
icon: iconMap.langNocodbSdk,
},
{
name: 'php',
icon: iconMap.langPhp,
},
{
name: 'python',
clients: ['python3', 'requests'],
icon: iconMap.langPython,
},
{
name: 'ruby',
icon: iconMap.langRuby,
},
{
name: 'java',
icon: iconMap.langJava,
},
{
name: 'c',
icon: iconMap.langC,
},
]
@ -96,31 +107,31 @@ const snippet = computed(
const activeLang = computed(() => langs.find((lang) => lang.name === selectedLangName.value))
const code = computed(() => {
if (activeLang.value?.name === 'nocodb-sdk') {
if (activeLang.value?.name === 'NocoDB-SDK') {
return `${selectedClient.value === 'node' ? 'const { Api } = require("nocodb-sdk");' : 'import { Api } from "nocodb-sdk";'}
const api = new Api({
baseURL: "${(appInfo.value && appInfo.value.ncSiteUrl) || '/'}",
headers: {
"xc-token": "CREATE_YOUR_API_TOKEN_FROM ${location.origin + location.pathname}#/account/tokens"
}
baseURL: "${(appInfo.value && appInfo.value.ncSiteUrl) || '/'}",
headers: {
"xc-token": "CREATE_YOUR_API_TOKEN_FROM ${location.origin + location.pathname}#/account/tokens"
}
})
api.dbViewRow.list(
"noco",
${JSON.stringify(base.value?.id)},
${JSON.stringify(meta.value?.id)},
${JSON.stringify(view.value?.id)}, ${JSON.stringify(queryParams.value, null, 4)}).then(function (data) {
console.log(data);
"noco",
${JSON.stringify(base.value?.id)},
${JSON.stringify(meta.value?.id)},
${JSON.stringify(view.value?.id)}, ${JSON.stringify(queryParams.value, null, 4)}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.error(error);
});
`
console.error(error);
});`
}
return snippet.value.convert(
activeLang.value?.name,
selectedClient.value || (activeLang.value?.clients && activeLang.value?.clients[0]),
{},
{ indent: '\t' },
)
})
@ -129,6 +140,12 @@ const onCopyToClipboard = async () => {
await copy(code.value)
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
isCopied.value = true
setTimeout(() => {
isCopied.value = false
}, 5000)
} catch (e: any) {
message.error(e.message)
}
@ -137,79 +154,242 @@ const onCopyToClipboard = async () => {
watch(activeLang, (newLang) => {
selectedClient.value = newLang?.clients?.[0]
})
const supportedDocs = [
{
title: 'Data APIs',
href: 'https://data-apis-v2.nocodb.com/',
},
{
title: 'Meta APIs',
href: 'https://meta-apis-v2.nocodb.com/',
},
{
title: 'Create API Token',
href: 'https://docs.nocodb.com/account-settings/api-tokens/#create-api-token',
},
{
title: 'Swagger',
href: 'https://docs.nocodb.com/bases/actions-on-base/#rest-apis',
},
] as {
title: string
href: string
}[]
</script>
<template>
<div class="flex flex-col w-full h-full px-6 mt-1">
<NcTabs v-model:activeKey="selectedLangName" class="!h-full">
<a-tab-pane v-for="item in langs" :key="item.name" class="!h-full">
<template #tab>
<div class="text-sm capitalize select-none">
<div
class="p-6"
:style="{
height: 'calc(100vh - var(--topbar-height) - var(--toolbar-height) - 16px)',
maxHeight: 'calc(100vh - var(--topbar-height) - var(--toolbar-height) - 16px)',
}"
>
<div class="flex gap-4 max-w-[1000px] mx-auto h-full">
<NcMenu class="nc-api-snippets-menu !h-full w-[252px] min-w-[252px] nc-scrollbar-thin !pr-3">
<div
class="p-2 text-xs text-gray-500 uppercase font-semibold"
:style="{
letterSpacing: '0.3px',
}"
>
{{ $t('general.languages') }}
</div>
<NcMenuItem
v-for="item in langs"
:key="item.name"
class="rounded-md capitalize select-none"
:class="{
'active-menu': selectedLangName === item.name,
}"
@click="selectedLangName = item.name"
>
<div class="flex gap-2 items-center">
<component :is="item.icon" class="!stroke-transparent h-5 w-5" />
{{ item.name }}
</div>
</template>
<div class="flex flex-row w-full space-x-3 my-4 items-center">
<NcSelect
v-if="activeLang?.clients"
v-model:value="selectedClient"
style="width: 10rem"
class="capitalize"
dropdown-class-name="nc-dropdown-snippet-active-lang"
>
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full capitalize" :value="client">
<div class="flex items-center w-full justify-between w-full gap-2">
<div class="truncate flex-1">{{ client }}</div>
<component
:is="iconMap.check"
v-if="selectedClient === client"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
<NcButton
v-e="[
'c:snippet:copy',
{ client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name },
]"
type="secondary"
size="small"
@click="onCopyToClipboard"
</NcMenuItem>
<NcDivider class="!my-3" />
<div class="flex flex-col gap-1">
<div
class="p-2 text-xs text-gray-500 uppercase font-semibold"
:style="{
letterSpacing: '0.3px',
}"
>
<div class="flex items-center">
<GeneralIcon icon="copy" class="mr-1" />
{{ $t('general.copy') }} code
</div>
</NcButton>
{{ $t('labels.documentation') }}
</div>
<div v-for="(doc, idx) of supportedDocs" :key="idx" class="flex items-center gap-2 px-2 h-7">
<GeneralIcon icon="bookOpen" class="flex-none w-4 h-4 text-gray-600" />
<a
:href="doc.href"
target="_blank"
rel="noopener noreferrer"
class="!text-gray-700 text-small leading-[18px] !no-underline !hover:underline"
>
{{ doc.title }}
</a>
</div>
</div>
</NcMenu>
<div class="w-[calc(100%_-_264px)] flex flex-col gap-6 h-full max-h-full">
<div class="nc-api-clents-tab-wrapper h-[calc(100%_-_56px)] flex flex-col mt-2">
<NcTabs v-model:activeKey="selectedClient" class="nc-api-clents-tab">
<template #rightExtra>
<NcButton
v-e="[
'c:snippet:copy',
{ client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name },
]"
type="text"
size="small"
class="!hover:bg-gray-200"
@click="onCopyToClipboard"
>
<div class="flex items-center gap-2 text-small leading-[18px] min-w-80px justify-center">
<GeneralIcon
:icon="isCopied ? 'circleCheck' : 'copy'"
class="h-4 w-4"
:class="{
'text-gray-700': !isCopied,
'text-green-700': isCopied,
}"
/>
{{ isCopied ? $t('general.copied') : $t('general.copy') }}
</div>
</NcButton>
</template>
<Suspense>
<MonacoEditor
class="border-1 border-gray-200 py-4 rounded-lg"
style="height: calc(100vh - (var(--topbar-height) * 2) - 8rem)"
:model-value="code"
:read-only="true"
lang="typescript"
:validate="false"
:disable-deep-compare="true"
hide-minimap
/>
<template #fallback>
<div class="h-full w-full flex flex-col justify-center items-center mt-28">
<a-spin size="large" :indicator="indicator" />
</div>
</template>
</Suspense>
</a-tab-pane>
</NcTabs>
<a-tab-pane v-for="client in activeLang?.clients || ['default']" :key="client" class="!h-full">
<template #tab>
<div class="text-small leading-[18px] capitalize select-none">
{{ client }}
</div>
</template>
<div></div>
</a-tab-pane>
</NcTabs>
<Suspense>
<MonacoEditor
class="h-[calc(100%_-_36px)] !bg-gray-50 pl-2"
:model-value="code"
:read-only="true"
lang="typescript"
:validate="false"
:disable-deep-compare="true"
:monaco-config="{
minimap: {
enabled: false,
},
fontSize: 13,
lineHeight: 18,
padding: {
top: 12,
bottom: 12,
},
overviewRulerBorder: false,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
lineDecorationsWidth: 12,
lineNumbersMinChars: 0,
roundedSelection: false,
selectOnLineNumbers: false,
scrollBeyondLastLine: false,
contextmenu: false,
glyphMargin: false,
folding: false,
bracketPairColorization: { enabled: false },
wordWrap: 'on',
scrollbar: {
horizontal: 'hidden',
verticalScrollbarSize: 6,
},
wrappingStrategy: 'advanced',
renderLineHighlight: 'none',
tabSize: 4,
detectIndentation: false,
insertSpaces: true,
lineNumbers: 'off',
}"
hide-minimap
/>
<template #fallback>
<div class="h-full w-full flex flex-col justify-center items-center mt-28">
<a-spin size="large" :indicator="indicator" />
</div>
</template>
</Suspense>
</div>
</div>
</div>
</div>
</template>
<style scoped>
:deep(.ant-tabs-tab + .ant-tabs-tab) {
@apply !ml-7;
<style lang="scss" scoped>
.nc-api-snippets-menu {
@apply border-r-0 !py-0;
:deep(.ant-menu-item) {
@apply h-7 leading-5 my-1.5 px-2 text-gray-700 flex items-center;
.nc-menu-item-inner {
@apply text-small leading-[18px] text-current font-weight-500;
}
&:hover:not(.active-menu) {
@apply !bg-gray-100;
}
&.active-menu {
@apply bg-brand-50;
.nc-menu-item-inner {
@apply text-brand-600 font-semibold;
}
}
}
}
:deep(.nc-api-clents-tab.ant-tabs) {
.ant-tabs-nav {
@apply px-3;
.ant-tabs-tab {
@apply px-3 pt-2 pb-2.5;
& + .ant-tabs-tab {
@apply !ml-2;
}
&.ant-tabs-tab-active {
@apply font-semibold;
}
}
}
.ant-tabs-content {
@apply h-full;
}
}
</style>
<style lang="scss">
.nc-api-clents-tab-wrapper {
@apply bg-gray-50 border-1 border-gray-200 rounded-lg flex-1;
.monaco-editor {
@apply !border-0 !rounded-none pr-3;
}
.overflow-guard {
@apply !border-0 !rounded-none;
}
.monaco-editor,
.monaco-diff-editor,
.monaco-component {
--vscode-editor-background: #f9f9fa;
--vscode-editorGutter-background: #f9f9fa;
}
}
</style>

32
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -1203,15 +1203,19 @@ watch(
<template #title>{{ $t('msg.clickToCopyFieldId') }}</template>
<div
class="flex flex-row px-3 py-2 w-46 justify-between items-center group hover:bg-gray-100 cursor-pointer"
class="flex flex-row gap-2 w-[calc(100%_-_12px)] p-2 mx-1.5 rounded-md justify-between items-center group hover:bg-gray-100 cursor-pointer"
data-testid="nc-field-item-action-copy-id"
@click="onClickCopyFieldUrl(field)"
>
<div class="flex flex-row items-baseline gap-x-1 font-bold text-xs">
<div class="text-gray-600">{{ $t('labels.idColon') }}</div>
<div class="flex flex-row text-gray-600 text-xs" data-testid="nc-field-item-id">
{{ field.id }}
</div>
<div
class="flex flex-row text-gray-500 text-xs items-baseline gap-x-1 font-bold"
data-testid="nc-field-item-id"
>
{{
$t('labels.idColon', {
fieldId: field.id,
})
}}
</div>
<NcButton size="xsmall" type="secondary" class="!group-hover:bg-gray-100">
<GeneralIcon v-if="isFieldIdCopied" icon="check" />
@ -1384,15 +1388,19 @@ watch(
<template #title>{{ $t('msg.clickToCopyFieldId') }}</template>
<div
class="flex flex-row px-3 py-2 w-46 justify-between items-center group hover:bg-gray-100 cursor-pointer"
class="flex flex-row gap-2 w-[calc(100%_-_12px)] p-2 mx-1.5 rounded-md justify-between items-center group hover:bg-gray-100 cursor-pointer"
data-testid="nc-field-item-action-copy-id"
@click="onClickCopyFieldUrl(displayColumn)"
>
<div class="flex flex-row items-baseline gap-x-1 font-bold text-xs">
<div class="text-gray-600">{{ $t('labels.idColon') }}</div>
<div class="flex flex-row text-gray-600 text-xs">
{{ displayColumn.id }}
</div>
<div
class="flex flex-row text-gray-500 text-xs items-baseline gap-x-1 font-bold"
data-testid="nc-field-item-id"
>
{{
$t('labels.idColon', {
fieldId: displayColumn.id,
})
}}
</div>
<NcButton size="xsmall" type="secondary" class="!group-hover:bg-gray-100">
<GeneralIcon v-if="isFieldIdCopied" icon="check" />

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

@ -775,7 +775,7 @@ export default {
>
<div
v-for="(col, i) of fields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
v-show="!isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
:class="`nc-expand-col-${col.title}`"
:col-id="col.id"

4
packages/nc-gui/components/smartsheet/form/field-settings.vue

@ -21,7 +21,7 @@ const columnSupportsScanning = (elementType: UITypes) =>
<!-- Field Settings -->
<template v-if="activeField">
<div class="nc-form-field-settings p-4 flex flex-col gap-4 border-b border-gray-200">
<div class="text-base font-bold">{{ $t('objects.field') }} {{ $t('activity.validations').toLowerCase() }}</div>
<div class="text-sm font-bold text-gray-800">{{ $t('objects.field') }} {{ $t('activity.validations').toLowerCase() }}</div>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between gap-3">
<div
@ -95,7 +95,7 @@ const columnSupportsScanning = (elementType: UITypes) =>
v-if="isSelectTypeCol(activeField.uidt)"
class="nc-form-field-appearance-settings p-4 flex flex-col gap-4 border-b border-gray-200"
>
<div class="text-base font-bold text-gray-600">{{ $t('general.appearance') }}</div>
<div class="text-sm font-bold text-gray-800">{{ $t('general.appearance') }}</div>
<div class="flex flex-col gap-6">
<!-- Select type field Options Layout -->
<div>

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

@ -30,6 +30,8 @@ const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(fal
const isDropDownOpen = ref(false)
const isPublic = inject(IsPublicInj, ref(false))
const column = toRef(props, 'column')
const { isUIAllowed, isMetaReadOnly } = useRoles()
@ -52,6 +54,14 @@ const addField = async (payload: any) => {
editColumnDropdown.value = true
}
const enableDescription = ref(false)
watch(editColumnDropdown, (val) => {
if (!val) {
enableDescription.value = false
}
})
const closeAddColumnDropdown = () => {
columnOrder.value = null
editColumnDropdown.value = false
@ -67,10 +77,13 @@ const isColumnEditAllowed = computed(() => {
return true
})
const openHeaderMenu = (e?: MouseEvent) => {
const openHeaderMenu = (e?: MouseEvent, description = false) => {
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick') || isExpandedBulkUpdateForm.value) return
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value && isColumnEditAllowed.value) {
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value && (isColumnEditAllowed.value || description)) {
if (description) {
enableDescription.value = true
}
editColumnDropdown.value = true
}
}
@ -113,7 +126,7 @@ const onClick = (e: Event) => {
isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
}"
@dblclick="openHeaderMenu"
@dblclick="openHeaderMenu($event, false)"
@click.right="openDropDown"
@click="onClick"
>
@ -181,6 +194,12 @@ const onClick = (e: Event) => {
}"
/>
</div>
<NcTooltip v-if="column.description?.length && isPublic && isGrid && !isExpandedForm && !hideMenu">
<template #title>
{{ column.description }}
</template>
<GeneralIcon icon="info" class="group-hover:opacity-100 !w-3.5 !h-3.5 !text-gray-500 flex-none" />
</NcTooltip>
<template v-if="!hideMenu">
<div v-if="!isExpandedForm" class="flex-1" />
@ -210,6 +229,7 @@ const onClick = (e: Event) => {
:column="columnOrder ? null : column"
:column-position="columnOrder"
class="w-full"
:edit-description="enableDescription"
@submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown"
@click.stop

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

@ -25,7 +25,7 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
} else if (isEmail(column)) {
return iconMap.cellEmail
} else if (isYear(column, abstractType)) {
return iconMap.cellDate
return iconMap.cellYear
} else if (isTime(column, abstractType)) {
return iconMap.cellTime
} else if (isRating(column)) {

126
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { type ColumnReqType, partialUpdateAllowedTypes, readonlyMetaAllowedTypes } from 'nocodb-sdk'
import { type ColumnReqType, type ColumnType, partialUpdateAllowedTypes, readonlyMetaAllowedTypes } from 'nocodb-sdk'
import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { SmartsheetStoreEvents, isColumnInvalid } from '#imports'
@ -307,9 +307,9 @@ const handleDelete = () => {
showDeleteColumnModal.value = true
}
const onEditPress = () => {
const onEditPress = (event?: MouseEvent, enableDescription = false) => {
isOpen.value = false
emit('edit')
emit('edit', event, enableDescription)
}
const onInsertBefore = () => {
@ -401,19 +401,40 @@ const changeTitleField = () => {
isOpen.value = false
changeTitleFieldMenu.value = true
}
const openDropdown = () => {
if (isLocked) return
isOpen.value = !isOpen.value
}
const isFieldIdCopied = ref(false)
const { copy } = useClipboard()
const onClickCopyFieldUrl = async (field: ColumnType) => {
await copy(field.id!)
isFieldIdCopied.value = true
}
</script>
<template>
<a-dropdown
v-if="!isLocked"
v-model:visible="isOpen"
:disabled="isLocked"
:trigger="['click']"
:placement="isExpandedForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-column-operations !border-1 rounded-lg !shadow-xl "
@click.stop="isOpen = !isOpen"
@click.stop="openDropdown"
>
<div class="flex items-center gap-2" @dblclick.stop>
<div class="flex gap-0.5 items-center" @dblclick.stop>
<div v-if="isExpandedForm" class="h-[1px]">&nbsp;</div>
<NcTooltip v-if="column?.description?.length && !isExpandedForm" class="flex">
<template #title>
{{ column?.description }}
</template>
<GeneralIcon icon="info" class="group-hover:opacity-100 !w-3.5 !h-3.5 !text-gray-500 flex-none" />
</NcTooltip>
<NcTooltip class="flex items-center">
<GeneralIcon
@ -427,7 +448,7 @@ const changeTitleField = () => {
</template>
</NcTooltip>
<GeneralIcon
v-if="!isExpandedForm"
v-if="!isExpandedForm && !isLocked"
icon="arrowDown"
class="text-grey h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0 mr-2"
/>
@ -439,12 +460,42 @@ const changeTitleField = () => {
'min-w-[256px]': isExpandedForm,
}"
>
<NcTooltip
:attrs="{
class: 'w-full',
}"
placement="top"
>
<template #title>{{ $t('msg.clickToCopyFieldId') }}</template>
<div
class="nc-copy-field flex flex-row justify-between items-center w-[calc(100%_-_12px)] p-2 mx-1.5 rounded-md hover:bg-gray-100 cursor-pointer group"
data-testid="nc-field-item-action-copy-id"
@click.stop="onClickCopyFieldUrl(column)"
>
<div class="w-full flex flex-row justify-between items-center gap-x-2 font-bold text-xs">
<div class="flex flex-row text-gray-500 text-xs items-baseline gap-x-1 font-bold">
{{
$t('labels.idColon', {
fieldId: column.id,
})
}}
</div>
<NcButton size="xsmall" type="secondary" class="!group-hover:bg-gray-100">
<GeneralIcon v-if="isFieldIdCopied" icon="check" class="h-4 w-4" />
<GeneralIcon v-else icon="copy" class="h-4 w-4" />
</NcButton>
</div>
</div>
</NcTooltip>
<a-divider class="!my-0" />
<GeneralSourceRestrictionTooltip message="Field properties cannot be edited." :enabled="!isColumnEditAllowed">
<NcMenuItem
v-if="isUIAllowed('fieldAlter')"
:disabled="column?.pk || isSystemColumn(column) || !isColumnEditAllowed || linksAssociated.length"
:title="linksAssociated.length ? 'Field is associated with a link column' : undefined"
@click="onEditPress"
@click="onEditPress($event, false)"
>
<div class="nc-column-edit nc-header-menu-item">
<component :is="iconMap.ncEdit" class="text-gray-500" />
@ -453,6 +504,31 @@ const changeTitleField = () => {
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<template v-if="!isExpandedForm">
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed && isMetaReadOnly">
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-500" />
<!-- Duplicate -->
{{ t('general.duplicate') }} {{ $t('objects.field').toLowerCase() }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed">
<NcMenuItem
v-if="isUIAllowed('duplicateColumn') && isExpandedForm && !column?.pk"
:disabled="!isDuplicateAllowed"
@click="openDuplicateDlg"
>
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-500" />
<!-- Duplicate -->
{{ t('general.duplicate') }} {{ $t('objects.field').toLowerCase() }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
</template>
<NcMenuItem
v-if="isUIAllowed('fieldAlter') && !!column?.pv"
title="Select a new field as display value"
@ -463,25 +539,19 @@ const changeTitleField = () => {
{{ $t('labels.changeDisplayValueField') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="isUIAllowed('fieldAlter')" title="Add field description" @click="onEditPress($event, true)">
<div class="nc-column-edit-description nc-header-menu-item">
<GeneralIcon icon="ncAlignLeft" class="text-gray-500 !w-4.25 !h-4.25" />
{{ $t('labels.editDescription') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="[UITypes.LinkToAnotherRecord, UITypes.Links].includes(column.uidt)" @click="openLookupMenuDialog">
<div v-e="['a:field:lookup:create']" class="nc-column-lookup-create nc-header-menu-item">
<component :is="iconMap.cellLookup" class="text-gray-500 !w-4.5 !h-4.5" />
{{ t('general.addLookupField') }}
</div>
</NcMenuItem>
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed">
<NcMenuItem
v-if="isUIAllowed('duplicateColumn') && isExpandedForm && !column?.pk"
:disabled="!isDuplicateAllowed"
@click="openDuplicateDlg"
>
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-500" />
<!-- Duplicate -->
{{ t('general.duplicate') }} {{ $t('objects.field').toLowerCase() }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<a-divider v-if="isUIAllowed('fieldAlter') && !column?.pv" class="!my-0" />
<NcMenuItem v-if="!column?.pv" @click="hideOrShowField">
<div v-e="['a:field:hide']" class="nc-column-insert-before nc-header-menu-item">
@ -590,15 +660,6 @@ const changeTitleField = () => {
</NcTooltip>
<a-divider class="!my-0" />
<GeneralSourceRestrictionTooltip message="Field cannot be duplicated." :enabled="!isDuplicateAllowed && isMetaReadOnly">
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-500" />
<!-- Duplicate -->
{{ t('general.duplicate') }} {{ $t('objects.field').toLowerCase() }}
</div>
</NcMenuItem>
</GeneralSourceRestrictionTooltip>
<NcMenuItem @click="onInsertAfter">
<div v-e="['a:field:insert:after']" class="nc-column-insert-after nc-header-menu-item">
<component :is="iconMap.colInsertAfter" class="text-gray-500 !w-4.5 !h-4.5" />
@ -649,7 +710,10 @@ const changeTitleField = () => {
<LazySmartsheetHeaderUpdateDisplayValue v-if="changeTitleFieldMenu" v-model:value="changeTitleFieldMenu" :column="column" />
</template>
<style scoped>
<style scoped lang="scss">
:deep(.nc-menu-item-inner) {
@apply !w-full;
}
.nc-header-menu-item {
@apply text-dropdown flex items-center gap-2;
}

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

@ -31,6 +31,8 @@ const editColumnDropdown = ref(false)
const isDropDownOpen = ref(false)
const enableDescription = ref(false)
const isLocked = inject(IsLockedInj, ref(false))
provide(ColumnInj, column)
@ -122,7 +124,13 @@ const closeAddColumnDropdown = () => {
editColumnDropdown.value = false
}
const openHeaderMenu = (e?: MouseEvent) => {
watch(editColumnDropdown, (val) => {
if (!val) {
enableDescription.value = false
}
})
const openHeaderMenu = (e?: MouseEvent, description = false) => {
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick') || isExpandedBulkUpdateForm.value) return
if (
@ -131,6 +139,9 @@ const openHeaderMenu = (e?: MouseEvent) => {
!isMobileMode.value &&
(!isMetaReadOnly.value || readonlyMetaAllowedTypes.includes(column.value.uidt))
) {
if (description) {
enableDescription.value = true
}
editColumnDropdown.value = true
}
}
@ -150,6 +161,7 @@ const onVisibleChange = () => {
editColumnDropdown.value = true
if (!editOrAddProviderRef.value?.isWebHookModalOpen()) {
editColumnDropdown.value = false
enableDescription.value = false
}
}
@ -231,7 +243,7 @@ const onClick = (e: Event) => {
:is-hidden-col="isHiddenCol"
:virtual="true"
@add-column="addField"
@edit="editColumnDropdown = true"
@edit="openHeaderMenu"
/>
</template>
@ -253,6 +265,7 @@ const onClick = (e: Event) => {
:column="columnOrder ? null : column"
:column-position="columnOrder"
class="w-full"
:edit-description="enableDescription"
@submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown"
@click.stop

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

@ -57,6 +57,8 @@ const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const reloadAggregate = inject(ReloadAggregateHookInj)
const isPublic = inject(IsPublicInj, ref(false))
const { $e } = useNuxtApp()
@ -93,7 +95,10 @@ const {
activeView,
parentId,
computed(() => autoSave.value),
() => reloadDataHook.trigger({ shouldShowLoading: showLoading.value, offset: 0 }),
() => {
reloadDataHook.trigger({ shouldShowLoading: showLoading.value, offset: 0 })
reloadAggregate?.trigger()
},
currentFilters,
props.nestedLevel > 0,
webHook.value,

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

Loading…
Cancel
Save