Browse Source

Merge branch 'develop' into l10n_develop_2

pull/7827/head
Raju Udava 9 months ago committed by GitHub
parent
commit
be24b1adbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/unit-test.yml
  2. 4
      docker-compose/traefik/docker-compose.yml
  3. 3
      packages/nc-gui/assets/nc-icons/cell-attachment.svg
  4. 3
      packages/nc-gui/assets/nc-icons/cell-barcode.svg
  5. 4
      packages/nc-gui/assets/nc-icons/cell-checkbox.svg
  6. 4
      packages/nc-gui/assets/nc-icons/cell-currency.svg
  7. 6
      packages/nc-gui/assets/nc-icons/cell-date.svg
  8. 8
      packages/nc-gui/assets/nc-icons/cell-datetime.svg
  9. 5
      packages/nc-gui/assets/nc-icons/cell-db.svg
  10. 7
      packages/nc-gui/assets/nc-icons/cell-decimal.svg
  11. 7
      packages/nc-gui/assets/nc-icons/cell-duration.svg
  12. 4
      packages/nc-gui/assets/nc-icons/cell-email.svg
  13. 8
      packages/nc-gui/assets/nc-icons/cell-formula.svg
  14. 7
      packages/nc-gui/assets/nc-icons/cell-geometry.svg
  15. 8
      packages/nc-gui/assets/nc-icons/cell-json.svg
  16. 7
      packages/nc-gui/assets/nc-icons/cell-link.svg
  17. 8
      packages/nc-gui/assets/nc-icons/cell-longtext.svg
  18. 9
      packages/nc-gui/assets/nc-icons/cell-lookup.svg
  19. 8
      packages/nc-gui/assets/nc-icons/cell-multiselect.svg
  20. 6
      packages/nc-gui/assets/nc-icons/cell-number.svg
  21. 5
      packages/nc-gui/assets/nc-icons/cell-percentage.svg
  22. 3
      packages/nc-gui/assets/nc-icons/cell-phone.svg
  23. 10
      packages/nc-gui/assets/nc-icons/cell-qrcode.svg
  24. 3
      packages/nc-gui/assets/nc-icons/cell-rating.svg
  25. 3
      packages/nc-gui/assets/nc-icons/cell-rollup.svg
  26. 4
      packages/nc-gui/assets/nc-icons/cell-select.svg
  27. 5
      packages/nc-gui/assets/nc-icons/cell-text.svg
  28. 4
      packages/nc-gui/assets/nc-icons/cell-time.svg
  29. 4
      packages/nc-gui/assets/nc-icons/cell-url.svg
  30. 4
      packages/nc-gui/assets/nc-icons/cell-user.svg
  31. 7
      packages/nc-gui/assets/nc-icons/system-date.svg
  32. 4
      packages/nc-gui/assets/nc-icons/system-key.svg
  33. 6
      packages/nc-gui/assets/nc-icons/system-text.svg
  34. 5
      packages/nc-gui/assets/nc-icons/system-user.svg
  35. 3
      packages/nc-gui/assets/style.scss
  36. 1
      packages/nc-gui/components/cell/Json.vue
  37. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  38. 10
      packages/nc-gui/components/cell/RichText.vue
  39. 2
      packages/nc-gui/components/cell/SingleSelect.vue
  40. 20
      packages/nc-gui/components/cell/TextArea.vue
  41. 1
      packages/nc-gui/components/cell/User.vue
  42. 74
      packages/nc-gui/components/dlg/ViewCreate.vue
  43. 156
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  44. 6
      packages/nc-gui/components/general/ColorPicker.vue
  45. 12
      packages/nc-gui/components/monaco/Editor.vue
  46. 6
      packages/nc-gui/components/nc/DateWeekSelector.vue
  47. 9
      packages/nc-gui/components/nc/ErrorBoundary.vue
  48. 6
      packages/nc-gui/components/nc/MonthYearSelector.vue
  49. 10
      packages/nc-gui/components/smartsheet/Cell.vue
  50. 122
      packages/nc-gui/components/smartsheet/Form.vue
  51. 14
      packages/nc-gui/components/smartsheet/calendar/Cell.vue
  52. 26
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  53. 386
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  54. 88
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  55. 23
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  56. 23
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  57. 4
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  58. 15
      packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue
  59. 56
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  60. 40
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  61. 8
      packages/nc-gui/components/smartsheet/calendar/index.vue
  62. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  63. 52
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  64. 32
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  65. 121
      packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue
  66. 18
      packages/nc-gui/components/webhook/Editor.vue
  67. 21
      packages/nc-gui/composables/useCalendarViewStore.ts
  68. 252
      packages/nc-gui/composables/useSharedFormViewStore.ts
  69. 2
      packages/nc-gui/composables/useViewData.ts
  70. 6
      packages/nc-gui/lang/en.json
  71. 6
      packages/nc-gui/lib/enums.ts
  72. 8
      packages/nc-gui/lib/types.ts
  73. 7
      packages/nc-gui/package.json
  74. 26
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  75. 69
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  76. 68
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  77. 3
      packages/nc-gui/store/views.ts
  78. 11
      packages/nc-gui/utils/cell.ts
  79. 76
      packages/nc-gui/utils/columnUtils.ts
  80. 6
      packages/nc-gui/utils/dataUtils.ts
  81. 66
      packages/nc-gui/utils/iconUtils.ts
  82. 2
      packages/noco-docs/docusaurus.config.js
  83. 4
      packages/noco-docs/package.json
  84. 4
      packages/nocodb/package.json
  85. 12
      packages/nocodb/src/db/conditionV2.ts
  86. 1
      packages/nocodb/src/helpers/webhookHelpers.ts
  87. 1
      packages/nocodb/src/services/calendar-datas.service.ts
  88. 275
      pnpm-lock.yaml

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

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

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)];
}
}
}
}

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"

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

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

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>

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

