Browse Source

Merge pull request #7858 from nocodb/develop

pull/7859/head 0.204.5
github-actions[bot] 4 months ago committed by GitHub
parent
commit
7efeba7f09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 39
      .github/workflows/ci-cd.yml
  2. 2
      .github/workflows/unit-test.yml
  3. 4
      docker-compose/traefik/docker-compose.yml
  4. 13
      package.json
  5. 6
      packages/nc-gui/assets/nc-icons/arrow-left.svg
  6. 6
      packages/nc-gui/assets/nc-icons/arrow-right.svg
  7. 3
      packages/nc-gui/assets/nc-icons/cell-attachment.svg
  8. 3
      packages/nc-gui/assets/nc-icons/cell-barcode.svg
  9. 4
      packages/nc-gui/assets/nc-icons/cell-checkbox.svg
  10. 4
      packages/nc-gui/assets/nc-icons/cell-currency.svg
  11. 6
      packages/nc-gui/assets/nc-icons/cell-date.svg
  12. 8
      packages/nc-gui/assets/nc-icons/cell-datetime.svg
  13. 5
      packages/nc-gui/assets/nc-icons/cell-db.svg
  14. 7
      packages/nc-gui/assets/nc-icons/cell-decimal.svg
  15. 7
      packages/nc-gui/assets/nc-icons/cell-duration.svg
  16. 4
      packages/nc-gui/assets/nc-icons/cell-email.svg
  17. 8
      packages/nc-gui/assets/nc-icons/cell-formula.svg
  18. 7
      packages/nc-gui/assets/nc-icons/cell-geometry.svg
  19. 8
      packages/nc-gui/assets/nc-icons/cell-json.svg
  20. 7
      packages/nc-gui/assets/nc-icons/cell-link.svg
  21. 8
      packages/nc-gui/assets/nc-icons/cell-longtext.svg
  22. 9
      packages/nc-gui/assets/nc-icons/cell-lookup.svg
  23. 8
      packages/nc-gui/assets/nc-icons/cell-multiselect.svg
  24. 6
      packages/nc-gui/assets/nc-icons/cell-number.svg
  25. 5
      packages/nc-gui/assets/nc-icons/cell-percentage.svg
  26. 3
      packages/nc-gui/assets/nc-icons/cell-phone.svg
  27. 10
      packages/nc-gui/assets/nc-icons/cell-qrcode.svg
  28. 3
      packages/nc-gui/assets/nc-icons/cell-rating.svg
  29. 3
      packages/nc-gui/assets/nc-icons/cell-rollup.svg
  30. 4
      packages/nc-gui/assets/nc-icons/cell-select.svg
  31. 5
      packages/nc-gui/assets/nc-icons/cell-text.svg
  32. 4
      packages/nc-gui/assets/nc-icons/cell-time.svg
  33. 4
      packages/nc-gui/assets/nc-icons/cell-url.svg
  34. 4
      packages/nc-gui/assets/nc-icons/cell-user.svg
  35. 4
      packages/nc-gui/assets/nc-icons/circle-check.svg
  36. 7
      packages/nc-gui/assets/nc-icons/system-date.svg
  37. 4
      packages/nc-gui/assets/nc-icons/system-key.svg
  38. 6
      packages/nc-gui/assets/nc-icons/system-text.svg
  39. 5
      packages/nc-gui/assets/nc-icons/system-user.svg
  40. 3
      packages/nc-gui/assets/style.scss
  41. 2
      packages/nc-gui/components/account/Profile.vue
  42. 6
      packages/nc-gui/components/account/Token.vue
  43. 4
      packages/nc-gui/components/account/UserList.vue
  44. 2
      packages/nc-gui/components/account/UserMenu.vue
  45. 6
      packages/nc-gui/components/cell/Checkbox.vue
  46. 27
      packages/nc-gui/components/cell/DatePicker.vue
  47. 28
      packages/nc-gui/components/cell/DateTimePicker.vue
  48. 1
      packages/nc-gui/components/cell/Json.vue
  49. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  50. 2
      packages/nc-gui/components/cell/Rating.vue
  51. 10
      packages/nc-gui/components/cell/RichText.vue
  52. 2
      packages/nc-gui/components/cell/SingleSelect.vue
  53. 20
      packages/nc-gui/components/cell/TextArea.vue
  54. 28
      packages/nc-gui/components/cell/TimePicker.vue
  55. 1
      packages/nc-gui/components/cell/User.vue
  56. 28
      packages/nc-gui/components/cell/YearPicker.vue
  57. 6
      packages/nc-gui/components/cell/attachment/index.vue
  58. 4
      packages/nc-gui/components/cell/attachment/utils.ts
  59. 35
      packages/nc-gui/components/cmd-k/index.vue
  60. 6
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  61. 68
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  62. 13
      packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue
  63. 32
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  64. 19
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  65. 5
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  66. 2
      packages/nc-gui/components/dlg/ProjectDelete.vue
  67. 1
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  68. 74
      packages/nc-gui/components/dlg/ViewCreate.vue
  69. 156
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  70. 2
      packages/nc-gui/components/dlg/share-and-collaborate/View.vue
  71. 104
      packages/nc-gui/components/general/BaseIconColorPicker.vue
  72. 110
      packages/nc-gui/components/general/ColorPicker.vue
  73. 61
      packages/nc-gui/components/general/ColorSliderWrapper.vue
  74. 58
      packages/nc-gui/components/general/ProjectIcon.vue
  75. 23
      packages/nc-gui/components/monaco/Editor.vue
  76. 2
      packages/nc-gui/components/nc/Badge.vue
  77. 78
      packages/nc-gui/components/nc/DateWeekSelector.vue
  78. 9
      packages/nc-gui/components/nc/ErrorBoundary.vue
  79. 12
      packages/nc-gui/components/nc/Modal.vue
  80. 6
      packages/nc-gui/components/nc/MonthYearSelector.vue
  81. 7
      packages/nc-gui/components/nc/Switch.vue
  82. 24
      packages/nc-gui/components/project/AccessSettings.vue
  83. 299
      packages/nc-gui/components/project/ShareBaseDlg.vue
  84. 4
      packages/nc-gui/components/project/View.vue
  85. 2
      packages/nc-gui/components/roles/Badge.vue
  86. 4
      packages/nc-gui/components/roles/Selector.vue
  87. 10
      packages/nc-gui/components/smartsheet/Cell.vue
  88. 168
      packages/nc-gui/components/smartsheet/Form.vue
  89. 4
      packages/nc-gui/components/smartsheet/Gallery.vue
  90. 2
      packages/nc-gui/components/smartsheet/Kanban.vue
  91. 2
      packages/nc-gui/components/smartsheet/Map.vue
  92. 14
      packages/nc-gui/components/smartsheet/calendar/Cell.vue
  93. 30
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  94. 475
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  95. 181
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  96. 23
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  97. 93
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  98. 6
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  99. 15
      packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue
  100. 141
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  101. Some files were not shown because too many files have changed in this diff Show More

39
.github/workflows/ci-cd.yml

@ -28,6 +28,33 @@ concurrency:
cancel-in-progress: true
jobs:
validate-swagger-json:
runs-on: ubuntu-20.04
timeout-minutes: 10
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
# enable after fixing all validation errors
# - name: Validate OpenAPI definition
# uses: char0n/swagger-editor-validate@v1
# with:
# swagger-editor-url: http://localhost/
# definition-file: packages/nocodb/src/schema/swagger.json
- name: Validate Swagger JSON
run: |
if ! jq empty packages/nocodb/src/schema/swagger.json; then
echo "swagger.json file is not valid JSON"
exit 1
fi
if ! jq empty packages/nocodb/src/schema/swagger-v2.json; then
echo "swaggerv2.json file is not valid JSON"
exit 1
fi
unit-tests:
runs-on: ubuntu-20.04
timeout-minutes: 40
@ -45,7 +72,7 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: remove use-node-version from .npmrc
- name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc
- name: Get pnpm store directory
shell: bash
@ -80,8 +107,8 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc
- name: remove use-node-version from .npmrc
run: sed -i '/^use-node-version/d' .npmrc
- name: Get pnpm store directory
shell: bash
run: |
@ -134,7 +161,7 @@ jobs:
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: mysql
shard: 4
shard: 4
playwright-sqlite-1:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
@ -162,7 +189,7 @@ jobs:
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: sqlite
shard: 4
shard: 4
playwright-pg-shard-1:
needs: pre-build-for-playwright
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
@ -190,4 +217,4 @@ jobs:
uses: ./.github/workflows/playwright-test-workflow.yml
with:
db: pg
shard: 4
shard: 4

2
.github/workflows/unit-test.yml

@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
node-version: [18.x]
node-version: 18.19.0
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:

4
docker-compose/traefik/docker-compose.yml

@ -34,7 +34,7 @@ services:
retries: 10
test: "pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}"
timeout: 2s
image: "postgres:12.1-alpine"
image: "postgres:12.17-alpine"
networks:
- traefik_proxy
restart: always
@ -71,7 +71,7 @@ services:
- "-c"
- "http://localhost:8081/ping"
timeout: 3s
image: "traefik:v2.2"
image: "traefik:v2.11"
networks:
- default
- traefik_proxy

13
package.json

@ -19,7 +19,7 @@
"fs": "0.0.1-security",
"lerna": "^7.4.2",
"husky": "^8.0.3",
"xlsx": "^0.17.5"
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz"
},
"husky": {
"hooks": {
@ -47,7 +47,16 @@
"pnpm": {
"overrides": {
"vue": "3.3.13",
"typescript": "latest"
"typescript": "latest",
"ajv@<6.12.3": ">=6.12.3",
"node.extend@<1.1.7": ">=1.1.7",
"tough-cookie@<4.1.3": ">=4.1.3",
"@babel/traverse@<7.23.2": ">=7.23.2",
"follow-redirects@<1.15.4": ">=1.15.4",
"axios@>=0.8.1 <0.28.0": ">=0.28.0",
"ip@<1.1.9": ">=1.1.9",
"ip@=2.0.0": ">=2.0.1",
"xml2js@<0.5.0": ">=0.5.0"
}
}
}

6
packages/nc-gui/assets/nc-icons/arrow-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="M12.6666 8H3.33331" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M7.99998 12.6666L3.33331 7.99998L7.99998 3.33331" stroke="currentColor" stroke-width="1.33333"
stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 404 B

6
packages/nc-gui/assets/nc-icons/arrow-right.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="M3.33331 8H12.6666" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M8 3.33331L12.6667 7.99998L8 12.6666" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 392 B

3
packages/nc-gui/assets/nc-icons/cell-attachment.svg

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11.376L12.5445 20.2938C11.3861 21.3862 9.81499 22 8.1768 22C6.53861 22 4.96752 21.3862 3.80914 20.2938C2.65077 19.2013 2 17.7195 2 16.1745C2 14.6295 2.65077 13.1477 3.80914 12.0553L13.2647 3.1375C14.0369 2.40917 15.0843 2 16.1765 2C17.2686 2 18.316 2.40917 19.0882 3.1375C19.8605 3.86583 20.2943 4.85365 20.2943 5.88366C20.2943 6.91367 19.8605 7.9015 19.0882 8.62983L9.6224 17.5476C9.23627 17.9118 8.71257 18.1163 8.16651 18.1163C7.62045 18.1163 7.09675 17.9118 6.71063 17.5476C6.3245 17.1834 6.10758 16.6895 6.10758 16.1745C6.10758 15.6595 6.3245 15.1656 6.71063 14.8014L15.4459 6.57263" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 793 B

3
packages/nc-gui/assets/nc-icons/cell-barcode.svg

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 19V5H3.65377V19H2ZM4.78206 19V5H6.43585V19H4.78206ZM7.56415 19V5H8.69244V19H7.56415ZM10.3462 19V5H12V19H10.3462ZM13.1283 19V5H15.3076V19H13.1283ZM16.4359 19V5H17.5641V19H16.4359ZM19.8207 19V5H22V19H19.8207Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

4
packages/nc-gui/assets/nc-icons/cell-checkbox.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="4" stroke="currentColor" stroke-width="2"/>
<path d="M18 7L9.75 16L6 11.9091" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

4
packages/nc-gui/assets/nc-icons/cell-currency.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1V23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 5H9.5C8.57174 5 7.6815 5.36875 7.02513 6.02513C6.36875 6.6815 6 7.57174 6 8.5C6 9.42826 6.36875 10.3185 7.02513 10.9749C7.6815 11.6313 8.57174 12 9.5 12H14.5C15.4283 12 16.3185 12.3687 16.9749 13.0251C17.6313 13.6815 18 14.5717 18 15.5C18 16.4283 17.6313 17.3185 16.9749 17.9749C16.3185 18.6313 15.4283 19 14.5 19H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 628 B

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

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 640 B

8
packages/nc-gui/assets/nc-icons/cell-datetime.svg

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 10.1389V6.66667C19 5.74619 18.2041 5 17.2222 5H4.77778C3.79594 5 3 5.74619 3 6.66667V18.3333C3 19.2538 3.79594 20 4.77778 20H11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 9.85706H18.4286" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.1428 3V6.42857" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.28577 3V6.42857" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 21C19.2091 21 21 19.2091 21 17C21 14.7909 19.2091 13 17 13C14.7909 13 13 14.7909 13 17C13 19.2091 14.7909 21 17 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 16V16.8889L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1021 B

5
packages/nc-gui/assets/nc-icons/cell-db.svg

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8C16.9706 8 21 6.65685 21 5C21 3.34315 16.9706 2 12 2C7.02944 2 3 3.34315 3 5C3 6.65685 7.02944 8 12 8Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 13.66 17 15 12 15C7 15 3 13.66 3 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 5V19C3 20.66 7 22 12 22C17 22 21 20.66 21 19V5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

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

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8.36365" y="3" width="4.3636" height="8.52631" rx="2.1818" stroke="currentColor" stroke-width="2"/>
<ellipse cx="4.96727" cy="11.2635" rx="0.967266" ry="0.945" fill="currentColor"/>
<rect x="15.6362" y="3" width="4.3636" height="8.52631" rx="2.1818" stroke="currentColor" stroke-width="2"/>
<path d="M16.6061 21L20 17.6842L16.6061 14.3684" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.36365 17.6843H19.9999" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

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

@ -0,0 +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>

After

Width:  |  Height:  |  Size: 949 B

4
packages/nc-gui/assets/nc-icons/cell-email.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 6L12 13L2 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

8
packages/nc-gui/assets/nc-icons/cell-formula.svg

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.19995 9.04529H12.6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 9.04529H17.4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 19.1035C3 21.7403 5.796 21.5217 6 19.1035L7.8 4.5949C8.004 2.17677 10.8 2.78134 10.8 4.5949" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M21 18.7177H17.4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6 18.7177H8.99998" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.2001 9.04529L19.8001 18.7177M19.8001 9.04529L10.2001 18.7177" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 858 B

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

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.73167 3.53666C5.84223 3.31554 6.15777 3.31554 6.26833 3.53666L8.78292 8.56584C8.88265 8.76531 8.7376 9 8.51459 9H3.48541C3.2624 9 3.11735 8.76531 3.21708 8.56584L5.73167 3.53666Z" stroke="currentColor" stroke-width="2"/>
<path d="M14.5714 3L21 9.42857" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.9999 3L14.5713 9.42857" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.33333 15H3.66667C3.29848 15 3 15.2985 3 15.6667V20.3333C3 20.7015 3.29848 21 3.66667 21H8.33333C8.70152 21 9 20.7015 9 20.3333V15.6667C9 15.2985 8.70152 15 8.33333 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="18" cy="18" r="3" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 921 B

8
packages/nc-gui/assets/nc-icons/cell-json.svg

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 20H7.5C6.39543 20 5.5 19.1046 5.5 18V14C5.5 12.8954 4.60457 12 3.5 12H3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M3 12H3.5C4.60457 12 5.5 11.1046 5.5 10V6C5.5 4.89543 6.39543 4 7.5 4H8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16 20H16.5C17.6046 20 18.5 19.1046 18.5 18V14C18.5 12.8954 19.3954 12 20.5 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M21 12H20.5C19.3954 12 18.5 11.1046 18.5 10V6C18.5 4.89543 17.6046 4 16.5 4H16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<ellipse cx="11.995" cy="9.63743" rx="1" ry="1.025" fill="currentColor"/>
<path d="M11.9999 14.7623L11.5499 16.2998" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 912 B

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

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6L21 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 18L21 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 12L21 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 14.3333L10.3333 12L8 9.66663" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 12L10 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 672 B

8
packages/nc-gui/assets/nc-icons/cell-longtext.svg

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 10H11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 6H11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 14H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 18H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10L5.41056 5.17889C5.44741 5.10518 5.55259 5.10518 5.58944 5.17889L8 10" stroke="#1F293A" stroke-width="2" stroke-linecap="round"/>
<path d="M4 9H7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 757 B

9
packages/nc-gui/assets/nc-icons/cell-lookup.svg

@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.3333 19.6667C18.1743 19.6667 19.6667 18.1743 19.6667 16.3333C19.6667 14.4924 18.1743 13 16.3333 13C14.4924 13 13 14.4924 13 16.3333C13 18.1743 14.4924 19.6667 16.3333 19.6667Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 21L19 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.3409 10H3.34094" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 14H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 10H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 6H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 18H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

8
packages/nc-gui/assets/nc-icons/cell-multiselect.svg

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.66667 4.5H3.33333C3.14924 4.5 3 4.64924 3 4.83333V7.16667C3 7.35076 3.14924 7.5 3.33333 7.5H5.66667C5.85076 7.5 6 7.35076 6 7.16667V4.83333C6 4.64924 5.85076 4.5 5.66667 4.5Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 6L21 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.66667 10.5H3.33333C3.14924 10.5 3 10.6492 3 10.8333V13.1667C3 13.3508 3.14924 13.5 3.33333 13.5H5.66667C5.85076 13.5 6 13.3508 6 13.1667V10.8333C6 10.6492 5.85076 10.5 5.66667 10.5Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 12L21 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.66667 16.5H3.33333C3.14924 16.5 3 16.6492 3 16.8333V19.1667C3 19.3508 3.14924 19.5 3.33333 19.5H5.66667C5.85076 19.5 6 19.3508 6 19.1667V16.8333C6 16.6492 5.85076 16.5 5.66667 16.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 18L21 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 9H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 15H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 3L8 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 3L14 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

5
packages/nc-gui/assets/nc-icons/cell-percentage.svg

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 5L5 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 9C7.88071 9 9 7.88071 9 6.5C9 5.11929 7.88071 4 6.5 4C5.11929 4 4 5.11929 4 6.5C4 7.88071 5.11929 9 6.5 9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.5 20C18.8807 20 20 18.8807 20 17.5C20 16.1193 18.8807 15 17.5 15C16.1193 15 15 16.1193 15 17.5C15 18.8807 16.1193 20 17.5 20Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B

3
packages/nc-gui/assets/nc-icons/cell-phone.svg

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.8881 16.9201V19.9201C21.8892 20.1986 21.8322 20.4743 21.7206 20.7294C21.6091 20.9846 21.4454 21.2137 21.2402 21.402C21.035 21.5902 20.7927 21.7336 20.5289 21.8228C20.265 21.912 19.9855 21.9452 19.7081 21.9201C16.631 21.5857 13.6751 20.5342 11.0781 18.8501C8.66194 17.3148 6.61345 15.2663 5.07812 12.8501C3.38809 10.2413 2.33636 7.27109 2.00812 4.1801C1.98313 3.90356 2.01599 3.62486 2.10462 3.36172C2.19324 3.09859 2.33569 2.85679 2.52288 2.65172C2.71008 2.44665 2.93792 2.28281 3.19191 2.17062C3.44589 2.05843 3.72046 2.00036 3.99812 2.0001H6.99812C7.48342 1.99532 7.95391 2.16718 8.32188 2.48363C8.68985 2.80008 8.93019 3.23954 8.99812 3.7201C9.12474 4.68016 9.35957 5.62282 9.69812 6.5301C9.83266 6.88802 9.86178 7.27701 9.78202 7.65098C9.70227 8.02494 9.51698 8.36821 9.24812 8.6401L7.97812 9.9101C9.40167 12.4136 11.4746 14.4865 13.9781 15.9101L15.2481 14.6401C15.52 14.3712 15.8633 14.1859 16.2372 14.1062C16.6112 14.0264 17.0002 14.0556 17.3581 14.1901C18.2654 14.5286 19.2081 14.7635 20.1681 14.8901C20.6539 14.9586 21.0975 15.2033 21.4146 15.5776C21.7318 15.9519 21.9003 16.4297 21.8881 16.9201Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

10
packages/nc-gui/assets/nc-icons/cell-qrcode.svg

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33333 4H4.66667C4.29848 4 4 4.29848 4 4.66667V9.33333C4 9.70152 4.29848 10 4.66667 10H9.33333C9.70152 10 10 9.70152 10 9.33333V4.66667C10 4.29848 9.70152 4 9.33333 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.3333 4H14.6667C14.2985 4 14 4.29848 14 4.66667V9.33333C14 9.70152 14.2985 10 14.6667 10H19.3333C19.7015 10 20 9.70152 20 9.33333V4.66667C20 4.29848 19.7015 4 19.3333 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33333 14H4.66667C4.29848 14 4 14.2985 4 14.6667V19.3333C4 19.7015 4.29848 20 4.66667 20H9.33333C9.70152 20 10 19.7015 10 19.3333V14.6667C10 14.2985 9.70152 14 9.33333 14Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.8889 14H14.1111C14.0497 14 14 14.0497 14 14.1111V14.8889C14 14.9503 14.0497 15 14.1111 15H14.8889C14.9503 15 15 14.9503 15 14.8889V14.1111C15 14.0497 14.9503 14 14.8889 14Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.3889 16.5H16.6111C16.5497 16.5 16.5 16.5497 16.5 16.6111V17.3889C16.5 17.4503 16.5497 17.5 16.6111 17.5H17.3889C17.4503 17.5 17.5 17.4503 17.5 17.3889V16.6111C17.5 16.5497 17.4503 16.5 17.3889 16.5Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.8889 19H19.1111C19.0497 19 19 19.0497 19 19.1111V19.8889C19 19.9503 19.0497 20 19.1111 20H19.8889C19.9503 20 20 19.9503 20 19.8889V19.1111C20 19.0497 19.9503 19 19.8889 19Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.8889 19H14.1111C14.0497 19 14 19.0497 14 19.1111V19.8889C14 19.9503 14.0497 20 14.1111 20H14.8889C14.9503 20 15 19.9503 15 19.8889V19.1111C15 19.0497 14.9503 19 14.8889 19Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.8889 14H19.1111C19.0497 14 19 14.0497 19 14.1111V14.8889C19 14.9503 19.0497 15 19.1111 15H19.8889C19.9503 15 20 14.9503 20 14.8889V14.1111C20 14.0497 19.9503 14 19.8889 14Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

3
packages/nc-gui/assets/nc-icons/cell-rating.svg

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

3
packages/nc-gui/assets/nc-icons/cell-rollup.svg

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7544 21.9998C9.62162 21.9998 7.48916 21.3746 5.67844 20.186C3.32397 18.6409 1.72994 16.3191 1.18949 13.6492C0.672309 11.0938 1.22537 8.37108 2.70682 6.17848C4.13963 4.05777 6.27968 2.63038 8.73333 2.15888C11.0751 1.70886 13.562 2.22527 15.5555 3.57522C17.4745 4.87405 18.7562 6.80196 19.1654 9.00329C19.5547 11.0974 19.0666 13.3111 17.8272 15.0761C16.642 16.7643 14.8983 17.8812 12.916 18.2209C11.0375 18.5424 9.06376 18.0899 7.50163 16.9786C6.01976 15.9243 5.05219 14.3891 4.7772 12.6557C4.51783 11.0199 4.94356 9.31537 5.94562 7.97896C6.88567 6.72533 8.23574 5.92267 9.74727 5.71849C11.1653 5.52742 12.627 5.92201 13.7568 6.80243C14.7998 7.61605 15.4482 8.76228 15.5816 10.0299C15.706 11.2146 15.3313 12.4116 14.5537 13.3152C13.8512 14.1313 12.8894 14.6106 11.8452 14.6656C10.8814 14.7154 9.90466 14.3479 9.22818 13.6808C8.63158 13.0918 8.32337 12.3229 8.36118 11.5162C8.39641 10.7637 8.81018 10.0062 9.41538 9.58673C9.92598 9.23274 10.5279 9.13798 11.1093 9.31967C11.5926 9.471 11.861 9.98245 11.7087 10.462C11.5565 10.9419 11.0406 11.2083 10.5581 11.0572C10.5327 11.0491 10.5175 11.0441 10.466 11.0799C10.3269 11.1762 10.2027 11.4144 10.1939 11.6001C10.1802 11.8953 10.2902 12.1604 10.5212 12.3878C10.8316 12.6941 11.3028 12.8671 11.7483 12.8463C12.4365 12.8101 12.8947 12.4377 13.158 12.132C13.604 11.6138 13.8279 10.8988 13.7558 10.2187C13.6751 9.4457 13.2725 8.74105 12.6232 8.23467C11.882 7.65684 10.9244 7.39681 9.99469 7.52299C8.97594 7.66033 8.06095 8.20845 7.41821 9.066C6.70853 10.0122 6.4069 11.2171 6.58994 12.3723C6.78773 13.6198 7.49103 14.7301 8.57024 15.498C9.73623 16.3271 11.2059 16.6656 12.6036 16.4257C14.0989 16.1693 15.4193 15.3203 16.3217 14.0345C17.2826 12.666 17.6612 10.9525 17.3605 9.33326C17.0413 7.6134 16.0328 6.10269 14.5218 5.07964C12.9293 4.0016 10.9468 3.5891 9.08216 3.94671C7.11213 4.32538 5.3894 5.47793 4.23114 7.19257C3.02032 8.98493 2.5673 11.2074 2.98903 13.2903C3.43209 15.4809 4.74697 17.3902 6.69095 18.6665C8.70975 19.9918 11.2052 20.4774 13.5371 19.9985C17.7225 19.1391 20.9312 15.3679 21.1663 11.0311C21.1936 10.5288 21.6239 10.1445 22.132 10.1707C22.6381 10.1978 23.0261 10.6268 22.9986 11.1293C22.7194 16.2781 18.8967 20.7588 13.9087 21.7828C13.1985 21.9279 12.4764 21.9998 11.7544 21.9998Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

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

@ -0,0 +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>

After

Width:  |  Height:  |  Size: 301 B

5
packages/nc-gui/assets/nc-icons/cell-text.svg

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 7V4H20V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 20H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

4
packages/nc-gui/assets/nc-icons/cell-time.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 6V12L16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

4
packages/nc-gui/assets/nc-icons/cell-url.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 7H18C18.6566 7 19.3068 7.12933 19.9134 7.3806C20.52 7.63188 21.0712 8.00017 21.5355 8.46447C21.9998 8.92876 22.3681 9.47996 22.6194 10.0866C22.8707 10.6932 23 11.3434 23 12C23 12.6566 22.8707 13.3068 22.6194 13.9134C22.3681 14.52 21.9998 15.0712 21.5355 15.5355C21.0712 15.9998 20.52 16.3681 19.9134 16.6194C19.3068 16.8707 18.6566 17 18 17H15M9 17H6C5.34339 17 4.69321 16.8707 4.08658 16.6194C3.47995 16.3681 2.92876 15.9998 2.46447 15.5355C1.52678 14.5979 1 13.3261 1 12C1 10.6739 1.52678 9.40215 2.46447 8.46447C3.40215 7.52678 4.67392 7 6 7H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 12H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

4
packages/nc-gui/assets/nc-icons/cell-user.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

4
packages/nc-gui/assets/nc-icons/circle-check.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11.0799V11.9999C21.9988 14.1563 21.3005 16.2545 20.0093 17.9817C18.7182 19.7088 16.9033 20.9723 14.8354 21.5838C12.7674 22.1952 10.5573 22.1218 8.53447 21.3744C6.51168 20.6271 4.78465 19.246 3.61096 17.4369C2.43727 15.6279 1.87979 13.4879 2.02168 11.3362C2.16356 9.18443 2.99721 7.13619 4.39828 5.49694C5.79935 3.85768 7.69279 2.71525 9.79619 2.24001C11.8996 1.76477 14.1003 1.9822 16.07 2.85986" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 4L12 14.01L9 11.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 722 B

7
packages/nc-gui/assets/nc-icons/system-date.svg

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 12V6.66667C20 5.74619 19.2041 5 18.2222 5H5.77778C4.79594 5 4 5.74619 4 6.66667V18.3333C4 19.2538 4.79594 20 5.77778 20H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 9.85704H19.4286" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.1428 3V6.42857" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.28577 3V6.42857" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 21V15.2414C15 15.1523 15.1077 15.1077 15.1707 15.1707L20.8293 20.8293C20.8923 20.8923 21 20.8477 21 20.7586V15" stroke="#3366FF" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 863 B

4
packages/nc-gui/assets/nc-icons/system-key.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 2L19 4M19 4L22 7L18.5 10.5L15.5 7.5M19 4L15.5 7.5M11.39 11.61C11.9064 12.1195 12.3168 12.726 12.5978 13.3948C12.8787 14.0635 13.0246 14.7813 13.0271 15.5066C13.0295 16.232 12.8884 16.9507 12.6119 17.6213C12.3355 18.2919 11.9291 18.9012 11.4162 19.4141C10.9033 19.9271 10.294 20.3334 9.62333 20.6099C8.95271 20.8864 8.23403 21.0275 7.50866 21.025C6.7833 21.0226 6.06557 20.8767 5.39682 20.5958C4.72807 20.3148 4.1215 19.9043 3.61203 19.388C2.61016 18.3507 2.05579 16.9614 2.06832 15.5193C2.08085 14.0772 2.65928 12.6977 3.67903 11.678C4.69877 10.6583 6.07824 10.0798 7.52032 10.0673C8.96241 10.0548 10.3517 10.6091 11.389 11.611L11.39 11.61ZM11.39 11.61L15.5 7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 22V16.2414C16 16.1523 16.1077 16.1077 16.1707 16.1707L21.8293 21.8293C21.8923 21.8923 22 21.8477 22 21.7586V16" stroke="#3366FF" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

6
packages/nc-gui/assets/nc-icons/system-text.svg

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6V3H18V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 18H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 3V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 22V16.2414C16 16.1523 16.1077 16.1077 16.1707 16.1707L21.8293 21.8293C21.8923 21.8923 22 21.8477 22 21.7586V16" stroke="#3366FF" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

5
packages/nc-gui/assets/nc-icons/system-user.svg

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 21V15.2414C15 15.1523 15.1077 15.1077 15.1707 15.1707L20.8293 20.8293C20.8923 20.8923 21 20.8477 21 20.7586V15" stroke="#3366FF" stroke-width="2" stroke-linecap="round"/>
<path d="M12.5 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

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

@ -784,4 +784,5 @@ svg.nc-cell-icon, svg.nc-virtual-cell-icon {
@apply !flex !pl-4 max-w-[calc(100%_-_16px)];
}
}
}
}

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