@ -104,15 +104,11 @@ watch(picked, (n, _o) => {
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
.color-selector:not(.new-design):hover {
.color-selector: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) {
filter: brightness(90%);
-webkit-filter: brightness(90%);

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

@ -12,12 +12,20 @@ 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>(), {
lang: 'json',
validate: true,
disableDeepCompare: false,
autoFocus: true,
})
const emits = defineEmits(['update:modelValue'])
const { hideMinimap, lang, validate, disableDeepCompare, modelValue, readOnly, autoFocus } = props
const vModel = computed<string>({
get: () => {
if (typeof modelValue === 'object') {
@ -120,7 +128,7 @@ onMounted(async () => {
}
})
if (!isDrawerOrModalExist()) {
if (!isDrawerOrModalExist() && autoFocus) {
// auto focus on json cells only
editor.focus()
}

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

@ -152,7 +152,7 @@ const paginate = (action: 'next' | 'prev') => {
class="flex items-center"
>
<NcTooltip>
<NcButton v-if="!disablePagination" size="medium" type="secondary" @click="paginate('prev')">
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton>
<template #title>
@ -165,11 +165,11 @@ const paginate = (action: 'next' | 'prev') => {
'text-xs': size === 'small',
'text-sm': size === 'medium',
}"
class="font-bold text-gray-700"
class="text-gray-700"
>{{ currentMonthYear }}</span
>
<NcTooltip>
<NcButton v-if="!disablePagination" size="medium" type="secondary" @click="paginate('next')">
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton>
<template #title>

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

@ -66,7 +66,14 @@ export default {
<template>
<slot :key="key"></slot>
<slot name="error">
<NcModal v-if="error" 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>

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-900 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-900 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') }}

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

122
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()
@ -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 &&
@ -736,7 +759,16 @@ useEventListener(
</div>
<div v-if="formViewData.submit_another_form || !isPublic" class="text-right mt-4">
<NcButton type="primary" size="medium" @click="submitted = false">
<NcButton
type="primary"
size="medium"
@click="
() => {
submitted = false
clearForm()
}
"
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
</div>
@ -1056,7 +1088,7 @@ 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 +1241,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 +1263,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 +1272,7 @@ 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 +1321,7 @@ useEventListener(
>
{{ $t('activity.clearForm') }}
</NcButton>
<NcButton
html-type="submit"
type="primary"
@ -1433,7 +1466,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">
@ -1792,21 +1825,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 +1847,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>

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>

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

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

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

@ -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!])
// If no start date is provided or startDate is after the endDate, we skip the record
if (!startDate.isValid() || startDate.isAfter(endDate)) continue
const sortedFormattedData = [...formattedData.value]
.filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.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 (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 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') }}
@ -749,7 +983,8 @@ const viewMore = (hour: dayjs.Dayjs) => {
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id"
: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')"
@ -761,13 +996,14 @@ const viewMore = (hour: dayjs.Dayjs) => {
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField"
: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"
@ -781,8 +1017,6 @@ const viewMore = (hour: dayjs.Dayjs) => {
</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>

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

@ -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"
>
@ -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="
() => {
@ -723,13 +742,13 @@ const isDateSelected = (date: dayjs.Dayjs) => {
}
"
>
<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,12 +761,12 @@ 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>
@ -761,10 +780,6 @@ const isDateSelected = (date: dayjs.Dayjs) => {
: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"
@ -773,17 +788,11 @@ const isDateSelected = (date: dayjs.Dayjs) => {
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id"
: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
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id || resizeRecord?.rowMeta?.id === record.rowMeta.id"
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
@ -798,6 +807,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</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"

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"

23
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'
@ -348,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>
@ -433,22 +433,7 @@ onUnmounted(() => {
@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>
<LazySmartsheetCalendarCell v-model="record.row[displayField!.title!]" :column="displayField" />
</template>
</LazySmartsheetCalendarSideRecordCard>
</LazySmartsheetRow>

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

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

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

@ -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)
}
}
dragRecord.value = null
const selectDate = (day: dayjs.Dayjs) => {
selectedDate.value = day
dragRecord.value = undefined
}
// 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,10 +553,12 @@ 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
@ -555,31 +572,21 @@ const dropEvent = (event: DragEvent) => {
: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)"
@mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id"
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta.position"
:record="record"
:selected="
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@dblclick="emits('expand-record', record)"
@dblclick.stop="emits('expand-record', record)"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
@ -593,6 +600,7 @@ const dropEvent = (event: DragEvent) => {
</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"

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

@ -394,8 +394,6 @@ const recordsAcrossAllRange = computed<{
}
})
const dragElement = ref<HTMLElement | null>(null)
const resizeInProgress = ref(false)
const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -444,12 +442,13 @@ const onResize = (event: MouseEvent) => {
const day = Math.floor(percentX * 7)
const hour = Math.floor(percentY * 23)
const minutes = Math.round((percentY * 24 * 60) % 60)
let updateProperty: string[] = []
let newRow: Row = resizeRecord.value
if (resizeDirection.value === 'right') {
let newEndDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
let newEndDate = dayjs(selectedDateRange.value.start).add(day, 'day').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
@ -467,7 +466,7 @@ const onResize = (event: MouseEvent) => {
},
}
} else if (resizeDirection.value === 'left') {
let newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
let newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').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
@ -520,7 +519,6 @@ const calculateNewRow = (
newRow: Row | null
updatedProperty: string[]
} => {
if (!isUIAllowed('dataEdit') || !container.value || !dragRecord.value) return { newRow: null, updatedProperty: [] }
const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -620,11 +618,6 @@ const stopDrag = (event: MouseEvent) => {
el.style.opacity = '100%'
})
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value = null
}
if (newRow) {
updateRowProperty(newRow, updatedProperty, false)
}
@ -654,10 +647,7 @@ const dragStart = (event: MouseEvent, record: Row) => {
}
})
dragRecord.value = record
isDragging.value = true
dragElement.value = target
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
@ -732,6 +722,19 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
return { isOverflow, overflowCount }
}
// 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>
<template>
@ -769,13 +772,16 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
:key="hourIndex"
:class="{
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
}"
class="text-center relative h-20 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100"
data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click="
() => {
selectedTime = hour
selectedDate = hour
dragRecord = undefined
}
"
>
@ -806,18 +812,19 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
:data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style "
class="absolute draggable-record w-1/7 group cursor-pointer pointer-events-auto"
@mousedown="dragStart($event, record)"
@mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
@dragover.prevent
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id"
: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')"
:record="record"
color="blue"
:selected="record.rowMeta!.id === dragRecord?.rowMeta?.id"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
@ -832,6 +839,7 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
</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"

8
packages/nc-gui/components/smartsheet/calendar/index.vue

@ -169,7 +169,7 @@ const headerText = computed(() => {
<template>
<div class="flex h-full flex-row" data-testid="nc-calendar-wrapper">
<div class="flex flex-col w-full">
<div class="flex justify-between p-3 items-center border-b-1 border-gray-200" data-testid="nc-calendar-topbar">
<div class="flex justify-between p-2 items-center border-b-1 border-gray-200" data-testid="nc-calendar-topbar">
<div class="flex justify-start gap-3 items-center">
<NcTooltip>
<template #title> {{ $t('labels.previous') }}</template>
@ -247,7 +247,7 @@ const headerText = computed(() => {
type="secondary"
@click="goToToday"
>
<span class="text-gray-700">
<span class="text-gray-600 !text-sm">
{{ $t('activity.goToToday') }}
</span>
</NcButton>
@ -265,7 +265,7 @@ const headerText = computed(() => {
type="secondary"
@click="showSideMenu = !showSideMenu"
>
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-700 transition-all" />
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton>
</NcTooltip>
</div>
@ -321,6 +321,7 @@ const headerText = computed(() => {
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
close-after-save
:meta="meta"
:row="expandedFormRow"
:state="expandedFormRowState"
@ -332,6 +333,7 @@ const headerText = computed(() => {
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
v-model="expandedFormOnRowIdDlg"
close-after-save
:meta="meta"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:row-id="route.query.rowId"

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

@ -309,7 +309,7 @@ if (props.fromTableExplorer) {
</template>
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-2 items-center">
<component :is="opt.icon" class="text-gray-700" />
<component :is="opt.icon" class="text-gray-700 w-4 h-4" />
<div class="flex-1">{{ opt.name }}</div>
<span v-if="opt.deprecated" class="!text-xs !text-gray-300">({{ $t('general.deprecated') }})</span>
<component

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

@ -40,60 +40,60 @@ import {
const renderIcon = (column: ColumnType, abstractType: any) => {
if (isPrimaryKey(column)) {
return iconMap.key
return iconMap.cellSystemKey
} else if (isSpecificDBType(column)) {
return iconMap.specificDbType
return iconMap.cellDb
} else if (isJSON(column)) {
return iconMap.json
return iconMap.cellJson
} else if (isDate(column, abstractType)) {
return iconMap.calendar
return iconMap.cellDate
} else if (isDateTime(column, abstractType)) {
return iconMap.datetime
return iconMap.cellDatetime
} else if (isGeoData(column)) {
return iconMap.geoData
} else if (isSet(column)) {
return iconMap.multiSelect
return iconMap.cellMultiSelect
} else if (isSingleSelect(column)) {
return iconMap.singleSelect
return iconMap.cellSingleSelect
} else if (isBoolean(column, abstractType)) {
return iconMap.boolean
return iconMap.cellCheckbox
} else if (isTextArea(column)) {
return iconMap.longText
return iconMap.cellLongText
} else if (isEmail(column)) {
return iconMap.email
return iconMap.cellEmail
} else if (isYear(column, abstractType)) {
return iconMap.calendar
return iconMap.cellDate
} else if (isTime(column, abstractType)) {
return iconMap.clock
return iconMap.cellTime
} else if (isRating(column)) {
return iconMap.rating
return iconMap.cellRating
} else if (isAttachment(column)) {
return iconMap.image
return iconMap.cellAttachment
} else if (isDecimal(column)) {
return iconMap.decimal
return iconMap.cellDecimal
} else if (isPhoneNumber(column)) {
return iconMap.phone
return iconMap.cellPhone
} else if (isURL(column)) {
return iconMap.web
return iconMap.cellUrl
} else if (isCurrency(column)) {
return iconMap.currency
return iconMap.cellCurrency
} else if (isDuration(column)) {
return iconMap.duration
return iconMap.cellDuration
} else if (isPercent(column)) {
return iconMap.percent
return iconMap.cellPercent
} else if (isGeometry(column)) {
return iconMap.calculator
return iconMap.cellGeometry
} else if (isUser(column)) {
if ((column.meta as { is_multi?: boolean; notify?: boolean })?.is_multi) {
return iconMap.phUsers
return iconMap.cellUser
}
return iconMap.phUser
return iconMap.cellUser
} else if (isInt(column, abstractType) || isFloat(column, abstractType)) {
return iconMap.number
return iconMap.cellNumber
} else if (isString(column, abstractType)) {
return iconMap.text
return iconMap.cellText
} else {
return iconMap.generic
return iconMap.cellSystemText
}
}

32
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -1,7 +1,7 @@
import type { PropType } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
ColumnInj,
MetaInj,
@ -33,44 +33,44 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
}
break
case UITypes.SpecificDBType:
return { icon: iconMap.specificDbType, color: 'text-grey' }
return { icon: iconMap.cellDb, color: 'text-grey' }
case UITypes.Formula:
return { icon: iconMap.formula, color: 'text-grey' }
return { icon: iconMap.cellFormula, color: 'text-grey' }
case UITypes.QrCode:
return { icon: iconMap.qrCode, color: 'text-grey' }
return { icon: iconMap.cellQrCode, color: 'text-grey' }
case UITypes.Barcode:
return { icon: iconMap.barCode, color: 'text-grey' }
return { icon: iconMap.cellBarcode, color: 'text-grey' }
case UITypes.Lookup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return { icon: iconMap.lookup, color: 'text-pink-500' }
return { icon: iconMap.cellLookup, color: 'text-pink-500' }
case RelationTypes.HAS_MANY:
return { icon: iconMap.lookup, color: 'text-orange-500' }
return { icon: iconMap.cellLookup, color: 'text-orange-500' }
case RelationTypes.BELONGS_TO:
return { icon: iconMap.lookup, color: 'text-blue-500' }
return { icon: iconMap.cellLookup, color: 'text-blue-500' }
}
return { icon: iconMap.lookup, color: 'text-grey' }
return { icon: iconMap.cellLookup, color: 'text-grey' }
case UITypes.Rollup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return { icon: iconMap.rollup, color: 'text-pink-500' }
return { icon: iconMap.cellRollup, color: 'text-pink-500' }
case RelationTypes.HAS_MANY:
return { icon: iconMap.rollup, color: 'text-orange-500' }
return { icon: iconMap.cellRollup, color: 'text-orange-500' }
case RelationTypes.BELONGS_TO:
return { icon: iconMap.rollup, color: 'text-blue-500' }
return { icon: iconMap.cellRollup, color: 'text-blue-500' }
}
return { icon: iconMap.rollup, color: 'text-grey' }
return { icon: iconMap.cellRollup, color: 'text-grey' }
case UITypes.Count:
return { icon: CountIcon, color: 'text-grey' }
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
return { icon: iconMap.datetime, color: 'text-grey' }
return { icon: iconMap.cellSystemDate, color: 'text-grey' }
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
return { icon: iconMap.phUser, color: 'text-grey' }
return { icon: iconMap.cellSystemUser, color: 'text-grey' }
}
return { icon: iconMap.generic, color: 'text-grey' }
return { icon: iconMap.cellSystemText, color: 'text-grey' }
}
export default defineComponent({

121
packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue

@ -28,7 +28,7 @@ const IsPublic = inject(IsPublicInj, ref(false))
const { loadViewColumns } = useViewColumnsOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData } = useCalendarViewStoreOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData, fetchActiveDates } = useCalendarViewStoreOrThrow()
const calendarRangeDropdown = ref(false)
@ -71,7 +71,7 @@ const saveCalendarRanges = async () => {
calendar_range: calRanges as CalendarRangeType[],
})
await loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData()])
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
} catch (e) {
console.log(e)
message.error('There was an error while updating view!')
@ -93,8 +93,6 @@ const dateFieldOptions = computed<SelectProps['options']>(() => {
)
})
// TODO: Add support for end date in future
// To add new calendar range
/* const addCalendarRange = async () => {
_calendar_ranges.value.push({
fk_from_column_id: dateFieldOptions.value![0].value as string,
@ -132,7 +130,7 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-button>
</div>
<template #overlay>
<div v-if="calendarRangeDropdown" class="w-full p-6 w-[22rem]" data-testid="nc-calendar-range-menu" @click.stop>
<div v-if="calendarRangeDropdown" class="w-full p-6 w-[23rem]" data-testid="nc-calendar-range-menu" @click.stop>
<div
v-for="(range, id) in _calendar_ranges"
:key="id"
@ -167,69 +165,60 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-select-option>
</NcSelect>
<!--
TODO: Add support for end date and multiple range in future
<div
v-if="range.fk_to_column_id === null && isEeUI"
class="flex cursor-pointer flex text-gray-800 items-center gap-1"
data-testid="nc-calendar-range-add-end-date"
@click="saveCalendarRange(range, undefined)"
>
<component :is="iconMap.plus" class="h-4 w-4" />
{{ $t('activity.addEndDate') }}
</div>
<template v-else-if="isEeUI && false">
<span>
{{ $t('activity.withEndDate') }}
</span>
<div class="flex">
<NcSelect
v-model:value="range.fk_to_column_id"
:disabled="!range.fk_from_column_id"
:placeholder="$t('placeholder.notSelected')"
class="!rounded-r-none nc-to-select"
data-testid="nc-calendar-range-to-field-select"
@change="saveCalendarRanges"
>
<a-select-option
v-for="(option, opId) in [...dateFieldOptions].filter((f) => {
const firstRange = dateFieldOptions.find((f) => f.value === calendarRange[0].fk_from_column_id)
return firstRange?.uidt === f.uidt
})"
:key="opId"
:value="option.value"
>
<div class="flex items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
</a-select-option>
</NcSelect>
<NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="saveCalendarRange(range, null)">
<component :is="iconMap.delete" class="h-4 w-4" />
</NcButton>
</div>
</template>
<NcButton v-if="id !== 0" size="small" type="secondary" @click="removeRange(id)">
<component :is="iconMap.close" />
</NcButton>
<!-- <div
v-if="range.fk_to_column_id === null && isEeUI"
class="flex cursor-pointer flex text-gray-800 items-center gap-1"
data-testid="nc-calendar-range-add-end-date"
@click="saveCalendarRange(range, 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="!range.fk_from_column_id"
:placeholder="$t('placeholder.notSelected')"
class="!rounded-r-none nc-to-select"
data-testid="nc-calendar-range-to-field-select"
@change="saveCalendarRanges"
>
<a-select-option
v-for="(option, opId) in [...dateFieldOptions].filter((f) => {
const firstRange = dateFieldOptions.find((f) => f.value === calendarRange[0].fk_from_column_id)
return firstRange?.uidt === f.uidt
})"
:key="opId"
:value="option.value"
>
<div class="flex items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
<NcButton
v-if="false"
class="mt-2"
data-testid="nc-calendar-range-add-btn"
size="small"
type="secondary"
@click="addCalendarRange"
>
<component :is="iconMap.plus" />
Add another date field
</NcButton>
-->
</a-select-option>
</NcSelect>
<NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="saveCalendarRange(range, null)">
<component :is="iconMap.delete" class="h-4 w-4" />
</NcButton>
</div>
</template>
<NcButton v-if="id !== 0" size="small" type="secondary" @click="removeRange(id)">
<component :is="iconMap.close" />
</NcButton>
-->
</div>
<!-- <NcButton class="mt-2" data-testid="nc-calendar-range-add-btn" size="small" type="secondary" @click="addCalendarRange">
<component :is="iconMap.plus" />
Add another date field
</NcButton> -->
</div>
</template>
</NcDropdown>

18
packages/nc-gui/components/webhook/Editor.vue

@ -9,6 +9,7 @@ import {
fieldRequiredValidator,
iconMap,
inject,
isEeUI,
message,
onMounted,
parseProp,
@ -64,7 +65,7 @@ let hookRef = reactive<
type: 'URL',
payload: {
method: 'POST',
body: '{{ json data }}',
body: '{{ json event }}',
headers: [{}],
parameters: [{}],
path: '',
@ -75,9 +76,10 @@ let hookRef = reactive<
version: 'v2',
})
const isBodyShown = ref(hookRef.version === 'v1')
const isBodyShownEasterEgg = ref(false)
const isBodyShown = ref(hookRef.version === 'v1' || isEeUI)
const urlTabKey = ref(isBodyShown.value ? 'body' : 'params')
const urlTabKey = ref(isBodyShownEasterEgg.value && isBodyShown.value ? 'body' : 'params')
const apps: Record<string, any> = ref()
@ -301,7 +303,8 @@ function onNotificationTypeChange(reset = false) {
}
if (hookRef.notification.type === 'URL') {
hookRef.notification.payload.body = hookRef.notification.payload.body || '{{ json data }}'
const body = hookRef.notification.payload.body
hookRef.notification.payload.body = body ? (body === '{{ json data }}' ? '{{ json event }}' : body) : '{{ json event }}'
hookRef.notification.payload.parameters = hookRef.notification.payload.parameters || [{}]
hookRef.notification.payload.headers = hookRef.notification.payload.headers || [{}]
hookRef.notification.payload.method = hookRef.notification.payload.method || 'POST'
@ -318,7 +321,7 @@ function setHook(newHook: HookType) {
payload: notification.payload,
},
})
if (hookRef.version === 'v1') {
if (hookRef.version === 'v1' || isEeUI) {
urlTabKey.value = 'body'
eventList.value = [
{ text: ['After', 'Insert'], value: ['after', 'insert'] },
@ -654,6 +657,7 @@ onMounted(async () => {
size="large"
class="nc-select-hook-url-method"
dropdown-class-name="nc-dropdown-hook-notification-url-method"
@dblclick="isBodyShownEasterEgg = !isBodyShownEasterEgg"
>
<a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title">
<div class="flex items-center gap-2 justify-between">
@ -683,7 +687,7 @@ onMounted(async () => {
<a-col :span="24">
<NcTabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="border-1 !pb-2 !rounded-lg">
<a-tab-pane v-if="isBodyShown" key="body" tab="Body">
<a-tab-pane v-if="isBodyShown && isBodyShownEasterEgg" key="body" tab="Body">
<LazyMonacoEditor
v-model="hookRef.notification.payload.body"
disable-deep-compare
@ -803,7 +807,7 @@ onMounted(async () => {
<a-row>
<a-col :span="24">
<div v-if="isBodyShown" class="text-gray-600">
<div v-if="isBodyShown && isBodyShownEasterEgg" class="text-gray-600">
<div class="flex items-center">
<em
>{{ $t('msg.webhookBodyMsg1') }} <strong>{{ $t('msg.webhookBodyMsg2') }}</strong>

21
packages/nc-gui/composables/useCalendarViewStore.ts

@ -1,13 +1,14 @@
import type { ComputedRef, Ref } from 'vue'
import {
import type {
type Api,
CalendarRangeType,
type CalendarType,
type ColumnType,
type PaginatedType,
type TableType,
UITypes,
type ViewType,
} from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs'
import { extractPkFromRow, extractSdkResponseErrorMsg, rowPkData } from '~/utils'
import { IsPublicInj, type Row, ref, storeToRefs, useBase, useInjectionState, useUndoRedo } from '#imports'
@ -110,14 +111,15 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calendarRange = ref<
Array<{
fk_from_col: ColumnType | null
fk_to_col: ColumnType | null
fk_from_col: ColumnType
fk_to_col?: ColumnType | null
id: string
}>
>([])
const calDataType = computed(() => {
if (!calendarRange.value || !calendarRange.value[0]) return null
return calendarRange.value[0]!.fk_from_col!.uidt
return calendarRange.value[0]?.fk_from_col?.uidt
})
const sideBarFilter = computed(() => {
@ -418,8 +420,9 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calMeta = typeof res.meta === 'string' ? JSON.parse(res.meta) : res.meta
activeCalendarView.value = calMeta?.active_view
if (!activeCalendarView.value) activeCalendarView.value = 'month'
calendarRange.value = res?.calendar_range?.map((range: any) => {
calendarRange.value = res?.calendar_range?.map((range: CalendarRangeType) => {
return {
id: range.id,
fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id),
fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null,
}
@ -730,9 +733,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
watch(activeCalendarView, async (value, oldValue) => {
if (oldValue === 'week') {
pageDate.value = selectedDate.value
selectedDate.value = selectedDateRange.value.start
selectedMonth.value = selectedDateRange.value.start
selectedTime.value = selectedDateRange.value.start
selectedMonth.value = selectedDate.value ?? selectedDateRange.value.start
selectedTime.value = selectedDate.value ?? selectedDateRange.value.start
} else if (oldValue === 'month') {
selectedDate.value = selectedMonth.value
pageDate.value = selectedDate.value
@ -784,6 +786,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
})
return {
fetchActiveDates,
formattedSideBarData,
loadMoreSidebarData,
loadSidebarData,

252
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -1,5 +1,6 @@
import useVuelidate from '@vuelidate/core'
import { helpers, minLength, required } from '@vuelidate/validators'
import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type {
BoolType,
@ -7,18 +8,22 @@ import type {
FormColumnType,
FormType,
LinkToAnotherRecordType,
SelectOptionsType,
StringOrNullType,
TableType,
} from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vue/shared'
import { filterNullOrUndefinedObjectProperties } from '~/helpers/parsers/parserHelpers'
import {
PreFilledMode,
SharedViewPasswordInj,
computed,
createEventHook,
extractSdkResponseErrorMsg,
isNumericFieldType,
message,
parseProp,
provide,
ref,
storeToRefs,
@ -48,8 +53,15 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const sharedFormView = ref<FormType>()
const meta = ref<TableType>()
const columns =
ref<(ColumnType & { required?: BoolType; show?: BoolType; label?: StringOrNullType; enable_scanner?: BoolType })[]>()
const columns = ref<
(ColumnType & {
required?: BoolType
show?: BoolType
label?: StringOrNullType
enable_scanner?: BoolType
read_only?: boolean
})[]
>()
const sharedViewMeta = ref<SharedViewMeta>({})
const formResetHook = createEventHook<void>()
@ -58,15 +70,20 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const { metas, setMeta } = useMetas()
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
const { base, sqlUis } = storeToRefs(baseStore)
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const { t } = useI18n()
const route = useRoute()
const formState = ref<Record<string, any>>({})
const preFilledformState = ref<Record<string, any>>({})
const { state: additionalState } = useProvideSmartsheetRowStore(
meta as Ref<TableType>,
ref({
@ -80,7 +97,11 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required)
const formColumns = computed(() =>
columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || isLinksOrLTAR(col.uidt)),
columns.value
?.filter((c) => c.show)
.filter(
(col) => !isSystemColumn(col) && col.uidt !== UITypes.SpecificDBType && (!isVirtualCol(col) || isLinksOrLTAR(col.uidt)),
),
)
const loadSharedView = async () => {
@ -107,11 +128,25 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
{} as Record<string, FormColumnType>,
)
columns.value = viewMeta.model?.columns?.map((c) => ({
...c,
meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) },
description: fieldById[c.id].description,
}))
columns.value = viewMeta.model?.columns?.map((c) => {
if (
!isSystemColumn(c) &&
!isVirtualCol(c) &&
!isAttachment(c) &&
c.uidt !== UITypes.SpecificDBType &&
c?.title &&
c?.cdf &&
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf)
) {
formState.value[c.title] = c.cdf
}
return {
...c,
meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) },
description: fieldById[c.id].description,
}
})
const _sharedViewMeta = (viewMeta as any).meta
sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta
@ -136,6 +171,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
if (viewMeta.users) {
basesUser.value.set(viewMeta.base_id, viewMeta.users)
}
handlePreFillForm()
} catch (e: any) {
if (e.response && e.response.status === 404) {
notFound.value = true
@ -233,10 +270,203 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
formResetHook.trigger()
additionalState.value = {}
formState.value = {}
formState.value = {
...([PreFilledMode.Locked, PreFilledMode.Hidden].includes(sharedViewMeta.value.preFilledMode)
? preFilledformState.value
: {}),
}
v$.value?.$reset()
}
function handlePreFillForm() {
if (Object.keys(route.query || {}).length && sharedViewMeta.value.preFillEnabled) {
columns.value = (columns.value || []).map((c) => {
const queryParam = route.query[c.title as string] || route.query[encodeURIComponent(c.title as string)]
if (
!c.title ||
!queryParam ||
isSystemColumn(c) ||
isVirtualCol(c) ||
isAttachment(c) ||
c.uidt === UITypes.SpecificDBType
) {
return c
}
const preFillValue = getPreFillValue(c, decodeURIComponent(queryParam as string).trim())
if (preFillValue !== undefined) {
// Prefill form state
formState.value[c.title] = preFillValue
// preFilledformState will be used in clear for to fill the filled data
preFilledformState.value[c.title] = preFillValue
// Update column
switch (sharedViewMeta.value.preFilledMode) {
case PreFilledMode.Hidden: {
c.show = false
break
}
case PreFilledMode.Locked: {
c.read_only = true
break
}
}
}
return c
})
}
}
function getColAbstractType(c: ColumnType) {
return (c?.source_id ? sqlUis.value[c?.source_id] : Object.values(sqlUis.value)[0]).getAbstractType(c)
}
function getPreFillValue(c: ColumnType, value: string) {
let preFillValue: any
switch (c.uidt) {
case UITypes.SingleSelect:
case UITypes.MultiSelect:
case UITypes.User: {
const limitOptions = (parseProp(c.meta).isLimitOption ? parseProp(c.meta).limitOptions || [] : []).reduce((ac, op) => {
if (op?.id) {
ac[op.id] = op
}
return ac
}, {})
const queryOptions = value.split(',')
let options: string[] = []
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(c.uidt as UITypes)) {
options = ((c.colOptions as SelectOptionsType)?.options || [])
.filter((op) => {
if (
op?.id &&
op?.title &&
queryOptions.includes(op.title) &&
(limitOptions[op.id]
? limitOptions[op.id]?.show
: parseProp(c.meta).isLimitOption
? !(parseProp(c.meta).limitOptions || []).length
: true)
) {
return true
}
return false
})
.map((op) => op.title as string)
if (options.length) {
preFillValue = c.uidt === UITypes.SingleSelect ? options[0] : options.join(',')
}
} else {
options = (meta.value?.base_id ? basesUser.value.get(meta.value.base_id) || [] : [])
.filter((user) => {
if (
user?.id &&
user?.email &&
(queryOptions.includes(user.email) || queryOptions.includes(user.id)) &&
(limitOptions[user.id]
? limitOptions[user.id]?.show
: parseProp(c.meta).isLimitOption
? !(parseProp(c.meta).limitOptions || []).length
: true)
) {
return true
}
return false
})
.map((user) => user.email)
if (options.length) {
preFillValue = !parseProp(c.meta)?.is_multi ? options[0] : options.join(',')
}
}
break
}
case UITypes.Checkbox: {
if (['true', '1'].includes(value.toLowerCase())) {
preFillValue = true
} else if (['false', '0'].includes(value.toLowerCase())) {
preFillValue = false
}
break
}
case UITypes.Rating: {
if (!isNaN(Number(value))) {
preFillValue = Math.min(Math.max(Number(value), 0), parseProp(c.meta).max ?? 5)
}
break
}
case UITypes.URL: {
if (parseProp(c.meta).validate) {
if (isValidURL(value)) {
preFillValue = value
}
} else {
preFillValue = value
}
break
}
case UITypes.Year: {
if (/^\d+$/.test(value)) {
preFillValue = Number(value)
}
break
}
case UITypes.Date: {
const parsedDate = dayjs(value)
if ((parsedDate.isValid() && parsedDate.toISOString() === value) || dayjs(value, 'YYYY-MM-DD').isValid()) {
preFillValue = dayjs(value).format('YYYY-MM-DD')
}
break
}
case UITypes.DateTime: {
const parsedDateTime = dayjs(value)
if (
(parsedDateTime.isValid() && parsedDateTime.toISOString() === value) ||
dayjs(value, 'YYYY-MM-DD HH:mm:ss').isValid()
) {
preFillValue = dayjs(value).utc().format('YYYY-MM-DD HH:mm:ssZ')
}
break
}
case UITypes.Time: {
let parsedTime = dayjs(value)
if (!parsedTime.isValid()) {
parsedTime = dayjs(value, 'HH:mm:ss')
}
if (!parsedTime.isValid()) {
parsedTime = dayjs(`1999-01-01 ${value}`)
}
if (parsedTime.isValid()) {
preFillValue = parsedTime.format(baseStore.isMysql(c.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ')
}
break
}
case UITypes.LinkToAnotherRecord:
case UITypes.Links: {
// Todo: create an api which will fetch query params records and then autofill records
break
}
default: {
if (isNumericFieldType(c, getColAbstractType(c))) {
if (!isNaN(Number(value))) {
preFillValue = Number(value)
}
} else {
preFillValue = value
}
}
}
return preFillValue
}
/** reset form if show_blank_form is true */
watch(submitted, (nextVal) => {
if (nextVal && sharedFormView.value?.show_blank_form) {

2
packages/nc-gui/composables/useViewData.ts

@ -297,7 +297,7 @@ export function useViewData(
fk_column_id: c.id,
fk_view_id: viewMeta.value?.id,
...(fieldById[c.id!] ? fieldById[c.id!] : {}),
meta: { ...parseProp(fieldById[c.id!]?.meta), ...parseProp(c.meta) }, // TODO: discuss with @pranav
meta: { ...parseProp(fieldById[c.id!]?.meta), ...parseProp(c.meta) },
order: (fieldById[c.id!] && fieldById[c.id!].order) || order++,
id: fieldById[c.id!] && fieldById[c.id!].id,
}))

6
packages/nc-gui/lang/en.json

@ -928,8 +928,8 @@
"addFieldFromFormView": "Add Field",
"selectAllFields": "Select all fields",
"preFilledFields": {
"title": "Pre-filled Fields",
"default": "Allow pre-filling fields",
"title": "Enable Pre-fill",
"default": "Default",
"locked": "Lock pre-filled fields as read-only",
"hidden": "Hide pre-filled fields",
"lockedFieldTooltip": "Pre-filled value"
@ -965,7 +965,7 @@
"clientKey": "Select .key file",
"clientCert": "Select .cert file",
"clientCA": "Select CA file",
"preFillFormInfo": "To get a prefilled link, make sure you’ve filled the necessary fields in the form view builder.",
"preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page"
},
"placeholder": {

6
packages/nc-gui/lib/enums.ts

@ -136,3 +136,9 @@ export enum ImportSource {
URL = 'url',
STRING = 'string',
}
export enum PreFilledMode {
Default = 'default',
Hidden = 'hidden',
Locked = 'locked',
}

8
packages/nc-gui/lib/types.ts

@ -2,7 +2,7 @@ import type { BaseType, ColumnType, FilterType, MetaType, PaginatedType, Roles,
import type { I18n } from 'vue-i18n'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import type { UploadFile } from 'ant-design-vue'
import type { ImportSource, ImportType, TabType } from './enums'
import type { ImportSource, ImportType, PreFilledMode, TabType } from './enums'
import type { rolePermissions } from './acl'
interface User {
@ -75,6 +75,10 @@ interface Row {
id?: string
position?: string
dayIndex?: number
overLapIteration?: number
numberOfOverlaps?: number
minutes?: number
}
}
@ -111,6 +115,8 @@ interface SharedViewMeta extends Record<string, any> {
theme?: Partial<ThemeConfig>
allowCSVDownload?: boolean
rtl?: boolean
preFillEnabled?: boolean
preFilledMode?: PreFilledMode
}
interface SharedView {

7
packages/nc-gui/package.json

@ -15,7 +15,7 @@
"url": "https://github.com/nocodb/nocodb/issues"
},
"engines": {
"node": ">=18"
"node": ">=18.19.1"
},
"license": "AGPL-3.0-or-later",
"web-types": "web-types.json",
@ -39,6 +39,7 @@
"@braks/revue-draggable": "^0.4.3",
"@ckpack/vue-color": "^1.5.0",
"@iconify/vue": "^4.1.1",
"@nuxt/image": "^1.3.0",
"@pinia/nuxt": "^0.5.1",
"@tiptap/extension-link": "2.2.4",
"@tiptap/extension-placeholder": "^2.2.4",
@ -116,7 +117,7 @@
"@iconify-json/ion": "^1.1.15",
"@iconify-json/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.171",
"@iconify-json/lucide": "^1.1.172",
"@iconify-json/material-symbols": "^1.1.74",
"@iconify-json/mdi": "^1.1.64",
"@iconify-json/mi": "^1.1.8",
@ -167,4 +168,4 @@
"vitest": "^1.2.2",
"windicss": "^3.5.6"
}
}
}

26
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -87,6 +87,15 @@ p {
&:not(.layout-list) {
@apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden;
&.readonly {
@apply bg-gray-50 cursor-not-allowed;
input,
textarea {
@apply !bg-transparent;
}
}
& > div {
@apply !bg-transparent;
}
@ -107,11 +116,17 @@ p {
}
}
&:not(.readonly) {
input,
textarea,
&.nc-virtual-cell {
@apply bg-white !disabled:bg-transparent;
}
}
input,
textarea,
&.nc-virtual-cell {
@apply bg-white dark:(bg-slate-500 text-white);
.ant-btn {
@apply dark:(bg-slate-300);
}
@ -120,9 +135,7 @@ p {
@apply dark:(bg-slate-700 text-white);
}
}
&:not(.layout-list) > div {
@apply bg-white dark:(bg-slate-500 text-white);
}
&.layout-list > div {
.ant-btn {
@apply dark:(bg-slate-300);
@ -138,6 +151,9 @@ p {
& > div {
@apply w-full;
}
&.readonly > div {
@apply px-3 py-1;
}
textarea {
@apply px-3;

69
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue

@ -180,39 +180,44 @@ const onDecode = async (scannedCodeValue: string) => {
</div>
<div>
<LazySmartsheetDivDataCell class="flex relative">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
:model-value="null"
class="mt-0 nc-input nc-cell"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
/>
<NcTooltip :disabled="!field?.read_only">
<template #title> {{ $t('activity.preFilledFields.lockedFieldTooltip') }} </template>
<LazySmartsheetDivDataCell class="flex relative">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
:model-value="null"
class="mt-0 nc-input nc-cell"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="[`nc-form-input-${field.title?.replaceAll(' ', '')}`, { readonly: field?.read_only }]"
:column="field"
:read-only="field?.read_only"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input truncate"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="[
`nc-form-input-${field.title?.replaceAll(' ', '')}`,
{ 'layout-list': parseProp(field?.meta)?.isList },
]"
:column="field"
edit-enabled
/>
<a-button
v-if="field.enable_scanner"
class="nc-btn-fill-form-column-by-scan nc-toolbar-btn"
:alt="$t('activity.fillByCodeScan')"
@click="showCodeScannerForFieldTitle(field.title)"
>
<div class="flex items-center gap-1">
<component :is="iconMap.qrCodeScan" class="h-5 w-5" />
</div>
</a-button>
</LazySmartsheetDivDataCell>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input truncate"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="[
`nc-form-input-${field.title?.replaceAll(' ', '')}`,
{ 'layout-list': parseProp(field?.meta)?.isList, 'readonly': field?.read_only },
]"
:column="field"
:edit-enabled="!field?.read_only"
:read-only="field?.read_only"
/>
<a-button
v-if="field.enable_scanner"
class="nc-btn-fill-form-column-by-scan nc-toolbar-btn"
:alt="$t('activity.fillByCodeScan')"
@click="showCodeScannerForFieldTitle(field.title)"
>
<div class="flex items-center gap-1">
<component :is="iconMap.qrCodeScan" class="h-5 w-5" />
</div>
</a-button>
</LazySmartsheetDivDataCell>
</NcTooltip>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-sm mt-2">
<template v-if="isVirtualCol(field)">

68
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue

@ -280,37 +280,45 @@ onMounted(() => {
<LazyCellRichText :value="field?.description" class="!h-auto -ml-1" is-form-field read-only sync-value-change />
</div>
<LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
v-model="formState[field.title]"
class="mt-0 nc-input h-auto"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input h-auto"
:class="parseProp(field?.meta)?.isList ? 'layout-list' : ''"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
edit-enabled
/>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
<NcTooltip :disabled="!field?.read_only">
<template #title> {{ $t('activity.preFilledFields.lockedFieldTooltip') }} </template>
<LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
v-model="formState[field.title]"
class="mt-0 nc-input h-auto"
:class="{
readonly: field?.read_only,
}"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:read-only="field?.read_only"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input h-auto"
:class="{ 'layout-list': parseProp(field?.meta)?.isList, 'readonly': field?.read_only }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="!field?.read_only"
:read-only="field?.read_only"
/>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>
<div v-if="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center">
{{ $t('general.shift') }} <MdiAppleKeyboardShift class="mx-1 text-primary" /> + {{ $t('general.enter') }}
<MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> {{ $t('msg.makeLineBreak') }}
</div>
</div>
<div v-if="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center">
{{ $t('general.shift') }} <MdiAppleKeyboardShift class="mx-1 text-primary" /> + {{ $t('general.enter') }}
<MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> {{ $t('msg.makeLineBreak') }}
</div>
</div>
</LazySmartsheetDivDataCell>
</LazySmartsheetDivDataCell>
</NcTooltip>
</div>
<div class="ml-1 mt-4 flex w-full text-lg">

3
packages/nc-gui/store/views.ts

@ -116,6 +116,8 @@ export const useViewsStore = defineStore('viewsStore', () => {
// Used for Grid View Pagination
const isPaginationLoading = ref(true)
const preFillFormSearchParams = ref('')
const loadViews = async ({
tableId,
ignoreLoading,
@ -322,6 +324,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
activeSorts,
activeNestedFilters,
isActiveViewLocked,
preFillFormSearchParams,
}
})

11
packages/nc-gui/utils/cell.ts

@ -82,3 +82,14 @@ export const renderValue = (result?: any) => {
return dayjs(d).isValid() ? dayjs(d).format('YYYY-MM-DD HH:mm') : d
})
}
export const isNumericFieldType = (column: ColumnType, abstractType: any) => {
return (
isInt(column, abstractType) ||
isFloat(column, abstractType) ||
isDecimal(column) ||
isCurrency(column) ||
isPercent(column) ||
isDuration(column)
)
}

76
packages/nc-gui/utils/columnUtils.ts

@ -1,125 +1,123 @@
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { iconMap } from '#imports'
import LinkVariant from '~icons/mdi/link-variant'
import ID from '~icons/mdi/identifier'
const uiTypes = [
{
name: UITypes.Links,
icon: iconMap.link,
icon: iconMap.cellLinks,
virtual: 1,
},
{
name: UITypes.LinkToAnotherRecord,
icon: iconMap.link,
icon: iconMap.cellLinks,
virtual: 1,
deprecated: 1,
},
{
name: UITypes.Lookup,
icon: iconMap.lookup,
icon: iconMap.cellLookup,
virtual: 1,
},
{
name: UITypes.SingleLineText,
icon: iconMap.text,
icon: iconMap.cellText,
},
{
name: UITypes.LongText,
icon: iconMap.longText,
icon: iconMap.cellLongText,
},
{
name: UITypes.Number,
icon: iconMap.number,
icon: iconMap.cellNumber,
},
{
name: UITypes.Decimal,
icon: iconMap.decimal,
icon: iconMap.cellDecimal,
},
{
name: UITypes.Attachment,
icon: iconMap.image,
icon: iconMap.cellAttachment,
},
{
name: UITypes.Checkbox,
icon: iconMap.boolean,
icon: iconMap.cellCheckbox,
},
{
name: UITypes.MultiSelect,
icon: iconMap.multiSelect,
icon: iconMap.cellMultiSelect,
},
{
name: UITypes.SingleSelect,
icon: iconMap.singleSelect,
icon: iconMap.cellSingleSelect,
},
{
name: UITypes.Date,
icon: iconMap.calendar,
icon: iconMap.cellDate,
},
{
name: UITypes.Year,
icon: iconMap.calendar,
icon: iconMap.cellDate,
},
{
name: UITypes.Time,
icon: iconMap.clock,
icon: iconMap.cellTime,
},
{
name: UITypes.PhoneNumber,
icon: iconMap.phone,
icon: iconMap.cellPhone,
},
{
name: UITypes.Email,
icon: iconMap.email,
icon: iconMap.cellEmail,
},
{
name: UITypes.URL,
icon: iconMap.web,
icon: iconMap.cellUrl,
},
{
name: UITypes.Currency,
icon: iconMap.currency,
icon: iconMap.cellCurrency,
},
{
name: UITypes.Percent,
icon: iconMap.percent,
icon: iconMap.cellPercent,
},
{
name: UITypes.Duration,
icon: iconMap.duration,
icon: iconMap.cellDuration,
},
{
name: UITypes.Rating,
icon: iconMap.rating,
icon: iconMap.cellRating,
},
{
name: UITypes.Formula,
icon: iconMap.formula,
icon: iconMap.cellFormula,
virtual: 1,
},
{
name: UITypes.Rollup,
icon: iconMap.rollup,
icon: iconMap.cellRollup,
virtual: 1,
},
{
name: UITypes.DateTime,
icon: iconMap.datetime,
icon: iconMap.cellDatetime,
},
{
name: UITypes.QrCode,
icon: iconMap.qrCode,
icon: iconMap.cellQrCode,
virtual: 1,
},
{
name: UITypes.Barcode,
icon: iconMap.barCode,
icon: iconMap.cellBarcode,
virtual: 1,
},
{
name: UITypes.Geometry,
icon: iconMap.calculator,
icon: iconMap.cellGeometry,
},
{
@ -128,31 +126,31 @@ const uiTypes = [
},
{
name: UITypes.JSON,
icon: iconMap.json,
icon: iconMap.cellJson,
},
{
name: UITypes.SpecificDBType,
icon: iconMap.specificDbType,
icon: iconMap.cellDb,
},
{
name: UITypes.User,
icon: iconMap.phUser,
icon: iconMap.cellUser,
},
{
name: UITypes.CreatedTime,
icon: iconMap.datetime,
icon: iconMap.cellSystemDate,
},
{
name: UITypes.LastModifiedTime,
icon: iconMap.datetime,
icon: iconMap.cellSystemDate,
},
{
name: UITypes.CreatedBy,
icon: iconMap.phUser,
icon: iconMap.cellSystemUser,
},
{
name: UITypes.LastModifiedBy,
icon: iconMap.phUser,
icon: iconMap.cellSystemUser,
},
]
@ -162,15 +160,15 @@ const getUIDTIcon = (uidt: UITypes | string) => {
...uiTypes,
{
name: UITypes.CreatedTime,
icon: iconMap.calendar,
icon: iconMap.cellSystemDate,
},
{
name: UITypes.ID,
icon: ID,
icon: iconMap.cellSystemKey,
},
{
name: UITypes.ForeignKey,
icon: LinkVariant,
icon: iconMap.cellLinks,
},
].find((t) => t.name === uidt) || {}
).icon

6
packages/nc-gui/utils/dataUtils.ts

@ -1,4 +1,4 @@
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isColumnRequiredAndNull } from './columnUtils'
import type { Row } from '~/lib'
@ -102,9 +102,9 @@ export const rowDefaultData = (columns: ColumnType[] = []) => {
if (
!isSystemColumn(col) &&
!isVirtualCol(col) &&
!isLinksOrLTAR({ uidt: col.uidt! }) &&
![UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.Barcode, UITypes.QrCode].includes(col.uidt) &&
col?.cdf
col?.cdf &&
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(col.cdf)
) {
const defaultValue = col.cdf
acc[col.title!] = typeof defaultValue === 'string' ? defaultValue.replace(/^'/, '').replace(/'$/, '') : defaultValue

66
packages/nc-gui/utils/iconUtils.ts

@ -130,6 +130,39 @@ import NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop'
import NcLink from '~icons/nc-icons/link'
import NcCellBarcode from '~icons/nc-icons/cell-barcode'
import NcCellCheckbox from '~icons/nc-icons/cell-checkbox'
import NcCellDate from '~icons/nc-icons/cell-date'
import NcCellEmail from '~icons/nc-icons/cell-email'
import NcCellFormula from '~icons/nc-icons/cell-formula'
import NcCellCurrency from '~icons/nc-icons/cell-currency'
import NcCellDatetime from '~icons/nc-icons/cell-datetime'
import NcCellDb from '~icons/nc-icons/cell-db'
import NcCellDecimal from '~icons/nc-icons/cell-decimal'
import NcCellDuration from '~icons/nc-icons/cell-duration'
import NcCellGeometry from '~icons/nc-icons/cell-geometry'
import NcCellJson from '~icons/nc-icons/cell-json'
import NcCellLinks from '~icons/nc-icons/cell-link'
import NcCellLongText from '~icons/nc-icons/cell-longtext'
import NcCellLookup from '~icons/nc-icons/cell-lookup'
import NcCellMultiSelect from '~icons/nc-icons/cell-multiselect'
import NcCellNumber from '~icons/nc-icons/cell-number'
import NcCellPercent from '~icons/nc-icons/cell-percentage'
import NcCellPhone from '~icons/nc-icons/cell-phone'
import NcCellQrCode from '~icons/nc-icons/cell-qrcode'
import NcCellRating from '~icons/nc-icons/cell-rating'
import NcCellRollup from '~icons/nc-icons/cell-rollup'
import NcCellSingleSelect from '~icons/nc-icons/cell-select'
import NcCellText from '~icons/nc-icons/cell-text'
import NcCellTime from '~icons/nc-icons/cell-time'
import NcCellUrl from '~icons/nc-icons/cell-url'
import NcCellUser from '~icons/nc-icons/cell-user'
import NcCellSystemDate from '~icons/nc-icons/system-date'
import NcCellSystemKey from '~icons/nc-icons/system-key'
import NcCellSystemUser from '~icons/nc-icons/system-user'
import NcCellSystemText from '~icons/nc-icons/system-text'
import NcCellAttachment from '~icons/nc-icons/cell-attachment'
// keep it for reference
// todo: remove it after all icons are migrated
/* export const iconMapOld = {
@ -278,6 +311,39 @@ import NcLink from '~icons/nc-icons/link'
} as const */
export const iconMap = {
cellBarcode: NcCellBarcode,
cellCheckbox: NcCellCheckbox,
cellDate: NcCellDate,
cellEmail: NcCellEmail,
cellFormula: NcCellFormula,
cellCurrency: NcCellCurrency,
cellDatetime: NcCellDatetime,
cellDb: NcCellDb,
cellDecimal: NcCellDecimal,
cellDuration: NcCellDuration,
cellGeometry: NcCellGeometry,
cellJson: NcCellJson,
cellLinks: NcCellLinks,
cellLongText: NcCellLongText,
cellLookup: NcCellLookup,
cellMultiSelect: NcCellMultiSelect,
cellNumber: NcCellNumber,
cellPercent: NcCellPercent,
cellPhone: NcCellPhone,
cellQrCode: NcCellQrCode,
cellRating: NcCellRating,
cellRollup: NcCellRollup,
cellSingleSelect: NcCellSingleSelect,
cellText: NcCellText,
cellTime: NcCellTime,
cellUrl: NcCellUrl,
cellUser: NcCellUser,
cellSystemDate: NcCellSystemDate,
cellSystemKey: NcCellSystemKey,
cellSystemUser: NcCellSystemUser,
cellSystemText: NcCellSystemText,
cellAttachment: NcCellAttachment,
sort: Sort,
group: Group,
filter: Filter,

2
packages/noco-docs/docusaurus.config.js

@ -68,7 +68,7 @@ const config = {
sidebarPath: require.resolve("./sidebars.js"),
routeBasePath: "/",
editUrl:
"https://github.com/nocodb/nocodb/tree/develop/packages/noco-docs/docs/",
"https://github.com/nocodb/nocodb/tree/develop/packages/noco-docs/",
lastVersion: "current",
versions: {
current: {

4
packages/noco-docs/package.json

@ -45,7 +45,7 @@
"sass": "^1.71.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.0.1",
"@docusaurus/module-type-aliases": "3.1.1",
"@tsconfig/docusaurus": "^1.0.7",
"typescript": "^4.9.5"
},
@ -62,6 +62,6 @@
]
},
"engines": {
"node": ">=16.14.2"
"node": ">=16.20.2"
}
}

4
packages/nocodb/package.json

@ -138,7 +138,7 @@
"nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.4.2",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.11",
"nodemailer": "^6.9.12",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.4",
"os-locale": "^6.0.2",
@ -184,7 +184,7 @@
"@types/jest": "^29.5.12",
"@types/mocha": "^10.0.6",
"@types/multer": "^1.4.11",
"@types/node": "20.11.24",
"@types/node": "20.11.25",
"@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.16",

12
packages/nocodb/src/db/conditionV2.ts

@ -902,7 +902,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
if (qb.client.config.client === 'pg') {
@ -944,7 +945,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
if (qb.client.config.client === 'pg') {
@ -985,7 +987,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
if (qb.client.config.client === 'pg') {
@ -1028,7 +1031,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
if (qb.client.config.client === 'pg') {

1
packages/nocodb/src/helpers/webhookHelpers.ts

@ -30,6 +30,7 @@ export function parseBody(template: string, data: any): string {
return Handlebars.compile(template, { noEscape: true })({
data,
event: data,
});
}

1
packages/nocodb/src/services/calendar-datas.service.ts

@ -53,6 +53,7 @@ export class CalendarDatasService {
return await this.datasService.dataList({
...param,
...query,
viewName: view.id,
baseName: model.base_id,
tableName: model.id,
calendarLimitOverride: 3000, // TODO: make this configurable in env

275
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save