@ -94,7 +94,7 @@ const onValidate = async (_: any, valid: boolean) => {
<a-input
v-model:value="email"
class="w-full !rounded-md !py-1.5"
:placeholder="$t('general.email')"
:placeholder="$t('labels.email')"
disabled
data-testid="nc-account-settings-email-input"
/>

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

@ -312,17 +312,17 @@ const handleCancel = () => {
class="flex pl-5 py-3 justify-between token items-center border-l-1 border-r-1 border-b-1"
>
<span class="text-black font-bold text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" length="20">
<GeneralTruncateText placement="top" :length="20">
{{ el.description }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" length="20">
<GeneralTruncateText placement="top" :length="20">
{{ el.created_by }}
</GeneralTruncateText>
</span>
<span class="text-gray-500 font-medium text-3.5 text-start w-3/9">
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" length="29">
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" :length="29">
{{ el.token }}
</GeneralTruncateText>
<span v-else>************************************</span>

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

@ -239,11 +239,11 @@ const openDeleteModal = (user: UserType) => {
<template #title>
{{ el.email }}
</template>
<GeneralTruncateText length="29">
<GeneralTruncateText :length="29">
{{ el.display_name }}
</GeneralTruncateText>
</NcTooltip>
<GeneralTruncateText v-else length="29">
<GeneralTruncateText v-else :length="29">
{{ el.email }}
</GeneralTruncateText>
</div>

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

@ -4,7 +4,7 @@ import type { UsersSortType } from '~/lib'
const { field, direction, handleUserSort } = defineProps<{
field: UsersSortType['field']
direction: UsersSortType['direction']
direction?: UsersSortType['direction']
handleUserSort: Function
}>()

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

@ -4,6 +4,7 @@ import {
ColumnInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
getMdiIcon,
inject,
@ -44,6 +45,8 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const rowHeight = inject(RowHeightInj, ref())
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const checkboxMeta = computed(() => {
return {
icon: {
@ -98,7 +101,8 @@ useSelectedCellKeyupListener(active, (e) => {
}"
:tabindex="readOnly ? -1 : 0"
@click="onClick(false, $event)"
@keydown.enter.stop="onClick(true, $event)"
@keydown.enter.stop="!isSurveyForm ? onClick(true, $event) : undefined"
@keydown.space.stop="isSurveyForm ? onClick(true, $event) : undefined"
>
<div
class="flex items-center"

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

@ -7,6 +7,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
inject,
@ -45,6 +46,10 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isDateInvalid = ref(false)
const dateFormat = computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD')
@ -98,7 +103,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isDateInvalid.value) {
return dateFormat.value
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -232,6 +239,22 @@ const clickHandler = () => {
}
cellClickHandler()
}
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
</script>
<template>
@ -251,7 +274,7 @@ const clickHandler = () => {
:open="isOpen"
@click="clickHandler"
@update:open="updateOpen"
@keydown.enter="open = !open"
@keydown="handleKeydown"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -6,6 +6,8 @@ import {
CellClickHookInj,
ColumnInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
inject,
isDrawerOrModalExist,
@ -35,6 +37,10 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false))
@ -151,7 +157,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isDateInvalid.value) {
return dateTimeFormat.value
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -286,6 +294,22 @@ const clickHandler = () => {
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
</script>
<template>
@ -304,7 +328,7 @@ const isColDisabled = computed(() => {
:open="isOpen"
@click="clickHandler"
@ok="okHandler"
@keydown.enter="open = !open"
@keydown="handleKeydown"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -184,6 +184,7 @@ watch(isExpanded, () => {
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
:hide-minimap="true"
:disable-deep-compare="true"
:auto-focus="!isForm"
@update:model-value="localValue = $event"
@keydown.enter.stop
/>

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

@ -394,7 +394,7 @@ const onFocus = () => {
@click="toggleMenu"
>
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full">
<a-checkbox-group v-model:value="vModel" class="nc-field-layout-list">
<a-checkbox-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list">
<a-checkbox
v-for="op of options"
:key="op.title"

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

@ -37,7 +37,7 @@ const ratingMeta = computed(() => {
})
const vModel = computed({
get: () => modelValue ?? NaN,
get: () => Number(modelValue),
set: (val) => emits('update:modelValue', val),
})

10
packages/nc-gui/components/cell/RichText.vue

@ -9,7 +9,7 @@ import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links'
import { IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, RowHeightInj } from '#imports'
import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, ReadonlyInj, RowHeightInj } from '#imports'
const props = defineProps<{
value?: string | null
@ -33,6 +33,8 @@ const readOnlyCell = inject(ReadonlyInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
const isFocused = ref(false)
const turndownService = new TurndownService({})
@ -222,6 +224,7 @@ useEventListener(
'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode,
'readonly': readOnly,
'nc-form-rich-text-field !p-0': isFormField,
'nc-rich-text-grid': isGrid,
}"
:tabindex="readOnlyCell || isFormField ? -1 : 0"
>
@ -285,11 +288,12 @@ useEventListener(
.ProseMirror {
@apply !border-transparent max-h-full;
}
&:not(.nc-form-rich-text-field) {
&:not(.nc-form-rich-text-field):not(.nc-rich-text-grid) {
.ProseMirror {
min-height: 8rem;
}
}
&.nc-form-rich-text-field {
.ProseMirror {
padding: 0;
@ -346,7 +350,7 @@ useEventListener(
pointer-events: none;
}
.ProseMirror {
@apply flex-grow pt-1 border-1 border-gray-200 rounded-lg;
@apply flex-grow pt-1.5 border-1 border-gray-200 rounded-lg;
> * {
@apply ml-1;

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

@ -315,7 +315,7 @@ const onFocus = () => {
@keydown.enter.stop.prevent="toggleMenu"
>
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full">
<a-radio-group v-model:value="vModel" class="nc-field-layout-list">
<a-radio-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list">
<a-radio
v-for="op of options"
:key="op.title"

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

@ -203,7 +203,7 @@ watch(inputWrapperRef, () => {
<template>
<div>
<div
class="flex flex-row pt-0.5 w-full long-text-wrapper"
class="flex flex-row w-full long-text-wrapper"
:class="{
'min-h-10': rowHeight !== 1 || isExpandedFormOpen,
'min-h-9': rowHeight === 1 && !isExpandedFormOpen,
@ -213,6 +213,10 @@ watch(inputWrapperRef, () => {
<div
v-if="isRichMode"
class="w-full cursor-pointer nc-readonly-rich-text-wrapper"
:class="{
'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm,
'nc-readonly-rich-text-sort-height': rowHeight === 1 && !isExpandedFormOpen && !isForm,
}"
:style="{
maxHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${25 * (rowHeight || 1)}px`,
minHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${25 * (rowHeight || 1)}px`,
@ -256,10 +260,9 @@ watch(inputWrapperRef, () => {
v-else-if="rowHeight"
:value="vModel"
:lines="rowHeight"
class="mr-7 nc-text-area-clamped-text"
class="nc-text-area-clamped-text my-auto"
:style="{
'word-break': 'break-word',
'white-space': 'pre-line',
'max-height': `${25 * (rowHeight || 1)}px`,
}"
@click="onTextClick"
@ -350,6 +353,17 @@ textarea:focus {
.nc-longtext-scrollbar {
@apply scrollbar-thin scrollbar-thumb-gray-200 hover:scrollbar-thumb-gray-300 scrollbar-track-transparent;
}
.nc-readonly-rich-text-wrapper {
&.nc-readonly-rich-text-grid {
:deep(.ProseMirror) {
@apply !pt-0;
}
&.nc-readonly-rich-text-sort-height {
@apply mt-2;
}
}
}
</style>
<style lang="scss">

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

@ -3,6 +3,8 @@ import dayjs from 'dayjs'
import {
ActiveCellInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
inject,
onClickOutside,
@ -32,6 +34,10 @@ const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const column = inject(ColumnInj)!
const isTimeInvalid = ref(false)
@ -90,7 +96,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isTimeInvalid.value) {
return 'HH:mm'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -107,6 +115,22 @@ const isOpen = computed(() => {
return (readOnly.value || (localState.value && isPk)) && !active.value && !editable.value ? false : open.value
})
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
@ -126,6 +150,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
<template>
<a-time-picker
v-model:value="localState"
:tabindex="0"
:disabled="readOnly"
:show-time="true"
:bordered="false"
@ -138,6 +163,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:input-read-only="true"
:open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@keydown="handleKeydown"
@click="open = (active || editable) && !open"
@ok="open = !open"
>

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

@ -323,6 +323,7 @@ const filterOption = (input: string, option: any) => {
<component
:is="isMultiple ? CheckboxGroup : RadioGroup"
v-model:value="vModelListLayout"
:disabled="readOnly || !editAllowed"
class="nc-field-layout-list"
@update:value="
(value) => {

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

@ -3,6 +3,8 @@ import dayjs from 'dayjs'
import {
ActiveCellInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
inject,
@ -31,6 +33,10 @@ const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isYearInvalid = ref(false)
const { t } = useI18n()
@ -77,7 +83,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isYearInvalid.value) {
return 'YYYY'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -94,6 +102,22 @@ const isOpen = computed(() => {
return (readOnly.value || (localState.value && isPk)) && !active.value && !editable.value ? false : open.value
})
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
@ -123,10 +147,10 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:input-read-only="true"
:open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@keydown="handleKeydown"
@click="open = (active || editable) && !open"
@change="open = (active || editable) && !open"
@ok="open = !open"
@keydown.enter="open = !open"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -9,6 +9,7 @@ import {
IsExpandedFormOpenInj,
IsGalleryInj,
IsKanbanInj,
IsSurveyFormInj,
RowHeightInj,
iconMap,
inject,
@ -49,6 +50,8 @@ const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()!
const { isMobileMode } = useGlobal()
@ -208,7 +211,8 @@ const onImageClick = (item: any) => {
data-testid="attachment-cell-file-picker-button"
tabindex="0"
@click="open"
@keydown.enter="open"
@keydown.enter="!isSurveyForm ? open($event) : undefined"
@keydown.space="isSurveyForm ? open($event) : undefined"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

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

@ -63,7 +63,9 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const { api, isLoading } = useApi()
const { files, open } = useFileDialog()
const { files, open } = useFileDialog({
reset: true,
})
const { appInfo } = useGlobal()

35
packages/nc-gui/components/cmd-k/index.vue

@ -17,6 +17,7 @@ interface CmdAction {
keywords?: string[]
section?: string
is_default?: number | null
iconColor?: string
}
const props = defineProps<{
@ -385,20 +386,26 @@ defineExpose({
@click="fireAction(act)"
>
<div class="cmdk-action-content w-full">
<component
:is="(iconMap as any)[act.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]"
class="cmdk-action-icon"
:class="{
'!text-blue-500': act.icon === 'grid',
'!text-purple-500': act.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery',
}"
/>
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly />
</div>
<template v-if="title === 'Bases' || act.icon === 'project'">
<GeneralBaseIconColorPicker :key="act.iconColor" :model-value="act.iconColor" type="database" readonly>
</GeneralBaseIconColorPicker>
</template>
<template v-else>
<component
:is="(iconMap as any)[act.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]"
class="cmdk-action-icon"
:class="{
'!text-blue-500': act.icon === 'grid',
'!text-purple-500': act.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery',
}"
/>
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly />
</div>
</template>
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ act.title }}

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

@ -6,6 +6,8 @@ const { isUIAllowed } = useRoles()
const { appInfo } = useGlobal()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { isWorkspaceLoading, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore)
const { navigateToWorkspaceSettings } = workspaceStore
@ -15,8 +17,10 @@ const { isSharedBase } = storeToRefs(baseStore)
const isCreateProjectOpen = ref(false)
const navigateToSettings = () => {
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
// TODO: Handle cloud case properly
navigateToWorkspaceSettings()
navigateToWorkspaceSettings('', cmdOrCtrl)
// if (appInfo.value.baseHostName) {
// window.location.href = `https://app.${appInfo.value.baseHostName}/dashboard`

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

@ -16,6 +16,7 @@ import {
h,
inject,
navigateTo,
navigateToBlankTargetOpenOption,
openLink,
ref,
resolveComponent,
@ -26,6 +27,7 @@ import {
useDialog,
useGlobal,
useI18n,
useMagicKeys,
useNuxtApp,
useRoles,
useRouter,
@ -71,6 +73,10 @@ const { orgRoles, isUIAllowed } = useRoles()
useTabs()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { refreshCommandPalette } = useCommandPalette()
const editMode = ref(false)
const tempTitle = ref('')
@ -168,18 +174,20 @@ defineExpose({
enableEditMode,
})
const setIcon = async (icon: string, base: BaseType) => {
const setColor = async (color: string, base: BaseType) => {
try {
const meta = {
...((base.meta as object) || {}),
icon,
...parseProp(base.meta),
iconColor: color,
}
basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:base:icon:navdraw', { icon })
$e('a:base:icon:color:navdraw', { iconColor: color })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
refreshCommandPalette()
}
}
@ -253,12 +261,29 @@ const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggl
if (!base) {
return
}
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
if (!toggleIsExpanded) $e('c:base:open')
if (!toggleIsExpanded && !cmdOrCtrl) $e('c:base:open')
ignoreNavigation = isMobileMode.value || ignoreNavigation
toggleIsExpanded = isMobileMode.value || toggleIsExpanded
if (cmdOrCtrl && !ignoreNavigation) {
await navigateTo(
`${cmdOrCtrl ? '#' : ''}${baseUrl({
id: base.id!,
type: 'database',
isSharedBase: isSharedBase.value,
})}`,
cmdOrCtrl
? {
open: navigateToBlankTargetOpenOption,
}
: undefined,
)
return
}
if (toggleIsExpanded) {
base.isExpanded = !base.isExpanded
} else {
@ -399,19 +424,20 @@ const projectDelete = () => {
</NcButton>
<div class="flex items-center mr-1" @click="onProjectClick(base)">
<div v-e="['c:base:emojiSelect']" class="flex items-center select-none w-6 h-full">
<div class="flex items-center select-none w-6 h-full">
<a-spin v-if="base.isLoading" class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8" :indicator="indicator" />
<LazyGeneralEmojiPicker
v-else
:key="base.meta?.icon"
:emoji="base.meta?.icon"
:readonly="true"
size="small"
@emoji-selected="setIcon($event, base)"
>
<GeneralProjectIcon :type="base.type" />
</LazyGeneralEmojiPicker>
<div v-else>
<GeneralBaseIconColorPicker
:key="`${base.id}_${parseProp(base.meta).iconColor}`"
:type="base?.type"
:model-value="parseProp(base.meta).iconColor"
size="small"
:readonly="(base?.type && base?.type !== 'database') || !isUIAllowed('baseRename')"
@update:model-value="setColor($event, base)"
>
</GeneralBaseIconColorPicker>
</div>
</div>
</div>
@ -497,7 +523,12 @@ const projectDelete = () => {
</NcMenuItem>
<!-- ERD View -->
<NcMenuItem key="erd" data-testid="nc-sidebar-base-relations" @click="openErdView(base?.sources?.[0]!)">
<NcMenuItem
v-if="base?.sources?.[0]?.enabled"
key="erd"
data-testid="nc-sidebar-base-relations"
@click="openErdView(base?.sources?.[0]!)"
>
<div v-e="['c:base:erd']" class="flex gap-2 items-center">
<GeneralIcon icon="erd" />
{{ $t('title.relations') }}
@ -523,7 +554,7 @@ const projectDelete = () => {
</NcMenuItem>
</template>
<template v-if="base.sources && base.sources[0] && showBaseOption">
<template v-if="base?.sources?.[0]?.enabled && showBaseOption">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template>
@ -560,6 +591,7 @@ const projectDelete = () => {
<NcButton
v-if="isUIAllowed('tableCreate', { roles: baseRole })"
v-e="['c:base:create-table']"
:disabled="!base?.sources?.[0]?.enabled"
class="nc-sidebar-node-btn"
size="xxsmall"
type="text"

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

@ -2,10 +2,15 @@
import type { BaseType } from 'nocodb-sdk'
import { ProjectInj, ProjectRoleInj } from '#imports'
const props = defineProps<{
baseRole: string | string[]
base: BaseType
}>()
const props = withDefaults(
defineProps<{
baseRole: string | string[]
base: BaseType
}>(),
{
baseRole: '',
},
)
const baseRole = toRef(props, 'baseRole')
const base = toRef(props, 'base')

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

@ -4,7 +4,7 @@ import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia'
import { ProjectRoleInj, TreeViewInj, useNuxtApp, useRoles, useTabs } from '#imports'
import { ProjectRoleInj, TreeViewInj, useMagicKeys, useNuxtApp, useRoles, useTabs } from '#imports'
import type { SidebarTableNode } from '~/lib'
const props = withDefaults(
@ -39,13 +39,15 @@ useTableNew({
baseId: base.value.id!,
})
const { meta: metaKey, ctrlKey } = useMagicKeys()
const baseRole = inject(ProjectRoleInj)
provide(SidebarTableInj, table)
const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)!
const { loadViews: _loadViews } = useViewsStore()
const { activeView, activeViewTitleOrId } = storeToRefs(useViewsStore())
const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
// todo: temp
@ -106,6 +108,11 @@ const onExpand = async () => {
}
const onOpenTable = async () => {
if (isMac() ? metaKey.value : ctrlKey.value) {
await _openTable(table.value, true)
return
}
isLoading.value = true
try {
await _openTable(table.value)
@ -138,6 +145,27 @@ watch(
const isTableOpened = computed(() => {
return openedTableId.value === table.value?.id && (activeView.value?.is_default || !activeViewTitleOrId.value)
})
let tableTimeout: NodeJS.Timeout
watch(openedTableId, () => {
if (tableTimeout) {
clearTimeout(tableTimeout)
}
if (table.value.id !== openedTableId.value && isExpanded.value) {
const views = viewsByTable.value.get(table.value.id!)?.filter((v) => !v.is_default) ?? []
if (views.length) return
tableTimeout = setTimeout(() => {
if (isExpanded.value) {
isExpanded.value = false
}
clearTimeout(tableTimeout)
}, 10000)
}
})
</script>
<template>

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

@ -9,6 +9,7 @@ import {
message,
onKeyStroke,
useDebounceFn,
useMagicKeys,
useNuxtApp,
useRoles,
useVModel,
@ -52,6 +53,8 @@ const { activeView } = storeToRefs(useViewsStore())
const { getMeta } = useMetas()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const table = computed(() => props.table)
const injectedTable = ref(table.value)
@ -80,11 +83,21 @@ const _title = ref<string | undefined>()
/** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */
const onClick = useDebounceFn(() => {
if (isEditing.value || isStopped.value) return
emits('changeView', vModel.value)
}, 250)
const handleOnClick = () => {
if (isEditing.value || isStopped.value) return
const cmdOrCtrl = isMac() ? metaKey.value : ctrlKey.value
if (cmdOrCtrl) {
emits('changeView', vModel.value)
} else {
onClick()
}
}
/** Enable editing view name on dbl click */
function onDblClick() {
if (isMobileMode.value) return
@ -209,7 +222,7 @@ watch(isDropdownOpen, async () => {
}"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick"
@click="onClick"
@click.prevent="handleOnClick"
>
<div v-e="['a:view:open', { view: vModel.type }]" class="text-sm flex items-center w-full gap-1" data-testid="view-item">
<div

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

@ -222,6 +222,11 @@ const editBase = async () => {
const config = { ...formState.value.dataSource, connection }
// todo: refactor and remove this duplicate path in config
if (config.client === ClientType.SQLITE && config.connection?.connection?.filename) {
config.connection.filename = config.connection.connection.filename
}
await api.source.update(base.value?.id, props.sourceId, {
alias: formState.value.title,
type: formState.value.dataSource.client,

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

@ -52,7 +52,7 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralProjectIcon :type="base.type" class="nc-view-icon px-1.5 w-10" />
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon w-6 h-6 mx-1" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

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

@ -62,6 +62,7 @@ const _duplicate = async () => {
primaryColor: color,
accentColor: complement.toHex8String(),
},
iconColor: parseProp(props.base.meta).iconColor,
}),
},
})

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

@ -218,7 +218,6 @@ async function onSubmit() {
}
/*
TODO: Add support for end date and multiple range in future
const addCalendarRange = async () => {
form.calendar_range.push({
fk_from_column_id: viewSelectFieldOptions.value[0].value as string,
@ -318,7 +317,7 @@ onMounted(async () => {
<template>
<NcModal
v-model:visible="vModel"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'medium' : 'small'"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'small' : 'small'"
>
<template #header>
<div class="flex w-full flex-row justify-between items-center">
@ -453,23 +452,20 @@ onMounted(async () => {
</div>
</a-select-option>
</NcSelect>
<!--
<div
v-if="range.fk_to_column_id === null && isEeUI"
class="cursor-pointer flex items-center text-gray-800 gap-1"
@click="range.fk_to_column_id = undefined"
>
<component :is="iconMap.plus" class="h-4 w-4" />
{{ $t('activity.addEndDate') }}
</div>
<template v-else-if="isEeUI">
<span>
{{ $t('activity.withEndDate') }}
</span>
TODO: Add support for end date and multiple range in future
<div class="flex">
<!-- <div
v-if="range.fk_to_column_id === null && isEeUI"
class="cursor-pointer flex items-center text-gray-800 gap-1"
@click="range.fk_to_column_id = undefined"
>
<component :is="iconMap.plus" class="h-4 w-4" />
{{ $t('activity.addEndDate') }}
</div>
<template v-else-if="isEeUI">
<span>
{{ $t('activity.withEndDate') }}
</span>
<div class="flex">
<NcSelect
v-model:value="range.fk_to_column_id"
:disabled="isMetaLoading"
@ -499,27 +495,29 @@ class="cursor-pointer flex items-center text-gray-800 gap-1"
</NcSelect>
<NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="range.fk_to_column_id = null">
<component :is="iconMap.delete" class="h-4 w-4" />
</NcButton> -->
</div>
</template>
<!-- <NcButton
v-if="index !== 0"
size="small"
type="secondary"
@click="
() => {
form.calendar_range = form.calendar_range.filter((_, i) => i !== index)
}
"
>
<component :is="iconMap.close" />
</NcButton>
</div>
<NcButton class="mt-2" size="small" type="secondary" @click="addCalendarRange">
</NcButton>
</div>
<NcButton
v-if="index !== 0"
size="small"
type="secondary"
@click="
() => {
form.calendar_range = form.calendar_range.filter((_, i) => i !== index)
}
"
>
<component :is="iconMap.close" />
</NcButton>
</template>
</div> -->
<!-- <NcButton class="mt-2" size="small" type="secondary" @click="addCalendarRange">
<component :is="iconMap.plus" />
Add another date field
</NcButton>
-->
</NcButton> -->
</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">
<GeneralIcon class="!text-5xl text-orange-500" icon="warning" />

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ColumnType, KanbanType, ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import { useMetas } from '#imports'
import { PreFilledMode, useMetas } from '#imports'
const { view: _view, $api } = useSmartsheetStoreOrThrow()
const { $e } = useNuxtApp()
@ -44,6 +44,10 @@ const activeView = computed<(ViewType & { meta: object & Record<string, any> })
},
})
const isPublicShared = computed(() => {
return !!activeView.value?.uuid
})
const url = computed(() => {
return sharedViewUrl() ?? ''
})
@ -145,6 +149,37 @@ const surveyMode = computed({
},
})
const formPreFill = computed({
get: () => ({
preFillEnabled: parseProp(activeView.value?.meta)?.preFillEnabled ?? false,
preFilledMode: parseProp(activeView.value?.meta)?.preFilledMode || PreFilledMode.Default,
}),
set: (value) => {
if (!activeView.value?.meta) return
if (formPreFill.value.preFillEnabled !== value.preFillEnabled) {
$e(`a:view:share:prefilled-mode-${value.preFillEnabled ? 'enabled' : 'disabled'}`)
}
if (formPreFill.value.preFilledMode !== value.preFilledMode) {
$e(`a:view:share:${value.preFilledMode}-prefilled-mode`)
}
activeView.value.meta = {
...activeView.value.meta,
...value,
}
savePreFilledMode()
},
})
const handleChangeFormPreFill = (value: { preFillEnabled?: boolean; preFilledMode?: PreFilledMode }) => {
formPreFill.value = {
...formPreFill.value,
...value,
}
}
function sharedViewUrl() {
if (!activeView.value) return
@ -177,7 +212,11 @@ function sharedViewUrl() {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return encodeURI(`${dashboardUrl1}#/nc/${viewType}/${activeView.value.uuid}`)
return encodeURI(
`${dashboardUrl1}#/nc/${viewType}/${activeView.value.uuid}${surveyMode.value ? '/survey' : ''}${
viewStore.preFillFormSearchParams && formPreFill.value.preFillEnabled ? `?${viewStore.preFillFormSearchParams}` : ''
}`,
)
}
const toggleViewShare = async () => {
@ -254,9 +293,11 @@ async function updateSharedView() {
return true
}
const isPublicShared = computed(() => {
return !!activeView.value?.uuid
})
async function savePreFilledMode() {
await updateSharedView()
}
watchEffect(() => {})
</script>
<template>
@ -279,7 +320,7 @@ const isPublicShared = computed(() => {
<GeneralCopyUrl v-model:url="url" />
</div>
<div class="flex flex-col justify-between mt-1 py-2 px-3 bg-gray-50 rounded-md">
<div class="flex flex-row justify-between">
<div class="flex flex-row items-center justify-between">
<div class="flex text-black">{{ $t('activity.restrictAccessWithPassword') }}</div>
<a-switch
v-e="['c:share:view:password:toggle']"
@ -287,6 +328,7 @@ const isPublicShared = computed(() => {
:loading="isUpdating.password"
class="share-password-toggle !mt-0.25"
data-testid="share-password-toggle"
size="small"
@click="togglePasswordProtected"
/>
</div>
@ -307,13 +349,9 @@ const isPublicShared = computed(() => {
<div
v-if="
activeView &&
(activeView.type === ViewTypes.GRID ||
activeView.type === ViewTypes.KANBAN ||
activeView.type === ViewTypes.GALLERY ||
activeView.type === ViewTypes.MAP ||
activeView.type === ViewTypes.CALENDAR)
[ViewTypes.GRID, ViewTypes.KANBAN, ViewTypes.GALLERY, ViewTypes.MAP, ViewTypes.CALENDAR].includes(activeView.type)
"
class="flex flex-row justify-between"
class="flex flex-row items-center justify-between"
>
<div class="flex text-black">{{ $t('activity.allowDownload') }}</div>
<a-switch
@ -322,27 +360,81 @@ const isPublicShared = computed(() => {
:loading="isUpdating.download"
class="public-password-toggle !mt-0.25"
data-testid="share-download-toggle"
size="small"
/>
</div>
<div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-row justify-between">
<div class="text-black">{{ $t('activity.surveyMode') }}</div>
<template v-if="activeView?.type === ViewTypes.FORM">
<div class="flex flex-row items-center justify-between">
<div class="text-black flex items-center space-x-1">
<div>
{{ $t('activity.surveyMode') }}
</div>
<NcTooltip>
<template #title> {{ $t('tooltip.surveyFormInfo') }}</template>
<GeneralIcon icon="info" class="text-gray-600 cursor-pointer"></GeneralIcon>
</NcTooltip>
</div>
<a-switch
v-model:checked="surveyMode"
v-e="['c:share:view:surver-mode:toggle']"
data-testid="nc-modal-share-view__surveyMode"
size="small"
>
</a-switch>
</div>
<div v-if="!isEeUI" class="flex flex-row items-center justify-between">
<div class="text-black">{{ $t('activity.rtlOrientation') }}</div>
<a-switch
v-model:checked="withRTL"
v-e="['c:share:view:rtl-orientation:toggle']"
data-testid="nc-modal-share-view__RTL"
size="small"
>
</a-switch>
</div>
</template>
</div>
<div
v-if="activeView?.type === ViewTypes.FORM"
class="nc-pre-filled-mode-wrapper flex flex-col justify-between gap-y-3 mt-1 py-2 px-3 bg-gray-50 rounded-md"
>
<div class="flex flex-row items-center justify-between">
<div class="text-black flex items-center space-x-1">
<div>
{{ $t('activity.preFilledFields.title') }}
</div>
<NcTooltip>
<template #title>
<div class="text-center">
{{ $t('tooltip.preFillFormInfo') }}
</div>
</template>
<GeneralIcon icon="info" class="text-gray-600 cursor-pointer"></GeneralIcon>
</NcTooltip>
</div>
<a-switch
v-model:checked="surveyMode"
v-e="['c:share:view:surver-mode:toggle']"
data-testid="nc-modal-share-view__surveyMode"
>
</a-switch>
</div>
<div v-if="activeView?.type === ViewTypes.FORM && !isEeUI" class="flex flex-row justify-between">
<div class="text-black">{{ $t('activity.rtlOrientation') }}</div>
<a-switch
v-model:checked="withRTL"
v-e="['c:share:view:rtl-orientation:toggle']"
data-testid="nc-modal-share-view__RTL"
:checked="formPreFill.preFillEnabled"
data-testid="nc-modal-share-view__preFill"
size="small"
@update:checked="handleChangeFormPreFill({ preFillEnabled: $event as boolean })"
>
</a-switch>
</div>
<a-radio-group
v-if="formPreFill.preFillEnabled"
:value="formPreFill.preFilledMode"
class="nc-modal-share-view-preFillMode"
data-testid="nc-modal-share-view__preFillMode"
@update:value="handleChangeFormPreFill({ preFilledMode: $event })"
>
<a-radio v-for="mode of Object.values(PreFilledMode)" :key="mode" :value="mode">
<div class="flex-1">{{ $t(`activity.preFilledFields.${mode}`) }}</div>
</a-radio>
</a-radio-group>
</div>
</template>
</div>
@ -367,4 +459,18 @@ const isPublicShared = computed(() => {
line-height: 1rem !important;
}
}
.nc-modal-share-view-preFillMode {
@apply flex flex-col;
.ant-radio-wrapper {
@apply !m-0 !flex !items-center w-full px-2 py-1 rounded-lg hover:bg-gray-100;
.ant-radio {
@apply !top-0;
}
.ant-radio + span {
@apply !flex !pl-4;
}
}
}
</style>

2
packages/nc-gui/components/dlg/share-and-collaborate/View.vue

@ -154,7 +154,7 @@ watch(showShareModal, (val) => {
</div>
<div class="share-base">
<div class="flex flex-row items-center gap-x-2 px-4 pt-3 pb-3 select-none">
<GeneralProjectIcon :type="base.type" class="nc-view-icon group-hover" />
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon group-hover" />
<div>{{ $t('activity.shareBase.label') }}</div>
<div

104
packages/nc-gui/components/general/BaseIconColorPicker.vue

@ -0,0 +1,104 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import { NcProjectType, baseIconColors } from '#imports'
const props = withDefaults(
defineProps<{
type?: NcProjectType | string
modelValue?: string
size?: 'small' | 'medium' | 'large' | 'xlarge'
readonly?: boolean
iconClass?: string
}>(),
{
type: NcProjectType.DB,
size: 'small',
},
)
const emit = defineEmits(['update:modelValue'])
const { modelValue } = toRefs(props)
const { size, readonly } = props
const isOpen = ref(false)
const colorRef = ref(tinycolor(modelValue.value).isValid() ? modelValue.value : baseIconColors[0])
const updateIconColor = (color: string) => {
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
colorRef.value = color
}
}
const onClick = (e: Event) => {
if (readonly) return
e.stopPropagation()
isOpen.value = !isOpen.value
}
watch(
isOpen,
(value) => {
if (!value && colorRef.value !== modelValue.value) {
emit('update:modelValue', colorRef.value)
}
},
{
immediate: true,
},
)
</script>
<template>
<div>
<a-dropdown v-model:visible="isOpen" :trigger="['click']" :disabled="readonly">
<div
class="flex flex-row justify-center items-center select-none rounded-md nc-base-icon-picker-trigger"
:class="{
'hover:bg-gray-500 hover:bg-opacity-15 cursor-pointer': !readonly,
'bg-gray-500 bg-opacity-15': isOpen,
'h-6 w-6 text-lg': size === 'small',
'h-8 w-8 text-xl': size === 'medium',
'h-10 w-10 text-2xl': size === 'large',
'h-14 w-16 text-5xl': size === 'xlarge',
}"
@click="onClick"
>
<NcTooltip placement="topLeft" :disabled="readonly">
<template #title> {{ $t('tooltip.changeIconColour') }} </template>
<div>
<GeneralProjectIcon :color="colorRef" :type="type" />
</div>
</NcTooltip>
</div>
<template #overlay>
<div
class="nc-base-icon-color-picker-dropdown relative bg-white rounded-lg border-1 border-gray-200 overflow-hidden max-w-[342px]"
>
<div class="flex justify-start">
<GeneralColorPicker
:model-value="colorRef"
:colors="baseIconColors"
:is-new-design="true"
class="nc-base-icon-color-picker"
@input="updateIconColor"
/>
</div>
</div>
</template>
</a-dropdown>
</div>
</template>
<style lang="scss" scoped>
.nc-base-icon-color-picker-dropdown {
box-shadow: 0px 8px 8px -4px #0000000a, 0px 20px 24px -4px #0000001a;
}
</style>

110
packages/nc-gui/components/general/ColorPicker.vue

@ -1,4 +1,5 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import { computed, enumColor, ref, watch } from '#imports'
interface Props {
@ -7,7 +8,7 @@ interface Props {
rowSize?: number
advanced?: boolean
pickButton?: boolean
borders?: string[]
colorBoxBorder?: boolean
isNewDesign?: boolean
}
@ -17,6 +18,7 @@ const props = withDefaults(defineProps<Props>(), {
rowSize: 10,
advanced: true,
pickButton: false,
colorBoxBorder: false,
isNewDesign: false,
})
@ -41,7 +43,8 @@ const selectColor = (color: string, closeModal = false) => {
const isPickerOn = ref(false)
const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase()
const compare = (colorA: string, colorB: string) =>
colorA.toLowerCase() === colorB.toLowerCase() || colorA.toLowerCase() === tinycolor(colorB).toHex8String().toLowerCase()
watch(picked, (n, _o) => {
vModel.value = n
@ -50,31 +53,57 @@ watch(picked, (n, _o) => {
<template>
<div class="color-picker">
<div v-for="colId in Math.ceil(props.colors.length / props.rowSize)" :key="colId" class="color-picker-row">
<button
<div
v-for="colId in Math.ceil(props.colors.length / props.rowSize)"
:key="colId"
class="color-picker-row"
:class="{
'mt-2': colId > 1,
}"
>
<div
v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)"
:key="`color-${colId}-${i}`"
class="color-selector"
:class="{ 'selected': compare(picked, color), 'new-design': isNewDesign }"
:style="{
'background-color': `${color}`,
'border': borders?.length && borders[i] ? `1px solid ${borders[i]}` : undefined,
class="p-1 rounded-md flex h-8"
:class="{
'hover:bg-gray-200': isNewDesign,
}"
@click="selectColor(color, true)"
>
{{ compare(picked, color) && !isNewDesign ? '&#10003;' : '' }}
</button>
<button class="h-6 w-6 mt-2.7 ml-1 border-1 border-[grey] rounded-md" @click="isPickerOn = !isPickerOn">
<GeneralTooltip>
<template #title>{{ $t('activity.moreColors') }}</template>
<div class="flex items-center justify-center">
<GeneralIcon :icon="isPickerOn ? 'minus' : 'plus'" class="w-4 h-4" />
</div>
</GeneralTooltip>
</button>
<button
class="color-selector"
:class="{ 'selected': compare(picked, color), 'new-design': isNewDesign }"
:style="{
backgroundColor: `${color}`,
border: colorBoxBorder ? `1px solid ${tinycolor(color).darken(30).toString()}` : undefined,
}"
@click="selectColor(color, true)"
>
{{ compare(picked, color) && !isNewDesign ? '&#10003;' : '' }}
</button>
</div>
<div
class="p-1 rounded-md h-8"
:class="{
'hover:bg-gray-200': isNewDesign,
}"
>
<button class="nc-more-colors-trigger h-6 w-6 border-1 border-gray-400 rounded" @click="isPickerOn = !isPickerOn">
<GeneralTooltip>
<template #title>{{ $t('activity.moreColors') }}</template>
<div class="flex items-center justify-center">
<GeneralIcon :icon="isPickerOn ? 'minus' : 'plus'" class="w-4 h-4" />
</div>
</GeneralTooltip>
</button>
</div>
</div>
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false">
<a-card
v-if="props.advanced"
class="w-full mt-2"
:body-style="{ paddingLeft: '4px !important', paddingRight: '4px !important' }"
:bordered="false"
>
<div v-if="isPickerOn" class="flex justify-center">
<LazyGeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div>
@ -82,49 +111,30 @@ watch(picked, (n, _o) => {
</div>
</template>
<style scoped>
<style lansg="scss" scoped>
.color-picker {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: white;
padding: 10px;
@apply flex flex-col items-center justify-center bg-white p-2.5;
}
.color-picker-row {
display: flex;
flex-direction: row;
@apply flex flex-row space-x-1;
}
.color-selector {
position: relative;
height: 25px;
width: 25px;
margin: 10px 5px;
border-radius: 5px;
@apply h-6 w-6 rounded;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
.color-selector:not(.new-design):hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector.new-design:hover {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
.color-selector.selected:not(.new-design) {
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector:focus.new-design {
.color-selector:focus,
.color-selector.selected,
.nc-more-colors-trigger:focus {
outline: none;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
.color-selector.selected.new-design {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
:deep(.vc-chrome-toggle-icon) {
@apply ml-3!important;
@apply !ml-3;
}
</style>

61
packages/nc-gui/components/general/ColorSliderWrapper.vue

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { Slider } from '@ckpack/vue-color'
import tinycolor from 'tinycolor2'
interface Props {
modelValue?: any
mode?: 'hsl' | 'hsv'
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '#3069FE',
mode: 'hsv',
})
const emit = defineEmits(['update:modelValue', 'input'])
const picked = computed({
get: () => tinycolor(props.modelValue || '#3069FE').toHsv() as any,
set: (val) => {
if (val) {
emit('update:modelValue', val[props.mode] || null)
emit('input', val[props.mode] || null)
}
},
})
</script>
<template>
<Slider
v-model="picked"
class="nc-color-slider-wrapper min-w-[200px]"
:style="{
'--nc-color-slider-pointer': tinycolor(`hsv(${picked.h ?? 199}, 100%, 100%)`).toHexString(),
}"
/>
</template>
<style lang="scss" scoped>
.nc-color-slider-wrapper {
&.vc-slider {
@apply !w-full;
}
:deep(.vc-slider-swatches) {
@apply hidden;
}
:deep(.vc-slider-hue-warp) {
@apply h-1.5;
.vc-hue {
@apply rounded-lg;
}
.vc-hue-pointer {
top: -3px !important;
}
.vc-hue-picker {
background-color: white;
box-shadow: 0 0 0 3px var(--nc-color-slider-pointer) !important;
}
}
}
</style>

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

@ -1,18 +1,62 @@
<script lang="ts" setup>
const { hoverable } = defineProps<{
type?: string
hoverable?: boolean
}>()
import tinycolor from 'tinycolor2'
import { baseIconColors } from '#imports'
const props = withDefaults(
defineProps<{
type?: string
hoverable?: boolean
color?: string
}>(),
{
color: baseIconColors[0],
},
)
const { color } = toRefs(props)
const iconColor = computed(() => {
return color.value && tinycolor(color.value).isValid()
? {
tint: baseIconColors.includes(color.value) ? color.value : tinycolor(color.value).lighten(10).toHexString(),
shade: tinycolor(color.value).darken(40).toHexString(),
}
: {
tint: baseIconColors[0],
shade: tinycolor(baseIconColors[0]).darken(40).toHexString(),
}
})
</script>
<template>
<GeneralIcon
icon="project"
<svg
width="16"
height="16"
viewBox="0 0 1073 1073"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="text-[#2824FB] base"
:class="{
'nc-base-icon-hoverable': hoverable,
}"
/>
>
<mask id="mask0_1749_80944" style="mask-type: luminance" maskUnits="userSpaceOnUse" x="94" y="40" width="885" height="993">
<path d="M978.723 40H94V1033H978.723V40Z" fill="white" />
</mask>
<g mask="url(#mask0_1749_80944)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M638.951 291.265L936.342 462.949C966.129 480.145 980.256 502.958 978.723 525.482V774.266C980.256 796.789 966.129 819.602 936.342 836.798L638.951 1008.48C582.292 1041.19 490.431 1041.19 433.773 1008.48L136.381 836.798C106.595 819.602 92.4675 796.789 93.9999 774.266L93.9999 525.482C92.4675 502.957 106.595 480.145 136.381 462.949L433.773 291.265C490.431 258.556 582.292 258.556 638.951 291.265Z"
:fill="iconColor.shade"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M638.951 65.0055L936.342 236.69C966.129 253.886 980.256 276.699 978.723 299.222V548.006C980.256 570.529 966.129 593.343 936.342 610.538L638.951 782.223C582.292 814.931 490.431 814.931 433.773 782.223L136.381 610.538C106.595 593.343 92.4675 570.529 93.9999 548.006L93.9999 299.222C92.4675 276.699 106.595 253.886 136.381 236.69L433.773 65.0055C490.431 32.2968 582.292 32.2968 638.951 65.0055Z"
:fill="iconColor.tint"
/>
</g>
</svg>
</template>
<style scoped>

23
packages/nc-gui/components/monaco/Editor.vue

@ -12,22 +12,33 @@ interface Props {
validate?: boolean
disableDeepCompare?: boolean
readOnly?: boolean
autoFocus?: boolean
}
const { hideMinimap, lang = 'json', validate = true, disableDeepCompare = false, modelValue, readOnly } = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
lang: 'json',
validate: true,
disableDeepCompare: false,
autoFocus: true,
})
const emits = defineEmits(['update:modelValue'])
const { modelValue } = toRefs(props)
const { hideMinimap, lang, validate, disableDeepCompare, readOnly, autoFocus } = props
const vModel = computed<string>({
get: () => {
if (typeof modelValue === 'object') {
return JSON.stringify(modelValue, null, 2)
if (typeof modelValue.value === 'object') {
return JSON.stringify(modelValue.value, null, 2)
} else {
return modelValue
return modelValue.value ?? ''
}
},
set: (newVal: string | Record<string, any>) => {
if (typeof modelValue === 'object') {
if (typeof modelValue.value === 'object') {
try {
emits('update:modelValue', typeof newVal === 'object' ? newVal : JSON.parse(newVal))
} catch (e) {
@ -120,7 +131,7 @@ onMounted(async () => {
}
})
if (!isDrawerOrModalExist()) {
if (!isDrawerOrModalExist() && autoFocus) {
// auto focus on json cells only
editor.focus()
}

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
color: string
color?: string
border?: boolean
size?: 'sm' | 'md' | 'lg'
}>(),

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

@ -2,6 +2,7 @@
import dayjs from 'dayjs'
interface Props {
size?: 'medium' | 'large' | 'small'
selectedDate?: dayjs.Dayjs | null
isDisabled?: boolean
pageDate?: dayjs.Dayjs
@ -16,6 +17,7 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
size: 'large',
selectedDate: null,
isDisabled: false,
isMondayFirst: true,
@ -135,7 +137,13 @@ const paginate = (action: 'next' | 'prev') => {
</script>
<template>
<div class="px-4 pt-3 pb-4 flex flex-col gap-4">
<div
:class="{
'gap-1': size === 'small',
'gap-4': size === 'medium' || size === 'large',
}"
class="flex flex-col"
>
<div
:class="{
'justify-center': disablePagination,
@ -152,7 +160,14 @@ const paginate = (action: 'next' | 'prev') => {
</template>
</NcTooltip>
<span class="font-bold text-gray-700">{{ currentMonthYear }}</span>
<span
:class="{
'text-xs': size === 'small',
'text-sm': size === 'medium',
}"
class="text-gray-700"
>{{ currentMonthYear }}</span
>
<NcTooltip>
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
@ -162,13 +177,41 @@ const paginate = (action: 'next' | 'prev') => {
</template>
</NcTooltip>
</div>
<div class="border-1 border-gray-200 rounded-y-xl max-w-[320px]">
<div class="flex flex-row bg-gray-100 gap-2 rounded-t-xl justify-between px-2">
<span v-for="(day, index) in days" :key="index" class="h-9 flex items-center justify-center w-9 text-gray-500">{{
day
}}</span>
<div
:class="{
'rounded-lg': size === 'small',
'rounded-y-xl': size !== 'small',
}"
class="border-1 border-gray-200 max-w-[320px]"
>
<div
:class="{
'gap-1 px-1': size === 'medium',
'gap-2 px-2': size === 'large',
'px-2 py-1 !rounded-t-lg': size === 'small',
'rounded-t-xl': size !== 'small',
}"
class="flex flex-row bg-gray-100 justify-between"
>
<span
v-for="(day, index) in days"
:key="index"
:class="{
'w-9 h-9': size === 'large',
'w-8 h-8': size === 'medium',
'text-[10px]': size === 'small',
}"
class="flex items-center uppercase justify-center text-gray-500"
>{{ day[0] }}</span
>
</div>
<div class="grid grid-cols-7 gap-2 p-2">
<div
:class="{
'gap-2 p-2': size === 'large',
'gap-1 p-1': size === 'medium',
}"
class="grid grid-cols-7"
>
<span
v-for="(date, index) in dates"
:key="index"
@ -177,16 +220,31 @@ const paginate = (action: 'next' | 'prev') => {
'bg-brand-50 border-1 !border-brand-500': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !isWeekPicker,
'nc-selected-week z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker,
'border-transparent': !isWeekPicker,
'text-gray-400': !isDateInCurrentMonth(date),
'nc-selected-week-start': isSameDate(date, selectedWeek?.start),
'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
'rounded-md bg-brand-50 nc-calendar-today text-brand-500': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'h-9 w-9': size === 'large',
'h-8 w-8': size === 'medium',
'h-6 w-6 text-[10px]': size === 'small',
}"
class="h-9 w-9 px-1 py-2 relative font-medium flex items-center cursor-pointer justify-center"
class="px-1 py-1 relative border-1 font-large flex items-center cursor-pointer justify-center"
data-testid="nc-calendar-date"
@click="handleSelectDate(date)"
>
<span v-if="isActiveDate(date)" class="absolute z-2 h-1.5 w-1.5 rounded-full bg-brand-500 top-1 right-1"></span>
<span
v-if="isActiveDate(date)"
:class="{
'h-1.5 w-1.5': size === 'large',
'h-1 w-1': size === 'medium',
'h-0.75 w-0.75': size === 'small',
'top-1 right-1': size !== 'small',
'top-0.5 right-0.5': size === 'small',
}"
class="absolute z-2 rounded-full bg-brand-500"
></span>
<span class="z-2">
{{ date.get('date') }}
</span>

9
packages/nc-gui/components/nc/ErrorBoundary.vue

@ -66,7 +66,14 @@ export default {
<template>
<slot :key="key"></slot>
<slot name="error">
<NcModal v-model:visible="errModal" :class="{ active: errModal }" :centered="true" :closable="false" :footer="null">
<NcModal
v-if="error"
v-model:visible="errModal"
:class="{ active: errModal }"
:centered="true"
:closable="false"
:footer="null"
>
<div class="w-full flex flex-col gap-1">
<h2 class="text-xl font-semibold">Oops! Something unexpected happened :/</h2>

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

@ -6,19 +6,21 @@ const props = withDefaults(
size?: 'small' | 'medium' | 'large'
destroyOnClose?: boolean
maskClosable?: boolean
showSeparator?: boolean
wrapClassName?: string
}>(),
{
size: 'medium',
destroyOnClose: true,
maskClosable: true,
showSeparator: true,
wrapClassName: '',
},
)
const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName } = props
const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName, showSeparator } = props
const { isMobileMode } = useGlobal()
@ -98,7 +100,13 @@ const slots = useSlots()
maxHeight: height,
}"
>
<div v-if="slots.header" class="flex pb-2 mb-2 text-lg font-medium border-b-1 border-gray-100">
<div
v-if="slots.header"
:class="{
'border-b-1 border-gray-100': showSeparator,
}"
class="flex pb-2 mb-2 text-lg font-medium"
>
<slot name="header" />
</div>

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

@ -95,7 +95,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<span>{{ $t('labels.previous') }}</span>
</template>
</NcTooltip>
<span class="font-bold text-gray-700">{{ isYearPicker ? $t('labels.selectYear') : pageDate.year() }}</span>
<span class="text-gray-700">{{ isYearPicker ? $t('labels.selectYear') : pageDate.year() }}</span>
<NcTooltip>
<NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
@ -114,7 +114,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
:class="{
'!bg-brand-50 border-1 !border-brand-500': isMonthSelected(month),
}"
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-500 cursor-pointer"
class="h-9 rounded-lg flex items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = month"
>
{{ month.format('MMM') }}
@ -127,7 +127,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
:class="{
'!bg-brand-50 !border-1 !border-brand-500': compareYear(year, selectedDate),
}"
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-500 cursor-pointer"
class="h-9 rounded-lg flex items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = year"
>
{{ year.format('YYYY') }}

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

@ -1,5 +1,7 @@
<script lang="ts" setup>
const props = defineProps<{ checked: boolean; disabled?: boolean }>()
const props = withDefaults(defineProps<{ checked: boolean; disabled?: boolean; size?: 'default' | 'small' }>(), {
size: 'small',
})
const emit = defineEmits(['change', 'update:checked'])
@ -11,7 +13,8 @@ const onChange = (e: boolean) => {
</script>
<template>
<a-switch v-model:checked="checked" :disabled="props.disabled" class="nc-switch" size="small" @change="onChange"> </a-switch>
<a-switch v-model:checked="checked" :disabled="disabled" class="nc-switch" v-bind="$attrs" :size="size" @change="onChange">
</a-switch>
<span v-if="$slots.default" class="cursor-pointer pl-2" @click="checked = !checked">
<slot />
</span>

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

@ -9,8 +9,8 @@ import {
timeAgo,
} from 'nocodb-sdk'
import type { Roles, WorkspaceUserRoles } from 'nocodb-sdk'
import { isEeUI, storeToRefs, useUserSorts } from '#imports'
import type { User } from '#imports'
import { isEeUI, storeToRefs, useUserSorts } from '#imports'
const basesStore = useBases()
const { getBaseUsers, createProjectUser, updateProjectUser, removeProjectUser } = basesStore
@ -22,6 +22,8 @@ const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = u
const isSuper = computed(() => orgRoles.value?.[OrgUserRoles.SUPER_ADMIN])
const isInviteModalVisible = ref(false)
interface Collaborators {
id: string
email: string
@ -132,20 +134,34 @@ onMounted(async () => {
isLoading.value = false
}
})
watch(isInviteModalVisible, () => {
if (!isInviteModalVisible.value) {
loadCollaborators()
}
})
</script>
<template>
<div class="nc-collaborator-table-container mt-4 nc-access-settings-view h-[calc(100vh-8rem)]">
<LazyProjectShareBaseDlg v-model:model-value="isInviteModalVisible" />
<div v-if="isLoading" class="nc-collaborators-list items-center justify-center">
<GeneralLoader size="xlarge" />
</div>
<template v-else>
<div class="w-full flex flex-row justify-between items-baseline mt-6.5 mb-2 pr-0.25">
<div class="w-full flex flex-row justify-between items-baseline max-w-350 mt-6.5 mb-2 pr-0.25">
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md" :placeholder="$t('title.searchMembers')">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
</a-input>
<NcButton size="small" @click="isInviteModalVisible = true">
<div class="flex gap-1">
<component :is="iconMap.plus" class="w-4 h-4" />
{{ $t('activity.addMembers') }}
</div>
</NcButton>
</div>
<div v-if="isSearching" class="nc-collaborators-list items-center justify-center">
@ -156,7 +172,7 @@ onMounted(async () => {
v-else-if="!filteredCollaborators?.length"
class="nc-collaborators-list w-full h-full flex flex-col items-center justify-center mt-36"
>
<Empty description="$t('title.noMembersFound')" />
<a-empty description="$t('title.noMembersFound')" />
</div>
<div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
@ -208,7 +224,7 @@ onMounted(async () => {
: null
"
:description="false"
:on-role-change="(role: ProjectRoles) => updateCollaborator(collab, role)"
:on-role-change="(role) => updateCollaborator(collab, role as ProjectRoles)"
/>
</template>
<template v-else>

299
packages/nc-gui/components/project/ShareBaseDlg.vue

@ -0,0 +1,299 @@
<script setup lang="ts">
import type { RoleLabels } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles } from 'nocodb-sdk'
import type { User } from '#imports'
const props = defineProps<{
modelValue: boolean
baseId?: string
}>()
const emit = defineEmits(['update:modelValue'])
const dialogShow = useVModel(props, 'modelValue', emit)
const inviteData = reactive({
email: '',
roles: ProjectRoles.NO_ACCESS,
})
const { baseRoles } = useRoles()
const basesStore = useBases()
const { activeProjectId } = storeToRefs(basesStore)
const { createProjectUser } = basesStore
const divRef = ref<HTMLDivElement>()
const focusRef = ref<HTMLInputElement>()
const isDivFocused = ref(false)
const emailValidation = reactive({
isError: true,
message: '',
})
const allowedRoles = ref<ProjectRoles[]>([])
onMounted(async () => {
try {
const currentRoleIndex = OrderedProjectRoles.findIndex(
(role) => baseRoles.value && Object.keys(baseRoles.value).includes(role),
)
if (currentRoleIndex !== -1) {
allowedRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1).filter((r) => r)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
})
const singleEmailValue = ref('')
const emailBadges = ref<Array<string>>([])
const insertOrUpdateString = (str: string) => {
// Check if the string already exists in the array
const index = emailBadges.value.indexOf(str)
if (index !== -1) {
// If the string exists, remove it
emailBadges.value.splice(index, 1)
}
// Add the new string to the array
emailBadges.value.push(str)
}
const emailInputValidation = (input: string): boolean => {
if (!input.length) {
emailValidation.isError = true
emailValidation.message = 'Email should not be empty'
return false
}
if (!validateEmail(input.trim())) {
emailValidation.isError = true
emailValidation.message = 'Invalid Email'
return false
}
return true
}
const isInvitButtonDiabled = computed(() => {
if (!emailBadges.value.length && !singleEmailValue.value.length) {
return true
}
if (emailBadges.value.length && inviteData.email) {
return true
}
})
watch(inviteData, (newVal) => {
// when user only want to enter a single email
// we dont convert that as badge
const isSingleEmailValid = validateEmail(newVal.email)
if (isSingleEmailValid && !emailBadges.value.length) {
singleEmailValue.value = newVal.email
emailValidation.isError = false
return
}
singleEmailValue.value = ''
// when user enters multiple emails comma sepearted or space sepearted
const isNewEmail = newVal.email.charAt(newVal.email.length - 1) === ',' || newVal.email.charAt(newVal.email.length - 1) === ' '
if (isNewEmail && newVal.email.trim().length) {
const emailToAdd = newVal.email.split(',')[0].trim() || newVal.email.split(' ')[0].trim()
if (!validateEmail(emailToAdd)) {
emailValidation.isError = true
emailValidation.message = 'Invalid Email'
return
}
/**
if email is already enterd we delete the already
existing email and add new one
**/
if (emailBadges.value.includes(emailToAdd)) {
insertOrUpdateString(emailToAdd)
inviteData.email = ''
return
}
emailBadges.value.push(emailToAdd)
inviteData.email = ''
singleEmailValue.value = ''
}
if (!newVal.email.length && emailValidation.isError) {
emailValidation.isError = false
}
})
const handleEnter = () => {
const isEmailIsValid = emailInputValidation(inviteData.email)
if (!isEmailIsValid) return
inviteData.email += ' '
emailValidation.isError = false
emailValidation.message = ''
}
const focusOnDiv = () => {
focusRef.value?.focus()
isDivFocused.value = true
}
// remove one email per backspace
onKeyStroke('Backspace', () => {
if (isDivFocused.value && inviteData.email.length < 1) {
emailBadges.value.pop()
}
})
watch(dialogShow, (newVal) => {
if (newVal) {
setTimeout(() => {
focusOnDiv()
}, 100)
}
})
// when bulk email is pasted
const onPaste = (e: ClipboardEvent) => {
const pastedText = e.clipboardData?.getData('text')
const inputArray = pastedText?.split(',') || pastedText?.split(' ')
// if data is pasted to a already existing text in input
// we add existingInput + pasted data
if (inputArray?.length === 1 && inviteData.email.length) {
inputArray[0] = inviteData.email += inputArray[0]
}
inputArray?.forEach((el) => {
const isEmailIsValid = emailInputValidation(el)
if (!isEmailIsValid) return
/**
if email is already enterd we delete the already
existing email and add new one
**/
if (emailBadges.value.includes(el)) {
insertOrUpdateString(el)
return
}
emailBadges.value.push(el)
inviteData.email = ''
})
inviteData.email = ''
}
const inviteProjectCollaborator = async () => {
try {
const payloadData = singleEmailValue.value || emailBadges.value.join(',')
if (!payloadData.includes(',')) {
const validationStatus = validateEmail(payloadData)
if (!validationStatus) {
emailValidation.isError = true
emailValidation.message = 'invalid email'
}
}
await createProjectUser(activeProjectId.value!, {
email: payloadData,
roles: inviteData.roles,
} as unknown as User)
message.success('Invitation sent successfully')
inviteData.email = ''
emailBadges.value = []
dialogShow.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
singleEmailValue.value = ''
}
}
const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles)
</script>
<template>
<NcModal
v-model:visible="dialogShow"
:show-separator="false"
:header="$t('activity.createTable')"
size="medium"
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
{{ $t('activity.addMember') }}
</div>
</template>
<div class="flex items-center justify-between gap-3 mt-2">
<div class="flex w-full flex-col">
<div class="flex justify-between gap-3 w-full">
<div
ref="divRef"
class="flex items-center border-1 gap-1 w-full overflow-x-scroll nc-scrollbar-x-md items-center h-10 rounded-lg !min-w-96"
tabindex="0"
:class="{
'border-primary/100': isDivFocused,
'p-1': emailBadges?.length > 1,
}"
@click="focusOnDiv"
@blur="isDivFocused = false"
>
<span
v-for="(email, index) in emailBadges"
:key="email"
class="border-1 text-gray-800 bg-gray-100 rounded-md flex items-center px-2 py-1"
>
{{ email }}
<component
:is="iconMap.close"
class="ml-0.5 hover:cursor-pointer mt-0.5 w-4 h-4"
@click="emailBadges.splice(index, 1)"
/>
</span>
<input
id="email"
ref="focusRef"
v-model="inviteData.email"
:placeholder="$t('activity.enterEmail')"
class="w-full min-w-36 outline-none px-2"
data-testid="email-input"
@keyup.enter="handleEnter"
@blur="isDivFocused = false"
@paste.prevent="onPaste"
/>
</div>
<RolesSelector
size="lg"
class="nc-invite-role-selector"
:role="inviteData.roles"
:roles="allowedRoles"
:on-role-change="onRoleChange"
:description="false"
/>
</div>
<span v-if="emailValidation.isError && emailValidation.message" class="ml-2 text-red-500 text-[10px] mt-1.5">{{
emailValidation.message
}}</span>
</div>
</div>
<div class="flex mt-8 justify-end">
<div class="flex gap-2">
<NcButton type="secondary" @click="dialogShow = false"> {{ $t('labels.cancel') }} </NcButton>
<NcButton
type="primary"
size="medium"
:disabled="isInvitButtonDiabled || emailValidation.isError"
@click="inviteProjectCollaborator"
>
{{ $t('activity.inviteToBase') }}
</NcButton>
</div>
</div>
</NcModal>
</template>

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

@ -82,7 +82,7 @@ watch(
<div class="flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5">
<GeneralProjectIcon :type="openedProject?.type" />
<GeneralProjectIcon :type="openedProject?.type" :color="parseProp(openedProject?.meta).iconColor" />
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only>
<template #title> {{ openedProject?.title }}</template>
<span class="truncate">
@ -140,7 +140,7 @@ watch(
</template>
<ProjectAccessSettings />
</a-tab-pane>
<a-tab-pane v-if="isUIAllowed('baseCreate')" key="data-source">
<a-tab-pane v-if="isUIAllowed('sourceCreate')" key="data-source">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" />

2
packages/nc-gui/components/roles/Badge.vue

@ -8,7 +8,7 @@ const props = withDefaults(
clickable?: boolean
inherit?: boolean
border?: boolean
size?: 'sm' | 'md'
size?: 'sm' | 'md' | 'lg'
}>(),
{
clickable: false,

4
packages/nc-gui/components/roles/Selector.vue

@ -11,7 +11,7 @@ const props = withDefaults(
description?: boolean
inherit?: string
onRoleChange: (role: keyof typeof RoleLabels) => void
size: 'sm' | 'md'
size?: 'sm' | 'md' | 'lg'
}>(),
{
description: true,
@ -38,7 +38,7 @@ function onChangeRole(val: SelectValue) {
<div ref="dropdownRef" size="lg" class="nc-roles-selector relative" @click="isDropdownOpen = !isDropdownOpen">
<RolesBadge data-testid="roles" :role="roleRef" :inherit="inheritRef === role" :size="sizeRef" clickable />
<a-select
v-model:value="roleRef"
:value="roleRef"
:open="isDropdownOpen"
:dropdown-match-select-width="false"
dropdown-class-name="!rounded-lg !h-fit max-w-[350px] nc-role-selector-dropdown"

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

@ -29,6 +29,7 @@ import {
isJSON,
isManualSaved,
isMultiSelect,
isNumericFieldType,
isPercent,
isPhoneNumber,
isPrimary,
@ -142,14 +143,7 @@ const navigate = (dir: NavigateDir, e: KeyboardEvent) => {
}
const isNumericField = computed(() => {
return (
isInt(column.value, abstractType.value) ||
isFloat(column.value, abstractType.value) ||
isDecimal(column.value) ||
isCurrency(column.value) ||
isPercent(column.value) ||
isDuration(column.value)
)
return isNumericFieldType(column.value, abstractType.value)
})
// disable contexxtmenu event propagation when cell is in

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

@ -31,7 +31,6 @@ import {
onClickOutside,
parseProp,
provide,
reactive,
ref,
useDebounceFn,
useEventListener,
@ -42,7 +41,7 @@ import {
useRoles,
useViewColumnsOrThrow,
useViewData,
watch,
useViewsStore,
} from '#imports'
import type { ImageCropperConfig } from '~/lib'
@ -72,8 +71,6 @@ const enum NcForm {
const { isMobileMode, user } = useGlobal()
const formRef = ref()
const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
@ -82,7 +79,9 @@ const { base } = storeToRefs(useBase())
const { getPossibleAttachmentSrc } = useAttachment()
let formState = reactive<Record<string, any>>({})
const formRef = ref()
const formState = ref<Record<string, any>>({})
const secondsRemain = ref(0)
@ -98,6 +97,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view)
const { preFillFormSearchParams } = storeToRefs(useViewsStore())
const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook())
reloadEventHook.on(async () => {
@ -110,7 +111,7 @@ const { fields, showAll, hideAll } = useViewColumnsOrThrow()
const { state, row } = useProvideSmartsheetRowStore(
meta,
ref({
row: formState,
row: formState.value,
oldRow: {},
rowMeta: { new: true },
}),
@ -193,6 +194,22 @@ const updateView = useDebounceFn(
{ maxWait: 2000 },
)
const updatePreFillFormSearchParams = useDebounceFn(() => {
if (isLocked.value || !isUIAllowed('dataInsert')) return
const preFilledData = { ...formState.value, ...state.value }
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])
}
}
preFillFormSearchParams.value = searchParams.toString()
}, 250)
async function submitForm() {
if (isLocked.value || !isUIAllowed('dataInsert')) return
@ -206,7 +223,7 @@ async function submitForm() {
}
await insertRow({
row: { ...formState, ...state.value },
row: { ...formState.value, ...state.value },
oldRow: {},
rowMeta: { new: true },
})
@ -217,9 +234,9 @@ async function submitForm() {
async function clearForm() {
if (isLocked.value || !isUIAllowed('dataInsert')) return
formState = reactive<Record<string, any>>({})
formState.value = {}
state.value = {}
await formRef.value.clearValidate()
await formRef.value?.clearValidate()
reloadEventHook.trigger()
}
@ -321,13 +338,6 @@ async function showOrHideColumn(column: Record<string, any>, show: boolean, isSi
await $api.dbView.formColumnUpdate(column.id, column)
fields.value[fieldIndex] = column as any
// await saveOrUpdate(
// {
// ...column,
// show,
// },
// fieldIndex,
// )
reloadEventHook.trigger()
@ -446,7 +456,7 @@ async function submitCallback() {
}
const updateColMeta = useDebounceFn(async (col: Record<string, any>) => {
if (col.id) {
if (col.id && isEditable) {
try {
await $api.dbView.formColumnUpdate(col.id, col)
} catch (e: any) {
@ -554,6 +564,8 @@ onMounted(async () => {
URL.revokeObjectURL(imageCropperData.value.imageConfig.src)
}
preFillFormSearchParams.value = ''
isLoadingFormView.value = true
await loadFormView()
setFormData()
@ -566,6 +578,7 @@ watch(submitted, (v) => {
const intvl = setInterval(() => {
if (--secondsRemain.value < 0) {
submitted.value = false
clearForm()
clearInterval(intvl)
}
}, 1000)
@ -578,25 +591,35 @@ watch(view, (nextView) => {
}
})
watch([formState, state.value], async () => {
for (const virtualField in state.value) {
if (!formState[virtualField]) {
formState[virtualField] = state.value[virtualField]
watch(
[formState, state],
async () => {
for (const virtualField in state.value) {
formState.value[virtualField] = state.value[virtualField]
}
}
try {
await formRef.value?.validateFields([...Object.keys(formState)])
} catch (e: any) {
e.errorFields.map((f: Record<string, any>) => console.error(f.errors.join(',')))
}
})
updatePreFillFormSearchParams()
try {
await formRef.value?.validateFields([...Object.keys(formState.value)])
} catch (e: any) {
e.errorFields.map((f: Record<string, any>) => console.error(f.errors.join(',')))
}
},
{
deep: true,
},
)
watch(activeRow, (newValue) => {
if (newValue) {
document
.querySelector(`.nc-form-field-item-${newValue?.replaceAll(' ', '')}`)
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
const field = document.querySelector(`.nc-form-field-item-${CSS.escape(newValue?.replaceAll(' ', ''))}`)
if (field) {
setTimeout(() => {
field?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 50)
}
}
})
@ -647,7 +670,7 @@ useEventListener(
(e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
switch (e.key.toLowerCase()) {
switch (e.key?.toLowerCase()) {
case 's':
if (
cmdOrCtrl &&
@ -727,18 +750,29 @@ useEventListener(
</template>
</a-alert>
<div v-if="formViewData.show_blank_form" class="text-gray-400 mt-4">
{{
$t('msg.newFormWillBeLoaded', {
seconds: secondsRemain,
})
}}
</div>
<div class="mt-16 w-full flex justify-between items-center gap-3">
<div v-if="formViewData.show_blank_form" class="text-gray-400">
{{
$t('msg.newFormWillBeLoaded', {
seconds: secondsRemain,
})
}}
</div>
<div v-if="formViewData.submit_another_form || !isPublic" class="text-right mt-4">
<NcButton type="primary" size="medium" @click="submitted = false">
{{ $t('activity.submitAnotherForm') }}
</NcButton>
<div v-if="formViewData.submit_another_form || !isPublic" class="flex-1 flex justify-end">
<NcButton
type="primary"
size="small"
@click="
() => {
submitted = false
clearForm()
}
"
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
</div>
</div>
</div>
</div>
@ -1056,7 +1090,10 @@ useEventListener(
</div>
<!-- Field Header -->
<div v-if="activeRow === element.title" class="w-full flex gap-3 items-center px-3 py-2 bg-gray-50">
<div
v-if="activeRow === element.title"
class="w-full flex gap-3 items-center px-3 py-2 bg-gray-50 border-b-1 border-gray-200"
>
<component
:is="iconMap.drag"
class="nc-form-field-drag-handler flex-none cursor-move !h-4 !w-4 text-gray-600"
@ -1209,7 +1246,7 @@ useEventListener(
<!-- Field Settings -->
<div
v-if="activeRow === element.title && isSelectTypeCol(element.uidt)"
class="nc-form-field-settings border-t border-gray-200 p-4 lg:p-6 flex flex-col gap-3"
class="nc-form-field-settings border-t border-gray-200 p-4 lg:p-6 flex flex-col gap-3 bg-gray-50"
>
<!-- Layout -->
<div v-if="isSelectTypeCol(element.uidt)">
@ -1231,7 +1268,7 @@ useEventListener(
</div>
<!-- Todo: Show on conditions,... -->
<!-- eslint-disable vue/no-constant-condition -->
<div v-if="false" class="flex items-start gap-3 px-3 py-2 border-1 border-gray-200 rounded-lg">
<div v-if="false" class="flex items-start gap-3 px-3 py-2 border-1 border-gray-200 rounded-lg bg-white">
<a-switch v-e="['a:form-view:field:show-on-condition']" size="small" class="nc-form-switch-focus" />
<div>
<div class="font-medium text-gray-800">{{ $t('labels.showOnConditions') }}</div>
@ -1240,7 +1277,10 @@ useEventListener(
</div>
<!-- Limit options -->
<div v-if="isSelectTypeCol(element.uidt)" class="px-3 py-2 border-1 border-gray-200 rounded-lg">
<div
v-if="isSelectTypeCol(element.uidt)"
class="px-3 py-2 border-1 border-gray-200 rounded-lg bg-white"
>
<div class="flex items-center gap-3">
<a-switch
v-model:checked="element.meta.isLimitOption"
@ -1289,6 +1329,7 @@ useEventListener(
>
{{ $t('activity.clearForm') }}
</NcButton>
<NcButton
html-type="submit"
type="primary"
@ -1433,7 +1474,7 @@ useEventListener(
<SmartsheetHeaderCellIcon v-else :column-meta="field" />
<div class="flex-1 flex items-center justify-start max-w-[calc(100%_-_68px)] mr-4">
<div class="w-full flex items-center">
<div class="ml-1 inline-block max-w-1/2">
<div class="ml-1 inline-flex" :class="field.label?.trim() ? 'max-w-1/2' : 'max-w-[95%]'">
<NcTooltip class="truncate text-sm" :disabled="drag" show-on-truncate-only>
<template #title>
<div class="text-center">
@ -1502,19 +1543,9 @@ useEventListener(
'#E5D4F5',
'#FFCFE6',
]"
:borders="[
'#6A7184',
'#FF4A3F',
'#FA8231',
'#FCBE3A',
'#27D665',
'#36BFFF',
'#FC3AC6',
'#7D26CD',
'#B33771',
]"
:is-new-design="true"
class="nc-form-theme-color-picker !p-0 !-ml-1"
color-box-border
is-new-design
class="nc-form-theme-color-picker !pb-0 !pl-0 -ml-1"
@input="handleChangeBackground"
/>
</div>
@ -1792,21 +1823,21 @@ useEventListener(
.nc-form-field-ghost {
@apply bg-gray-50;
}
:deep(.nc-form-input-required + button):focus {
:deep(.nc-form-input-required + button):focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
:deep(.nc-form-switch-focus):focus {
:deep(.nc-form-switch-focus):focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
.nc-form-field-layout {
@apply !flex !items-center w-full space-x-3;
:deep(.ant-radio-wrapper) {
@apply border-1 border-gray-200 rounded-lg !py-2 !px-3 basis-full !mr-0 !items-center;
@apply border-1 border-gray-200 rounded-lg !py-2 !px-3 basis-full !mr-0 !items-center bg-white;
.ant-radio {
@apply !top-0;
&:focus-within .ant-radio-inner {
.ant-radio-input:focus-visible + .ant-radio-inner {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
}
@ -1814,12 +1845,9 @@ useEventListener(
}
.nc-form-wrapper {
.ant-switch:focus,
.ant-switch-checked:focus {
.ant-switch:focus-visible,
.ant-switch-checked:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
&:hover {
box-shadow: none;
}
}
}
</style>

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

@ -285,7 +285,7 @@ watch(
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-cover"
class="h-52 !object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
@click="expandFormClick($event, record)"
/>
@ -372,7 +372,7 @@ watch(
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
v-if="expandedFormOnRowIdDlg && meta?.id"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"

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

@ -753,7 +753,7 @@ const getRowId = (row: RowType) => {
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
v-if="expandedFormOnRowIdDlg && meta?.id"
:key="route.query.rowId"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"

2
packages/nc-gui/components/smartsheet/Map.vue

@ -245,7 +245,7 @@ const count = computed(() => paginationData.value.totalRows)
</Suspense>
<Suspense v-if="!isPublic">
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
v-if="expandedFormOnRowIdDlg && meta?.id"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"

14
packages/nc-gui/components/smartsheet/calendar/Cell.vue

@ -356,6 +356,7 @@ const parseValue = (value: any, col: ColumnType): string => {
<template>
<span
class="calendar-cell text-xs before:px-1"
:class="{
'font-bold': bold,
'italic': italic,
@ -367,4 +368,15 @@ const parseValue = (value: any, col: ColumnType): string => {
</span>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.calendar-cell {
&::before {
content: '•';
padding: 0 4px;
}
&:first-child::before {
content: '';
padding: 0;
}
}
</style>

30
packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue

@ -4,7 +4,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { isRowEmpty } from '~/utils'
const emit = defineEmits(['expand-record', 'new-record'])
const emit = defineEmits(['expandRecord', 'newRecord'])
const meta = inject(MetaInj, ref())
@ -20,22 +20,22 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType) => {
const fi = _fields.value.find((f) => f.title === field.title)
const fi = _fields.value?.find((f) => f.title === field.title)
return {
underline: fi.underline,
bold: fi.bold,
italic: fi.italic,
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
}
const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
// We loop through all the records and calculate the position of each record based on the range
// We only need to calculate the top, of the record since there is no overlap in the day view of date Field
const recordsAcrossAllRange = computed<Row[]>(() => {
let dayRecordCount = 0
const perRecordHeight = 40
const perRecordHeight = 28
if (!calendarRange.value) return []
@ -183,6 +183,17 @@ const dropEvent = (event: DragEvent) => {
updateRowProperty(newRow, updateProperty, false)
}
}
// TODO: Add Support for multiple ranges when multiple ranges are supported
const newRecord = () => {
if (!isUIAllowed('dataEdit') || !calendarRange.value?.length) return
const record = {
row: {
[calendarRange.value[0].fk_from_col!.title!]: selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
}
</script>
<template>
@ -191,6 +202,7 @@ const dropEvent = (event: DragEvent) => {
ref="container"
class="w-full relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view"
@dblclick="newRecord"
@drop="dropEvent"
>
<div
@ -209,7 +221,7 @@ const dropEvent = (event: DragEvent) => {
:resize="false"
color="blue"
size="small"
@click="emit('expand-record', record)"
@click="emit('expandRecord', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
@ -223,6 +235,7 @@ const dropEvent = (event: DragEvent) => {
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
@ -240,6 +253,7 @@ const dropEvent = (event: DragEvent) => {
ref="container"
class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center"
@drop="dropEvent"
@dblclick="newRecord"
>
No records in this day
</div>

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

@ -4,7 +4,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['expandRecord', 'new-record'])
const emit = defineEmits(['expandRecord', 'newRecord'])
const {
// activeCalendarView,
@ -52,6 +52,146 @@ const hours = computed(() => {
return hours
})
const calculateNewDates = ({
endDate,
startDate,
scheduleStart,
scheduleEnd,
}: {
endDate: dayjs.Dayjs
startDate: dayjs.Dayjs
scheduleStart: dayjs.Dayjs
scheduleEnd: dayjs.Dayjs
}) => {
// If there is no end date, we add 15 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(15, 'minutes')
}
// If the start date is before the opened date, we use the schedule start as the start date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (startDate.isSameOrBefore(scheduleStart)) {
startDate = scheduleStart
}
// If the end date is after the schedule end, we use the schedule end as the end date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (endDate.isAfter(scheduleEnd)) {
endDate = scheduleEnd
}
return { endDate, startDate }
}
/* const getGridTime = (date: dayjs.Dayjs, round = false) => {
const gridCalc = date.hour() * 60 + date.minute()
if (round) {
return Math.ceil(gridCalc)
} else {
return Math.floor(gridCalc)
}
}
const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => {
return {
from: getGridTime(from, false),
to: getGridTime(to, true) - 1,
}
} */
/* const hasSlotForRecord = (
record: Row,
columnArray: Row[],
dates: {
fromDate: dayjs.Dayjs
toDate: dayjs.Dayjs
},
) => {
const { fromDate, toDate } = dates
if (!fromDate || !toDate) return false
for (const column of columnArray) {
const columnFromCol = column.rowMeta.range?.fk_from_col
const columnToCol = column.rowMeta.range?.fk_to_col
if (!columnFromCol) return false
const { startDate: columnFromDate, endDate: columnToDate } = calculateNewDates({
startDate: dayjs(column.row[columnFromCol.title!]),
endDate: columnToCol ? dayjs(column.row[columnToCol.title!]) : dayjs(column.row[columnFromCol.title!]).add(1, 'hour'),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
if (
fromDate.isBetween(columnFromDate, columnToDate, null, '[]') ||
toDate.isBetween(columnFromDate, columnToDate, null, '[]')
) {
return false
}
}
return true
} */
/* const getMaxOfGrid = (
{
fromDate,
toDate,
}: {
fromDate: dayjs.Dayjs
toDate: dayjs.Dayjs
},
gridTimeMap: Map<number, number>,
) => {
let max = 0
const gridTimes = getGridTimeSlots(fromDate, toDate)
for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
if (gridTimeMap.has(gridCounter) && gridTimeMap.get(gridCounter) > max) {
max = gridTimeMap.get(gridCounter)
}
}
return max
} */
/* const isOverlaps = (row1: Row, row2: Row) => {
const fromCol1 = row1.rowMeta.range?.fk_from_col
const toCol1 = row1.rowMeta.range?.fk_to_col
const fromCol2 = row2.rowMeta.range?.fk_from_col
const toCol2 = row2.rowMeta.range?.fk_to_col
if (!fromCol1 || !fromCol2) return false
const { startDate: startDate1, endDate: endDate1 } = calculateNewDates({
endDate: toCol1 ? dayjs(row1.row[toCol1.title!]) : dayjs(row1.row[fromCol1.title!]).add(1, 'hour'),
startDate: dayjs(row1.row[fromCol1.title!]),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
const { startDate: startDate2, endDate: endDate2 } = calculateNewDates({
endDate: toCol2 ? dayjs(row2.row[toCol2.title!]) : dayjs(row2.row[fromCol2.title!]).add(1, 'hour'),
startDate: dayjs(row2.row[fromCol2.title!]),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
return startDate1.isBetween(startDate2, endDate2, null, '[]') || endDate1.isBetween(startDate2, endDate2, null, '[]')
} */
/* const getMaxOverlaps = ({ row, rowArray }: { row: Row; rowArray: Row[] }) => {
let maxOverlaps = row.rowMeta.numberOfOverlaps
for (const record of rowArray) {
if (isOverlaps(row, record)) {
if (!record.rowMeta.numberOfOverlaps || !row.rowMeta.numberOfOverlaps) continue
if (record.rowMeta.numberOfOverlaps > row.rowMeta.numberOfOverlaps) {
maxOverlaps = record.rowMeta.numberOfOverlaps
}
}
}
return maxOverlaps
} */
const recordsAcrossAllRange = computed<{
record: Row[]
count: {
@ -82,6 +222,9 @@ const recordsAcrossAllRange = computed<{
const perRecordHeight = 80
/* const columnArray: Array<Array<Row>> = [[]]
const gridTimeMap = new Map() */
let recordsByRange: Array<Row> = []
calendarRange.value.forEach((range) => {
@ -90,48 +233,36 @@ const recordsAcrossAllRange = computed<{
// We fetch all the records that match the calendar ranges in a single time.
// But not all fetched records are valid for the certain range, so we filter them out & sort them
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
if (fromCol && endCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
return fromDate && toDate?.isValid() ? fromDate.isBefore(toDate) : true
} else if (fromCol && !endCol) {
return !!fromDate
}
return false
})
// If there is a start and end column, we calculate the top and height of the record based on the start and end date
if (fromCol && endCol) {
for (const record of sortedFormattedData) {
// We use this id during the drag and drop operation and to keep track of the number of records that overlap at a given time
const id = generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[endCol.title!])
const sortedFormattedData = [...formattedData.value]
.filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
// If no start date is provided or startDate is after the endDate, we skip the record
if (!startDate.isValid() || startDate.isAfter(endDate)) continue
if (fromCol && endCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
// If there is no end date, we add 30 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(30, 'minutes')
}
// If the start date is before the opened date, we use the schedule start as the start date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart
return fromDate && toDate?.isValid() ? fromDate.isSameOrBefore(toDate) : true
} else if (fromCol && !endCol) {
return !!fromDate
}
return false
})
.sort((a, b) => {
const aDate = dayjs(a.row[fromCol!.title!])
const bDate = dayjs(b.row[fromCol!.title!])
return aDate.isBefore(bDate) ? 1 : -1
})
// If the end date is after the schedule end, we use the schedule end as the end date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
for (const record of sortedFormattedData) {
const id = record.rowMeta.id ?? generateRandomNumber()
if (fromCol && endCol) {
const { endDate, startDate } = calculateNewDates({
endDate: dayjs(record.row[endCol.title!]),
startDate: dayjs(record.row[fromCol.title!]),
scheduleStart,
scheduleEnd,
})
// The top of the record is calculated based on the start hour and minute
const topInPixels = (startDate.hour() + startDate.minute() / 60) * 80
@ -140,7 +271,6 @@ const recordsAcrossAllRange = computed<{
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 80, perRecordHeight)
const startHour = startDate.hour()
let _startDate = startDate.clone()
const style: Partial<CSSStyleDeclaration> = {
@ -148,7 +278,7 @@ const recordsAcrossAllRange = computed<{
top: `${topInPixels + 5 + startHour * 2}px`,
}
// We loop through every 15 minutes between the start and end date and keep track of the number of records that overlap at a given time
// We loop through every 1 minutes between the start and end date and keep track of the number of records that overlap at a given time
// If the number of records exceeds 4, we hide the record and show a button to view more records
while (_startDate.isBefore(endDate)) {
const timeKey = _startDate.format('HH:mm')
@ -199,18 +329,13 @@ const recordsAcrossAllRange = computed<{
range: range as any,
},
})
}
} else if (fromCol) {
for (const record of sortedFormattedData) {
const id = generateRandomNumber()
const startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[fromCol.title!]).add(1, 'hour')
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
} else if (fromCol) {
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart,
scheduleEnd,
})
const startHour = startDate.hour()
@ -218,7 +343,7 @@ const recordsAcrossAllRange = computed<{
let _startDate = startDate.clone()
// We loop through every minute between the start and end date and keep track of the number of records that overlap at a given time
while (_startDate.isBefore(endDate)) {
while (_startDate.isBefore(endDate, 'minute')) {
const timeKey = _startDate.format('HH:mm')
if (!overlaps[timeKey]) {
@ -239,6 +364,7 @@ const recordsAcrossAllRange = computed<{
display: 'none',
}
}
_startDate = _startDate.add(1, 'minute')
}
@ -273,6 +399,13 @@ const recordsAcrossAllRange = computed<{
}
}
})
/*
recordsByRange.sort((a, b) => {
const fromColA = a.rowMeta.range?.fk_from_col
const fromColB = b.rowMeta.range?.fk_from_col
if (!fromColA || !fromColB) return 0
return dayjs(a.row[fromColA.title!]).isBefore(dayjs(b.row[fromColB.title!])) ? -1 : 1
}) */
// We can't calculate the width & left of the records without knowing the number of records that overlap at a given time
// So we loop through the records again and calculate the width & left of the records based on the number of records that overlap at a given time
@ -287,12 +420,9 @@ const recordsAcrossAllRange = computed<{
overlapIndex = Math.max(overlaps[minutes].id.indexOf(record.rowMeta.id!), overlapIndex)
}
}
const spacing = 0.25
const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps
const leftPerRecord = (widthPerRecord + spacing) * overlapIndex
record.rowMeta.style = {
...record.rowMeta.style,
left: `${leftPerRecord - 0.08}%`,
@ -301,6 +431,92 @@ const recordsAcrossAllRange = computed<{
return record
})
// TODO: Rewrite the calculations for the style of the records
/* for (const record of recordsByRange) {
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) continue
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart,
scheduleEnd,
})
const gridTimes = getGridTimeSlots(startDate, endDate)
for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
if (gridTimeMap.has(gridCounter)) {
gridTimeMap.set(gridCounter, gridTimeMap.get(gridCounter) + 1)
} else {
gridTimeMap.set(gridCounter, 1)
}
}
let foundAColumn = false
for (const column in columnArray) {
if (
hasSlotForRecord(record, columnArray[column], {
fromDate: startDate,
toDate: endDate,
})
) {
columnArray[column].push(record)
foundAColumn = true
break
}
}
if (!foundAColumn) {
columnArray.push([record])
}
}
for (const columnIndex in columnArray) {
for (const record of columnArray[columnIndex]) {
const recordRange = record.rowMeta.range
const fromCol = recordRange?.fk_from_col
const toCol = recordRange?.fk_to_col
if (!fromCol) continue
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
record.rowMeta.numberOfOverlaps =
getMaxOfGrid(
{
fromDate: startDate,
toDate: endDate,
},
gridTimeMap,
) - 1
record.rowMeta.overLapIteration = parseInt(columnIndex) + 1
}
}
for (const record of recordsByRange) {
record.rowMeta.numberOfOverlaps = getMaxOverlaps({
row: record,
rowArray: recordsByRange,
})
const width = 100 / columnArray.length
const left = width * (record.rowMeta.overLapIteration - 1)
record.rowMeta.style = {
...record.rowMeta.style,
width: `${width.toFixed(2)}%`,
left: `${left}%`,
}
} */
return {
count: overlaps,
record: recordsByRange,
@ -328,6 +544,7 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
// When the user is dragging a record, we calculate the new start and end date based on the mouse position
const calculateNewRow = (event: MouseEvent) => {
if (!container.value || !dragRecord.value) return { newRow: null, updateProperty: [] }
const { top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -391,10 +608,7 @@ const calculateNewRow = (event: MouseEvent) => {
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === newPk) {
return newRow
}
return r
return pk === newPk ? newRow : r
})
} else {
// If the old row is not found, we add the new row to the formattedData array and remove the old row from the formattedSideBarData array
@ -408,10 +622,9 @@ const calculateNewRow = (event: MouseEvent) => {
}
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !resizeRecord.value) return
const { top, bottom } = container.value.getBoundingClientRect()
if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
const { top, bottom } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
// If the mouse position is near the top or bottom of the scroll container, we scroll the container
@ -425,26 +638,28 @@ const onResize = (event: MouseEvent) => {
const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col
const toCol = resizeRecord.value.rowMeta.range?.fk_to_col
if (!fromCol || !toCol) return
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const hour = Math.max(Math.floor(percentY * 23), 0)
const hour = Math.floor(percentY * 24) // Round down to the nearest hour
const minutes = Math.round((percentY * 24 * 60) % 60)
let newRow: Row | null = null
let updateProperty: string[] = []
if (resizeDirection.value === 'right') {
// If the user is resizing the record to the right, we calculate the new end date based on the mouse position
let newEndDate = dayjs(selectedDate.value).add(hour, 'hour')
let newEndDate = dayjs(selectedDate.value).add(hour, 'hour').add(minutes, 'minute')
updateProperty = [toCol.title!]
// If the new end date is before the start date, we set the new end date to the start date
// This is to ensure the end date is always same or after the start date
if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone()
if (dayjs(newEndDate).isBefore(ogStartDate.add(1, 'hour'))) {
newEndDate = ogStartDate.clone().add(1, 'hour')
}
if (!newEndDate.isValid()) return
@ -457,14 +672,14 @@ const onResize = (event: MouseEvent) => {
},
}
} else if (resizeDirection.value === 'left') {
let newStartDate = dayjs(selectedDate.value).add(hour, 'hour')
let newStartDate = dayjs(selectedDate.value).add(hour, 'hour').add(minutes, 'minute')
updateProperty = [fromCol.title!]
// If the new start date is after the end date, we set the new start date to the end date
// This is to ensure the start date is always before or same the end date
if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone()
if (dayjs(newStartDate).isAfter(ogEndDate.subtract(1, 'hour'))) {
newStartDate = dayjs(dayjs(ogEndDate)).clone().add(-1, 'hour')
}
if (!newStartDate) return
@ -532,6 +747,10 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value = null
}
if (dragRecord.value) {
dragRecord.value = undefined
}
if (!newRow) return
updateRowProperty(newRow, updateProperty, false)
@ -564,7 +783,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragRecord.value = record
dragElement.value = target
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
@ -615,11 +833,26 @@ const viewMore = (hour: dayjs.Dayjs) => {
selectedTime.value = hour
showSideMenu.value = true
}
const selectHour = (hour: dayjs.Dayjs) => {
selectedTime.value = hour
dragRecord.value = null
}
// TODO: Add Support for multiple ranges when multiple ranges are supported
const newRecord = (hour: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value?.length) return
const record = {
row: {
[calendarRange.value[0].fk_from_col!.title!]: hour.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
}
</script>
<template>
<div
v-if="recordsAcrossAllRange.record.length"
ref="container"
class="w-full relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view"
@ -632,7 +865,8 @@ const viewMore = (hour: dayjs.Dayjs) => {
}"
class="flex w-full min-h-20 relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
data-testid="nc-calendar-day-hour"
@click="selectedTime = hour"
@click="selectHour(hour)"
@dblclick="newRecord(hour)"
>
<div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-20">
{{ dayjs(hour).format('H A') }}
@ -675,7 +909,7 @@ const viewMore = (hour: dayjs.Dayjs) => {
},
}
}
emit('new-record', record)
emit('newRecord', record)
}
"
>
@ -712,7 +946,7 @@ const viewMore = (hour: dayjs.Dayjs) => {
},
}
}
emit('new-record', record)
emit('newRecord', record)
}
"
>
@ -735,54 +969,55 @@ const viewMore = (hour: dayjs.Dayjs) => {
</div>
<div class="absolute inset-0 pointer-events-none">
<div class="relative !ml-[60px]" data-testid="nc-calendar-day-record-container">
<div
v-for="(record, rowIndex) in recordsAcrossAllRange.record"
:key="rowIndex"
:data-testid="`nc-calendar-day-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="record.rowMeta.style"
class="absolute draggable-record group cursor-pointer pointer-events-auto"
@mousedown="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id as string"
@dragover.prevent
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id"
:position="record.rowMeta!.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarVRecordCard>
</LazySmartsheetRow>
</div>
<template v-for="(record, rowIndex) in recordsAcrossAllRange.record" :key="rowIndex">
<div
v-if="record.rowMeta.style?.display !== 'none'"
:data-testid="`nc-calendar-day-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="record.rowMeta.style"
class="absolute draggable-record group cursor-pointer pointer-events-auto"
@mousedown="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id as string"
@dragover.prevent
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:selected="record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta!.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField!"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarVRecordCard>
</LazySmartsheetRow>
</div>
</template>
</div>
</div>
</div>
<div v-else class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center">No records in this day</div>
</template>
<style lang="scss" scoped>

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

@ -4,7 +4,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['new-record', 'expandRecord'])
const emit = defineEmits(['newRecord', 'expandRecord'])
const {
selectedDate,
@ -110,9 +110,9 @@ const recordsToDisplay = computed<{
const perWidth = gridContainerWidth.value / 7
const perHeight = gridContainerHeight.value / dates.value.length
const perRecordHeight = 40
const perRecordHeight = 24
const spaceBetweenRecords = 35
const spaceBetweenRecords = 26
// This object is used to keep track of the number of records in a day
// The key is the date in the format YYYY-MM-DD
@ -133,13 +133,12 @@ const recordsToDisplay = computed<{
// Filter out records that don't satisfy the range and sort them by start date
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
if (startCol && endCol) {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
const toDate = record.row[endCol!.title!] ? dayjs(record.row[endCol!.title!]) : null
const fromDate = record.row[startCol.title!] ? dayjs(record.row[startCol.title!]) : null
const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
return fromDate && toDate && !toDate.isBefore(fromDate)
} else if (startCol && !endCol) {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
return !!fromDate
}
return false
@ -148,7 +147,7 @@ const recordsToDisplay = computed<{
sortedFormattedData.forEach((record: Row) => {
if (!endCol && startCol) {
// If there is no end date, we just display the record on the start date
const startDate = dayjs(record.row[startCol!.title!])
const startDate = dayjs(record.row[startCol.title!])
const dateKey = startDate.format('YYYY-MM-DD')
if (!recordsInDay[dateKey]) {
@ -175,10 +174,10 @@ const recordsToDisplay = computed<{
// The top is calculated from the week index and the record index
// If the record in 1st week and no record in that date them the top will be 0
const top = weekIndex * perHeight + spaceBetweenRecords + (recordIndex - 1) * perRecordHeight
const top = weekIndex * perHeight + spaceBetweenRecords + (recordIndex - 1) * (perRecordHeight + 4)
// The 25 is obtained from the trial and error
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 25
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 12
if (heightRequired > perHeight) {
style.display = 'none'
@ -200,12 +199,13 @@ const recordsToDisplay = computed<{
})
} else if (startCol && endCol) {
// If the range specifies fromCol and endCol
const startDate = dayjs(record.row[startCol!.title!])
const endDate = dayjs(record.row[endCol!.title!])
const startDate = dayjs(record.row[startCol.title!])
const endDate = dayjs(record.row[endCol.title!])
let currentWeekStart = startDate.startOf('week')
const id = record.rowMeta.id ?? generateRandomNumber()
// Since the records can span multiple weeks, to display, we render multiple records
// Since the records can span multiple weeks, to display, we render multiple elements
// for each week the record spans. The id is used to identify the elements that belong to the same record
while (
@ -219,6 +219,11 @@ const recordsToDisplay = computed<{
const recordStart = currentWeekStart.isBefore(startDate) ? startDate : currentWeekStart
const recordEnd = currentWeekEnd.isAfter(endDate) ? endDate : currentWeekEnd
if (recordEnd.isBefore(dates.value[0][0])) {
currentWeekStart = currentWeekStart.add(1, 'week')
continue
}
// Update the recordsInDay object to keep track of the number of records in a day
let day = recordStart.clone()
while (day.isSameOrBefore(recordEnd)) {
@ -257,8 +262,7 @@ const recordsToDisplay = computed<{
overflowCount: 0,
}
}
const recordIndex = recordsInDay[dateKey].count
maxRecordCount = Math.max(maxRecordCount, recordIndex)
maxRecordCount = Math.max(maxRecordCount, recordsInDay[dateKey].count)
}
const startDayIndex = Math.max(
@ -279,8 +283,8 @@ const recordsToDisplay = computed<{
// The top is calculated from the week index and the record index
// If the record in 1st week and no record in that date them the top will be 0
// If the record in 1st week and 1 record in that date then the top will be perRecordHeight + spaceBetweenRecords
const top = weekIndex * perHeight + spaceBetweenRecords + Math.max(maxRecordCount - 1, 0) * perRecordHeight
const heightRequired = perRecordHeight * maxRecordCount + spaceBetweenRecords
const top = weekIndex * perHeight + spaceBetweenRecords + Math.max(maxRecordCount - 1, 0) * (perRecordHeight + 4)
const heightRequired = perRecordHeight * Math.max(maxRecordCount, 0) + spaceBetweenRecords + 12
let position = 'rounded'
// Here we are checking if the startDay is before all the dates shown in UI rather that the current month
@ -304,7 +308,7 @@ const recordsToDisplay = computed<{
// If the height required is more than the height of the week, we hide the record
// and update the recordsInDay object for all the spanned days
if (heightRequired + 15 > perHeight) {
if (heightRequired > perHeight) {
style.display = 'none'
for (let i = startDayIndex; i <= endDayIndex; i++) {
const week = dates.value[weekIndex]
@ -386,7 +390,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
updateProperty.push(toCol!.title!)
}
if (!newRow) return
if (!newRow) return { newRow: null, updateProperty: [] }
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
@ -528,6 +532,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value = null
}
dragRecord.value = undefined
updateRowProperty(newRow, updateProperty, false)
focusedDate.value = null
@ -550,12 +555,10 @@ const dragStart = (event: MouseEvent, record: Row) => {
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
// el.style.visibility = 'hidden'
el.style.opacity = '30%'
}
})
dragRecord.value = record
// selectedDate.value = null
isDragging.value = true
@ -621,6 +624,19 @@ const isDateSelected = (date: dayjs.Dayjs) => {
if (!selectedDate.value) return false
return dayjs(date).isSame(selectedDate.value, 'day')
}
// TODO: Add Support for multiple ranges when multiple ranges are supported
const addRecord = (date: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value) return
const fromCol = calendarRange.value[0].fk_from_col
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', newRecord)
}
</script>
<template>
@ -652,10 +668,12 @@ const isDateSelected = (date: dayjs.Dayjs) => {
'border-brand-500 border-1 !border-r-1 border-b-1':
isDateSelected(day) || (focusedDate && dayjs(day).isSame(focusedDate, 'day')),
'!text-gray-400': !isDayInPagedMonth(day),
'!bg-gray-50': day.get('day') === 0 || day.get('day') === 6,
}"
class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-100 font-medium hover:bg-gray-50 text-gray-800 bg-white"
data-testid="nc-calendar-month-day"
@click="selectDate(day)"
@dblclick="addRecord(day)"
>
<div v-if="isUIAllowed('dataEdit')" class="flex justify-between p-1">
<span
@ -672,7 +690,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block"
class="!group-hover:block rounded"
size="small"
type="secondary"
>
@ -692,7 +710,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
[range.fk_from_col!.title!]: dayjs(day).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('new-record', record)
emit('newRecord', record)
}
"
>
@ -705,12 +723,13 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</template>
</NcDropdown>
<NcButton
v-else
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block"
size="small"
class="!group-hover:block !w-6 !h-6 !rounded"
size="xsmall"
type="secondary"
@click="
() => {
@ -719,17 +738,17 @@ const isDateSelected = (date: dayjs.Dayjs) => {
[calendarRange[0].fk_from_col!.title!]: (day).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('new-record', record)
emit('newRecord', record)
}
"
>
<component :is="iconMap.plus" class="h-4 w-4" />
<component :is="iconMap.plus" />
</NcButton>
<span
:class="{
'bg-brand-50 text-brand-500': day.isSame(dayjs(), 'date'),
}"
class="px-1.5 rounded-lg py-1 my-1"
class="px-1.3 py-1 text-xs rounded-lg"
>
{{ day.format('DD') }}
</span>
@ -742,72 +761,64 @@ const isDateSelected = (date: dayjs.Dayjs) => {
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow &&
!draggingId
"
class="!absolute bottom-1 text-center w-15 mx-auto inset-x-0 z-3 text-gray-500"
class="!absolute bottom-1 right-1 text-center min-w-4.5 mx-auto z-3 text-gray-500"
size="xxsmall"
type="secondary"
@click="viewMore(day)"
>
<span class="text-xs"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} more </span>
<span class="text-xs px-1"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} </span>
</NcButton>
</div>
</div>
</div>
<div class="absolute inset-0 pointer-events-none mt-8 pb-7.5" data-testid="nc-calendar-month-record-container">
<div
v-for="(record, recordIndex) in recordsToDisplay.records"
:key="recordIndex"
:data-testid="`nc-calendar-month-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="{
...record.rowMeta.style,
zIndex: record.rowMeta.id === draggingId ? 100 : 0,
boxShadow:
record.rowMeta.id === draggingId
? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
: 'none',
}"
class="absolute group draggable-record cursor-pointer pointer-events-auto"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
@mousedown.stop="dragStart($event, record)"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id"
:position="record.rowMeta.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected="
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>
</div>
<template v-for="(record, recordIndex) in recordsToDisplay.records" :key="recordIndex">
<div
v-if="record.rowMeta.style?.display !== 'none'"
:data-testid="`nc-calendar-month-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="{
...record.rowMeta.style,
zIndex: record.rowMeta.id === draggingId ? 100 : 0,
}"
class="absolute group draggable-record cursor-pointer pointer-events-auto"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
@mousedown.stop="dragStart($event, record)"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === draggingId"
:position="record.rowMeta.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id || resizeRecord?.rowMeta?.id === record.rowMeta.id"
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>
</div>
</template>
</div>
</div>
</template>

23
packages/nc-gui/components/smartsheet/calendar/RecordCard.vue

@ -26,11 +26,11 @@ const emit = defineEmits(['resize-start'])
<template>
<div
:class="{
'min-h-9': size === 'small',
'h-6': size === 'small',
'h-full': size === 'auto',
'rounded-l-lg': position === 'leftRounded',
'rounded-r-lg': position === 'rightRounded',
'rounded-lg mx-1': position === 'rounded',
'rounded-l-md ml-1': position === 'leftRounded',
'rounded-r-md mr-1': position === 'rightRounded',
'rounded-md mx-1': position === 'rounded',
'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue',
@ -39,7 +39,8 @@ const emit = defineEmits(['resize-start'])
'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple',
'group-hover:(border-brand-500 border-1)': resize,
'!border-brand-500 border-1': selected || hover,
'!border-blue-200 border-1': selected,
'shadow-md': hover,
}"
class="relative transition-all flex items-center px-1 group border-1 border-transparent"
>
@ -53,13 +54,13 @@ const emit = defineEmits(['resize-start'])
'bg-pink-500': color === 'pink',
'bg-purple-500': color === 'purple',
}"
class="w-1 min-h-5 bg-blue-500 rounded-x rounded-y-sm"
class="w-1 min-h-4 bg-blue-500 rounded-x rounded-y-sm"
></div>
<div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.7 h-7.1 absolute -left-4 resize">
<NcButton
:class="{
'!block z-1 !border-brand-500': selected || hover,
'!block z-2 !border-brand-500': selected || hover,
'!hidden': !selected && !hover,
}"
size="xsmall"
@ -70,23 +71,23 @@ const emit = defineEmits(['resize-start'])
</NcButton>
</div>
<div class="overflow-hidden items-center flex w-full ml-2 h-8">
<div class="overflow-hidden items-center justify-center flex w-full ml-2">
<span v-if="position === 'rightRounded' || position === 'none'" class="mr-1"> .... </span>
<span
:class="{
'pr-7': position === 'leftRounded',
}"
class="text-sm pr-3 mr-3 break-word space-x-2 whitespace-nowrap gap-2 overflow-hidden text-ellipsis w-full truncate text-gray-800"
class="text-sm pr-3 mb-0.5 mr-3 break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-gray-800"
>
<slot />
</span>
<span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span>
</div>
<div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.3 z-1 -right-4 resize">
<div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.3 -right-4 resize">
<NcButton
:class="{
'!block !border-brand-500': selected || hover,
'!block !border-brand-500 z-2': selected || hover,
'!hidden': !selected && !hover,
}"
size="xsmall"

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs'
import { type Row, computed, iconMap, isRowEmpty, ref } from '#imports'
@ -269,33 +269,36 @@ const newRecord = () => {
emit('newRecord', { row, oldRow: {}, rowMeta: { new: true } })
}
const height = ref(0)
const width = ref(0)
const heightListener = () => {
height.value = window.innerHeight
const widthListener = () => {
width.value = window.innerWidth
}
onMounted(() => {
window.addEventListener('resize', heightListener)
window.addEventListener('resize', widthListener)
})
onUnmounted(() => {
window.removeEventListener('resize', heightListener)
window.removeEventListener('resize', widthListener)
})
</script>
<template>
<div
:class="{
'w-0': !props.visible,
'w-1/6 min-w-[22.1rem] nc-calendar-side-menu-open': props.visible,
'!w-0': !props.visible,
'min-w-[356px]': width > 1440 && props.visible,
'min-w-[264px]': width <= 1440 && props.visible,
'nc-calendar-side-menu-open': props.visible,
}"
class="h-full border-l-1 border-gray-200 transition-all"
data-testid="nc-calendar-side-menu"
>
<div
:class="{
'!hidden': height < 918,
'!hidden': width <= 1440,
'px-4 pt-3 pb-4 ': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const),
}"
class="flex flex-col"
>
@ -327,11 +330,11 @@ onUnmounted(() => {
<div
:class="{
'!border-t-0': height < 918,
'!border-t-0': width <= 1440,
}"
class="px-4 border-t-1 border-gray-200 relative flex flex-col gap-y-4 pt-3"
class="border-t-1 border-gray-200 relative flex flex-col gap-y-4 pt-3"
>
<div class="flex items-center gap-2">
<div class="flex px-4 items-center gap-2">
<a-input
v-model:value="searchQuery.value"
:class="{
@ -345,8 +348,8 @@ onUnmounted(() => {
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template>
</a-input>
<NcSelect v-model:value="sideBarFilterOption" class="min-w-38 !text-gray-800" data-testid="nc-calendar-sidebar-filter">
<a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-800">
<NcSelect v-model:value="sideBarFilterOption" class="min-w-38 !text-gray-600" data-testid="nc-calendar-sidebar-filter">
<a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-600">
<div class="flex items-center justify-between gap-2">
<div class="truncate flex-1">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only>
@ -369,11 +372,12 @@ onUnmounted(() => {
v-if="calendarRange"
:ref="sideBarListRef"
:class="{
'!h-[calc(100vh-10.5rem)]': height < 918,
'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && height > 918,
'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && height > 918,
'!h-[calc(100vh-10.5rem)]': width <= 1440,
'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440,
'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440,
}"
class="gap-2 flex flex-col nc-scrollbar-md overflow-y-auto nc-calendar-top-height"
class="nc-scrollbar-md pl-4 pr-4 overflow-y-auto"
data-testid="nc-calendar-side-menu-list"
@scroll="sideBarListScrollHandle"
>
@ -397,56 +401,43 @@ onUnmounted(() => {
</div>
</div>
<template v-else-if="renderData.length > 0">
<LazySmartsheetRow v-for="(record, rowIndex) in renderData" :key="rowIndex" :row="record">
<LazySmartsheetCalendarSideRecordCard
:draggable="sideBarFilterOption === 'withoutDates' && activeCalendarView !== 'year'"
:from-date="
<div class="gap-2 flex flex-col">
<LazySmartsheetRow v-for="(record, rowIndex) in renderData" :key="rowIndex" :row="record">
<LazySmartsheetCalendarSideRecordCard
:draggable="sideBarFilterOption === 'withoutDates' && activeCalendarView !== 'year'"
:from-date="
record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('DD MMM')
: dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('DD MMM•HH:mm A')
: null
"
:invalid="
:invalid="
record.rowMeta.range!.fk_to_col &&
dayjs(record.row[record.rowMeta.range!.fk_from_col.title!]).isAfter(
dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]),
)
"
:row="record"
:to-date="
:row="record"
:to-date="
record.rowMeta.range!.fk_to_col
? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM')
: dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM•HH:mm A')
: null
"
color="blue"
data-testid="nc-sidebar-record-card"
@click="emit('expand-record', record)"
@dragstart="dragStart($event, record)"
@dragover.prevent
>
<template v-if="!isRowEmpty(record, displayField)">
<div :class="{}">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
</template>
</LazySmartsheetCalendarSideRecordCard>
</LazySmartsheetRow>
color="blue"
data-testid="nc-sidebar-record-card"
@click="emit('expand-record', record)"
@dragstart="dragStart($event, record)"
@dragover.prevent
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell v-model="record.row[displayField!.title!]" :column="displayField" />
</template>
</LazySmartsheetCalendarSideRecordCard>
</LazySmartsheetRow>
</div>
</template>
</div>
</div>

6
packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue

@ -17,7 +17,7 @@ const props = withDefaults(defineProps<Props>(), {
<template>
<div class="border-1 cursor-pointer border-gray-200 items-center px-2 py-3 rounded-lg">
<div class="flex items-center gap-2">
<div class="flex items-center pl-1 gap-2">
<span
:class="{
'bg-maroon-500': props.color === 'maroon',
@ -29,8 +29,8 @@ const props = withDefaults(defineProps<Props>(), {
}"
class="block h-10 w-1 rounded"
></span>
<div class="flex text-ellipsis gap-1 flex-col">
<span class="text-sm max-w-40 truncate text-gray-800">
<div class="flex gap-1 flex-col">
<span class="text-sm max-w-56 truncate text-gray-800">
<slot />
</span>
<span v-if="showDate" class="text-xs text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span>

15
packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue

@ -26,7 +26,7 @@ const emit = defineEmits(['resize-start'])
>
<NcButton
:class="{
'!flex rounded-lg border-brand-500': selected || hover,
'!flex rounded-md border-brand-500': selected || hover,
}"
class="!group-hover:(border-brand-500) !border-1 text-gray-400 cursor-ns-resize"
size="xsmall"
@ -38,9 +38,9 @@ const emit = defineEmits(['resize-start'])
</div>
<div
:class="{
'rounded-t-lg': position === 'topRounded',
'rounded-b-lg': position === 'bottomRounded',
'rounded-lg': position === 'rounded',
'rounded-t-md': position === 'topRounded',
'rounded-b-md': position === 'bottomRounded',
'rounded-md': position === 'rounded',
'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue',
@ -49,7 +49,8 @@ const emit = defineEmits(['resize-start'])
'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple',
'group-hover:(border-brand-500)': resize,
'!border-brand-500 border-1': selected || hover,
'!border-blue-200 border-1': selected,
'shadow-md': hover,
}"
class="relative flex items-center h-full ml-0.25 border-1 border-transparent"
>
@ -69,9 +70,7 @@ const emit = defineEmits(['resize-start'])
<div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div>
<span
class="pl-1 pr-1 text-sm h-[80%] text-gray-800 leading-7 space-x-2 break-all whitespace-normal truncate w-full overflow-y-hidden"
>
<span class="pl-1 pr-1 text-sm h-[80%] text-gray-800 leading-7 break-all whitespace-normal truncate w-full overflow-y-hidden">
<slot />
</span>

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

@ -5,7 +5,7 @@ import type { Row } from '~/lib'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord'])
const emits = defineEmits(['expandRecord', 'newRecord'])
const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
useCalendarViewStoreOrThrow()
@ -23,7 +23,7 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
const fi = _fields.value?.find((f) => f.title === field.title)
const fi = _fields.value?.find((f) => f.title === field?.title)
return {
underline: fi?.underline,
@ -111,7 +111,7 @@ const calendarData = computed(() => {
return !endDate.isBefore(startDate)
})) {
// Generate a unique id for the record if it doesn't have one
const id = record.row.id ?? generateRandomNumber()
const id = record.rowMeta.id ?? generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
const ogStartDate = startDate.clone()
const endDate = dayjs(record.row[toCol.title!])
@ -196,14 +196,14 @@ const calendarData = computed(() => {
style: {
width: widthStyle,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableRow * 40}px`,
top: `${suitableRow * 28}px`,
},
},
})
}
} else if (fromCol) {
for (const record of formattedData.value) {
const id = record.row.id ?? generateRandomNumber()
const id = record.rowMeta.id ?? generateRandomNumber()
const startDate = dayjs(record.row[fromCol.title!])
const startDaysDiff = Math.max(startDate.diff(selectedDateRange.value.start, 'day'), 0)
@ -222,7 +222,7 @@ const calendarData = computed(() => {
style: {
width: `calc(${perDayWidth}px)`,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableRow * 40}px`,
top: `${suitableRow * 28}px`,
},
},
})
@ -418,7 +418,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
calculateNewRow(event)
calculateNewRow(event, false)
}
const stopDrag = (event: MouseEvent) => {
@ -443,6 +443,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value.style.boxShadow = 'none'
dragElement.value = null
}
dragRecord.value = undefined
updateRowProperty(newRow, updateProperty, false)
@ -470,8 +471,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
}
})
dragRecord.value = record
isDragging.value = true
dragElement.value = target
dragRecord.value = record
@ -512,9 +511,25 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
}
}
const selectDate = (day: dayjs.Dayjs) => {
selectedDate.value = day
dragRecord.value = undefined
}
dragRecord.value = null
// TODO: Add Support for multiple ranges when multiple ranges are supported
const addRecord = (date: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value) return
const fromCol = calendarRange.value[0].fk_from_col
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emits('newRecord', newRecord)
}
</script>
@ -538,71 +553,65 @@ const dropEvent = (event: DragEvent) => {
:key="dateIndex"
:class="{
'!border-1 !border-t-0 border-brand-500': dayjs(date).isSame(selectedDate, 'day'),
'!bg-gray-50': date.get('day') === 0 || date.get('day') === 6,
}"
class="flex flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
data-testid="nc-calendar-week-day"
@click="selectedDate = dayjs(date)"
@click="selectDate(date)"
@dblclick="addRecord(date)"
></div>
</div>
<div
class="absolute nc-scrollbar-md overflow-y-auto mt-9 pointer-events-none inset-0"
data-testid="nc-calendar-week-record-container"
>
<div
v-for="(record, id) in calendarData"
:key="id"
:data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="{
...record.rowMeta.style,
boxShadow:
record.rowMeta.id === dragElement?.getAttribute('data-unique-id')
? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
: 'none',
}"
class="absolute group draggable-record pointer-events-auto nc-calendar-week-record-card"
@mousedown="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id"
:position="record.rowMeta.position"
:record="record"
:selected="
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@dblclick="emits('expand-record', record)"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, index) in fieldsWithoutDisplay" :key="index">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>
</div>
<template v-for="(record, id) in calendarData" :key="id">
<div
v-if="record.rowMeta.style?.display !== 'none'"
:data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="{
...record.rowMeta.style,
}"
class="absolute group draggable-record pointer-events-auto nc-calendar-week-record-card"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
@mousedown.stop="dragStart($event, record)"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id"
color="blue"
@dblclick.stop="emits('expandRecord', record)"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, index) in fieldsWithoutDisplay" :key="index">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>
</div>
</template>
</div>
</div>
</template>

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

Loading…
Cancel
Save