Browse Source

Merge pull request #8718 from nocodb/develop

pull/8719/head 0.250.0
github-actions[bot] 3 weeks ago committed by GitHub
parent
commit
6e2a3e0328
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      .github/workflows/publish-docs-index-typesense.yml
  2. 2
      package.json
  3. BIN
      packages/nc-gui/assets/img/views/calendar.png
  4. BIN
      packages/nc-gui/assets/img/views/form.png
  5. BIN
      packages/nc-gui/assets/img/views/gallery.png
  6. BIN
      packages/nc-gui/assets/img/views/grid.png
  7. BIN
      packages/nc-gui/assets/img/views/kanban.png
  8. 6
      packages/nc-gui/assets/nc-icons/bell.svg
  9. 11
      packages/nc-gui/assets/nc-icons/check-circle.svg
  10. 20
      packages/nc-gui/assets/nc-icons/drag.svg
  11. 5
      packages/nc-gui/assets/nc-icons/key.svg
  12. 5
      packages/nc-gui/assets/nc-icons/maximize-all.svg
  13. 14
      packages/nc-gui/assets/nc-icons/maximize.svg
  14. 5
      packages/nc-gui/assets/nc-icons/minimize-all.svg
  15. 10
      packages/nc-gui/assets/nc-icons/minimize.svg
  16. 19
      packages/nc-gui/assets/style.scss
  17. 8
      packages/nc-gui/components/cell/Checkbox.vue
  18. 2
      packages/nc-gui/components/cell/Currency.vue
  19. 18
      packages/nc-gui/components/cell/DatePicker.vue
  20. 113
      packages/nc-gui/components/cell/DateTimePicker.vue
  21. 4
      packages/nc-gui/components/cell/Decimal.vue
  22. 6
      packages/nc-gui/components/cell/Duration.vue
  23. 3
      packages/nc-gui/components/cell/Email.vue
  24. 1
      packages/nc-gui/components/cell/Float.vue
  25. 35
      packages/nc-gui/components/cell/GeoData.vue
  26. 5
      packages/nc-gui/components/cell/Integer.vue
  27. 50
      packages/nc-gui/components/cell/Json.vue
  28. 1
      packages/nc-gui/components/cell/MultiSelect.vue
  29. 2
      packages/nc-gui/components/cell/Percent.vue
  30. 3
      packages/nc-gui/components/cell/PhoneNumber.vue
  31. 4
      packages/nc-gui/components/cell/ReadOnlyUser.vue
  32. 31
      packages/nc-gui/components/cell/RichText.vue
  33. 12
      packages/nc-gui/components/cell/RichText/LinkOptions.vue
  34. 10
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  35. 3
      packages/nc-gui/components/cell/SingleSelect.vue
  36. 1
      packages/nc-gui/components/cell/Text.vue
  37. 31
      packages/nc-gui/components/cell/TextArea.vue
  38. 53
      packages/nc-gui/components/cell/TimePicker.vue
  39. 5
      packages/nc-gui/components/cell/Url.vue
  40. 18
      packages/nc-gui/components/cell/YearPicker.vue
  41. 2
      packages/nc-gui/components/cell/attachment/Image.vue
  42. 1
      packages/nc-gui/components/cell/attachment/Modal.vue
  43. 2
      packages/nc-gui/components/cell/attachment/index.vue
  44. 222
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  45. 9
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  46. 10
      packages/nc-gui/components/dashboard/View.vue
  47. 2
      packages/nc-gui/components/dashboard/settings/Modal.vue
  48. 38
      packages/nc-gui/components/dlg/AirtableImport.vue
  49. 46
      packages/nc-gui/components/dlg/ColumnUpdateConfirm.vue
  50. 6
      packages/nc-gui/components/dlg/InviteDlg.vue
  51. 197
      packages/nc-gui/components/general/AdvanceColorPicker.vue
  52. 5
      packages/nc-gui/components/general/EmojiPicker.vue
  53. 8
      packages/nc-gui/components/general/Loader.vue
  54. 20
      packages/nc-gui/components/general/TruncateText.vue
  55. 10
      packages/nc-gui/components/general/ViewIcon.vue
  56. 21
      packages/nc-gui/components/nc/Button.vue
  57. 2
      packages/nc-gui/components/nc/DateWeekSelector.vue
  58. 18
      packages/nc-gui/components/nc/MenuItem.vue
  59. 26
      packages/nc-gui/components/nc/MonthYearSelector.vue
  60. 49
      packages/nc-gui/components/nc/Switch.vue
  61. 4
      packages/nc-gui/components/nc/TimeSelector.vue
  62. 169
      packages/nc-gui/components/notification/Card.vue
  63. 28
      packages/nc-gui/components/notification/Item.vue
  64. 17
      packages/nc-gui/components/notification/Item/ColumnEvent.vue
  65. 17
      packages/nc-gui/components/notification/Item/FilterViewEvent.vue
  66. 38
      packages/nc-gui/components/notification/Item/ProjectEvent.vue
  67. 12
      packages/nc-gui/components/notification/Item/ProjectInvite.vue
  68. 38
      packages/nc-gui/components/notification/Item/SharedViewEvent.vue
  69. 17
      packages/nc-gui/components/notification/Item/SortViewEvent.vue
  70. 38
      packages/nc-gui/components/notification/Item/TableEvent.vue
  71. 38
      packages/nc-gui/components/notification/Item/ViewEvent.vue
  72. 21
      packages/nc-gui/components/notification/Item/Welcome.vue
  73. 119
      packages/nc-gui/components/notification/Item/Wrapper.vue
  74. 36
      packages/nc-gui/components/notification/Menu.vue
  75. 4
      packages/nc-gui/components/project/AccessSettings.vue
  76. 11
      packages/nc-gui/components/project/AllTables.vue
  77. 12
      packages/nc-gui/components/project/View.vue
  78. 94
      packages/nc-gui/components/shared-view/AskPassword.vue
  79. 4
      packages/nc-gui/components/shared-view/Calendar.vue
  80. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  81. 2
      packages/nc-gui/components/shared-view/Grid.vue
  82. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  83. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  84. 2
      packages/nc-gui/components/smartsheet/Details.vue
  85. 12
      packages/nc-gui/components/smartsheet/Form.vue
  86. 329
      packages/nc-gui/components/smartsheet/Gallery.vue
  87. 1130
      packages/nc-gui/components/smartsheet/Kanban.vue
  88. 8
      packages/nc-gui/components/smartsheet/Map.vue
  89. 7
      packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue
  90. 12
      packages/nc-gui/components/smartsheet/Toolbar.vue
  91. 11
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  92. 76
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  93. 246
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  94. 104
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  95. 19
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  96. 39
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  97. 150
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  98. 2
      packages/nc-gui/components/smartsheet/calendar/YearView/index.vue
  99. 158
      packages/nc-gui/components/smartsheet/calendar/index.vue
  100. 17
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -3,7 +3,9 @@ name: "Publish : Docs search index (Typesense)"
on:
# Triggered manually
workflow_dispatch:
repository_dispatch:
types: trigger-docs-index
jobs:
doc-indexer:
runs-on: ubuntu-latest
@ -15,12 +17,12 @@ jobs:
uses: celsiusnarhwal/typesense-scraper@v2
with:
# The secret containing your Typesense API key. Required.
api-key: ${{ secrets.TYPESENSE_API_KEY }}
api-key: ${{ secrets.TYPESENSE_API_KEY }}
# The hostname or IP address of your Typesense server. Required.
host: ${{ secrets.TYPESENSE_HOST }}
host: ${{ secrets.TYPESENSE_HOST }}
# The port on which your Typesense server is listening. Optional. Default: 8108.
port: 443
port: 443
# The protocol to use when connecting to your Typesense server. Optional. Default: http.
protocol: https
protocol: https
# The path to your DocSearch config file. Optional. Default: docsearch.config.json.
config: ./packages/noco-docs/typesense-scrape-config.json
config: ./packages/noco-docs/typesense-scrape-config.json

2
package.json

@ -32,7 +32,7 @@
]
},
"scripts": {
"bootstrap": "pnpm --filter=nocodb-sdk install && pnpm --filter=nocodb-sdk run build && pnpm --filter=nocodb --filter=nc-gui --filter=playwright install",
"bootstrap": "pnpm --filter=nocodb-sdk install && pnpm --filter=nocodb-sdk run build && pnpm --filter=nocodb --filter=nc-mail-templates --filter=nc-gui --filter=playwright install",
"start:frontend": "pnpm --filter=nc-gui run dev",
"start:backend": "pnpm --filter=nocodb run start",
"lint:staged:playwright": "cd ./tests/playwright; pnpm dlx lint-staged; cd -",

BIN
packages/nc-gui/assets/img/views/calendar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
packages/nc-gui/assets/img/views/form.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
packages/nc-gui/assets/img/views/gallery.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
packages/nc-gui/assets/img/views/grid.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

BIN
packages/nc-gui/assets/img/views/kanban.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

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

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="bell">
<path id="Vector" d="M12 5.33325C12 4.27239 11.5786 3.25497 10.8284 2.50482C10.0783 1.75468 9.06087 1.33325 8 1.33325C6.93913 1.33325 5.92172 1.75468 5.17157 2.50482C4.42143 3.25497 4 4.27239 4 5.33325C4 9.99992 2 11.3333 2 11.3333H14C14 11.3333 12 9.99992 12 5.33325Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M9.15335 14C9.03614 14.2021 8.86791 14.3698 8.6655 14.4864C8.46309 14.6029 8.2336 14.6643 8.00001 14.6643C7.76643 14.6643 7.53694 14.6029 7.33453 14.4864C7.13212 14.3698 6.96389 14.2021 6.84668 14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 801 B

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

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="check-circle" clip-path="url(#clip0_494_50692)">
<path id="Vector" d="M14.6667 7.38674V8.00007C14.6659 9.43769 14.2003 10.8365 13.3396 11.988C12.4788 13.1394 11.2689 13.9817 9.89025 14.3893C8.51163 14.797 7.03818 14.748 5.68966 14.2498C4.34113 13.7516 3.18978 12.8308 2.40732 11.6248C1.62485 10.4188 1.2532 8.99212 1.34779 7.55762C1.44239 6.12312 1.99815 4.75762 2.9322 3.66479C3.86625 2.57195 5.12853 1.81033 6.5308 1.4935C7.93307 1.17668 9.40019 1.32163 10.7133 1.90674" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M14.6667 2.66675L8 9.34008L6 7.34008" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_494_50692">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 940 B

20
packages/nc-gui/assets/nc-icons/drag.svg

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M5.00016 13.3333C5.36835 13.3333 5.66683 13.0349 5.66683 12.6667C5.66683 12.2985 5.36835 12 5.00016 12C4.63197 12 4.3335 12.2985 4.3335 12.6667C4.3335 13.0349 4.63197 13.3333 5.00016 13.3333Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M5.00016 8.66683C5.36835 8.66683 5.66683 8.36835 5.66683 8.00016C5.66683 7.63197 5.36835 7.3335 5.00016 7.3335C4.63197 7.3335 4.3335 7.63197 4.3335 8.00016C4.3335 8.36835 4.63197 8.66683 5.00016 8.66683Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M5.00016 3.99984C5.36835 3.99984 5.66683 3.70136 5.66683 3.33317C5.66683 2.96498 5.36835 2.6665 5.00016 2.6665C4.63197 2.6665 4.3335 2.96498 4.3335 3.33317C4.3335 3.70136 4.63197 3.99984 5.00016 3.99984Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M11.0002 13.3333C11.3684 13.3333 11.6668 13.0349 11.6668 12.6667C11.6668 12.2985 11.3684 12 11.0002 12C10.632 12 10.3335 12.2985 10.3335 12.6667C10.3335 13.0349 10.632 13.3333 11.0002 13.3333Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M11.0002 8.66683C11.3684 8.66683 11.6668 8.36835 11.6668 8.00016C11.6668 7.63197 11.3684 7.3335 11.0002 7.3335C10.632 7.3335 10.3335 7.63197 10.3335 8.00016C10.3335 8.36835 10.632 8.66683 11.0002 8.66683Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M11.0002 3.99984C11.3684 3.99984 11.6668 3.70136 11.6668 3.33317C11.6668 2.96498 11.3684 2.6665 11.0002 2.6665C10.632 2.6665 10.3335 2.96498 10.3335 3.33317C10.3335 3.70136 10.632 3.99984 11.0002 3.99984Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

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

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M10.3333 4.99998L12.6667 2.66665M14 1.33331L12.6667 2.66665L14 1.33331ZM7.59333 7.73998C7.93756 8.07962 8.2112 8.48401 8.3985 8.92984C8.5858 9.37568 8.68306 9.85416 8.68468 10.3377C8.68631 10.8213 8.59225 11.3004 8.40794 11.7475C8.22363 12.1946 7.95271 12.6008 7.61076 12.9427C7.26882 13.2847 6.86261 13.5556 6.41554 13.7399C5.96846 13.9242 5.48933 14.0183 5.00575 14.0167C4.52218 14.015 4.0437 13.9178 3.59786 13.7305C3.15203 13.5432 2.74764 13.2695 2.408 12.9253C1.74009 12.2338 1.37051 11.3076 1.37886 10.3462C1.38722 9.38479 1.77284 8.46514 2.45267 7.78531C3.13249 7.10548 4.05214 6.71986 5.01353 6.71151C5.97492 6.70315 6.90113 7.07273 7.59267 7.74065L7.59333 7.73998ZM7.59333 7.73998L10.3333 4.99998L7.59333 7.73998ZM10.3333 4.99998L12.3333 6.99998L14.6667 4.66665L12.6667 2.66665L10.3333 4.99998Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

5
packages/nc-gui/assets/nc-icons/maximize-all.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M5.33333 2H3.33333C2.97971 2 2.64057 2.14048 2.39052 2.39052C2.14048 2.64057 2 2.97971 2 3.33333V5.33333M14 5.33333V3.33333C14 2.97971 13.8595 2.64057 13.6095 2.39052C13.3594 2.14048 13.0203 2 12.6667 2H10.6667M10.6667 14H12.6667C13.0203 14 13.3594 13.8595 13.6095 13.6095C13.8595 13.3594 14 13.0203 14 12.6667V10.6667M2 10.6667V12.6667C2 13.0203 2.14048 13.3594 2.39052 13.6095C2.64057 13.8595 2.97971 14 3.33333 14H5.33333"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 652 B

14
packages/nc-gui/assets/nc-icons/maximize.svg

@ -1,6 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 14H2V10" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 14L6.66667 9.33337" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 2H14V6" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.9999 2L9.33325 6.66667" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 14H2V10" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M2 14L6.66667 9.33331" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M10 2H14V6" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M14 2L9.33331 6.66667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 620 B

5
packages/nc-gui/assets/nc-icons/minimize-all.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M5.33333 2V4C5.33333 4.35362 5.19286 4.69276 4.94281 4.94281C4.69276 5.19286 4.35362 5.33333 4 5.33333H2M14 5.33333H12C11.6464 5.33333 11.3072 5.19286 11.0572 4.94281C10.8071 4.69276 10.6667 4.35362 10.6667 4V2M10.6667 14V12C10.6667 11.6464 10.8071 11.3072 11.0572 11.0572C11.3072 10.8071 11.6464 10.6667 12 10.6667H14M2 10.6667H4C4.35362 10.6667 4.69276 10.8071 4.94281 11.0572C5.19286 11.3072 5.33333 11.6464 5.33333 12V14"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 652 B

10
packages/nc-gui/assets/nc-icons/minimize.svg

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2.6665 9.3335H6.6665V13.3335" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M2 14.0002L6.66667 9.3335" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M13.3335 6.6665H9.3335V2.6665" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M9.3335 6.66667L14.0002 2" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 682 B

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

@ -249,9 +249,26 @@ a {
@apply !rounded-md;
}
}
.nc-select-shadow {
&.ant-select {
&:not(.ant-select-disabled):not(:hover):not(.ant-select-focused) .ant-select-selector,
&:not(.ant-select-disabled):hover.ant-select-disabled .ant-select-selector {
@apply shadow-default;
}
&:hover:not(.ant-select-focused):not(.ant-select-disabled) .ant-select-selector {
@apply border-gray-300 shadow-hover;
}
&.ant-select-disabled .ant-select-selector {
box-shadow: none;
}
}
}
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200;
@apply border-1 border-gray-200 rounded-lg;
.rc-virtual-list-scrollbar {
@apply !w-1;

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

@ -25,6 +25,8 @@ const isEditColumnMenu = inject(EditColumnInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const readOnly = inject(ReadonlyInj)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
@ -110,11 +112,11 @@ useSelectedCellKeyupListener(active, (e) => {
<div
class="flex items-center"
:class="{
'w-full justify-start': isEditColumnMenu || isGallery || isForm,
'justify-center': !isEditColumnMenu && !isGallery && !isForm,
'w-full justify-start': isEditColumnMenu || isGallery || isKanban || isForm,
'justify-center': !isEditColumnMenu && !isGallery && !isKanban && !isForm,
'py-2': isEditColumnMenu,
}"
@click="onClick(true)"
@click.stop="onClick(true)"
>
<Transition name="layout" mode="out-in" :duration="100">
<component

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

@ -108,7 +108,7 @@ onMounted(() => {
type="number"
class="nc-cell-field h-full border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder"
:disabled="readOnly"
@blur="onBlur"
@keydown.enter="onKeydownEnter"

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

@ -144,11 +144,10 @@ watch(editable, (nextValue) => {
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(columnMeta.value) && active.value)
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(columnMeta.value) && active.value) ||
isEditColumn.value
) {
return dateFormat.value
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isDateInvalid.value) {
@ -300,8 +299,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
:overlay-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
v-bind="$attrs"
:title="localState?.format(dateFormat)"
class="nc-date-picker h-full flex items-center justify-between ant-picker-input relative group"
class="nc-date-picker h-full flex items-center justify-between ant-picker-input relative"
>
<input
ref="datePickerRef"
@ -320,9 +320,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
/>
<GeneralIcon
v-if="localState"
v-if="localState && !readOnly"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-date-icon nc-action-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
@ -354,7 +354,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
.nc-cell-field {
&:hover .nc-clear-date-icon {
@apply visible;
}
}
</style>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { dateFormats, isSystemColumn, isValidTimeFormat, timeFormats } from 'nocodb-sdk'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
@ -9,8 +9,15 @@ interface Props {
}
const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const timeFormatsObj = {
[timeFormats[0]]: 'hh:mm A',
[timeFormats[1]]: 'hh:mm:ss A',
[timeFormats[2]]: 'hh:mm:ss.SSS A',
}
const { isMssql, isXcdbBase } = useBase()
const { showNull, isMobileMode } = useGlobal()
@ -183,11 +190,14 @@ watch(
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.value)
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.value) ||
isEditColumn.value
) {
return { dateTime: dateTimeFormat.value, date: dateFormat.value, time: timeFormat.value }
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
return {
dateTime: dateTimeFormat.value,
date: dateFormat.value,
time: parseProp(column.value.meta).is12hrFormat ? `${timeFormat.value} AM` : timeFormat.value,
}
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isDateInvalid.value) {
@ -346,12 +356,21 @@ const handleUpdateValue = (e: Event, _isDatePicker: boolean) => {
return
}
if (timeFormat.value === 'HH:mm' && targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
if (isValidTimeFormat(targetValue, timeFormat.value)) {
tempDate.value = dayjs(`${(tempDate.value ?? dayjs()).format('YYYY-MM-DD')} ${targetValue}`)
targetValue = parseProp(column.value.meta).is12hrFormat
? targetValue
.trim()
.toUpperCase()
.replace(/(AM|PM)$/, ' $1')
.replace(/\s+/g, ' ')
: targetValue.trim()
const parsedDate = dayjs(
targetValue,
parseProp(column.value.meta).is12hrFormat ? timeFormatsObj[timeFormat.value] : timeFormat.value,
)
if (parsedDate.isValid()) {
tempDate.value = dayjs(`${(tempDate.value ?? dayjs()).format('YYYY-MM-DD')} ${parsedDate.format(timeFormat.value)}`)
}
}
}
@ -384,35 +403,32 @@ function handleSelectTime(value: dayjs.Dayjs) {
open.value = false
}
const selectedTime = computed(() => {
const result = {
value: '',
label: '',
}
if (localState.value) {
const time = localState.value.format(timeFormat.value)
const [hours, minutes] = time.split(':')
result.value = `${hours}:${minutes}`
result.label = time
}
return result
})
const timeCellMaxWidth = computed(() => {
return {
[timeFormats[0]]: 'max-w-[65px]',
[timeFormats[1]]: 'max-w-[80px]',
[timeFormats[2]]: 'max-w-[110px]',
}[timeFormat.value]
[timeFormats[0]]: {
12: 'max-w-[85px]',
24: 'max-w-[65px]',
},
[timeFormats[1]]: {
12: 'max-w-[100px]',
24: 'max-w-[80px]',
},
[timeFormats[2]]: {
12: 'max-w-[130px]',
24: 'max-w-[110px]',
},
}[timeFormat.value][parseProp(column.value.meta).is12hrFormat ? 12 : 24]
})
const cellValue = computed(
() =>
localState.value?.format(parseProp(column.value.meta).is12hrFormat ? timeFormatsObj[timeFormat.value] : timeFormat.value) ??
'',
)
</script>
<template>
<div class="nc-cell-field group relative">
<div v-bind="$attrs" class="nc-cell-field relative">
<NcDropdown
:visible="isOpen"
:placement="isDatePicker ? 'bottomLeft' : 'bottomRight'"
@ -424,14 +440,18 @@ const timeCellMaxWidth = computed(() => {
>
<div
:title="localState?.format(dateTimeFormat)"
class="nc-date-picker ant-picker-input flex justify-between gap-2 relative group !w-auto"
class="nc-date-picker ant-picker-input flex relative !w-auto gap-2"
:class="{
'justify-between': !isColDisabled,
}"
>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border w-[60%] max-w-[110px]"
class="flex-none rounded-md box-border w-[60%] max-w-[110px]"
:class="{
'py-0': isForm,
'py-0.5': !isForm,
'py-0.5': !isForm && !isColDisabled,
'bg-gray-100': isDatePicker && isOpen,
'hover:bg-gray-100 px-1': !isColDisabled,
}"
>
<input
@ -449,19 +469,20 @@ const timeCellMaxWidth = computed(() => {
/>
</div>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border flex-1"
class="flex-none rounded-md box-border flex-1"
:class="[
`${timeCellMaxWidth}`,
{
'py-0': isForm,
'py-0.5': !isForm,
'py-0.5': !isForm && !isColDisabled,
'bg-gray-100': !isDatePicker && isOpen,
'hover:bg-gray-100 px-1': !isColDisabled,
},
]"
>
<input
ref="timePickerRef"
:value="selectedTime.value ? `${selectedTime.label}` : ''"
:value="cellValue"
:placeholder="typeof placeholder === 'string' ? placeholder : placeholder?.time"
class="nc-time-input w-full !truncate border-transparent outline-none !text-current !bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="!!isMobileMode || isColDisabled"
@ -497,6 +518,7 @@ const timeCellMaxWidth = computed(() => {
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is12hr-format="!!parseProp(column.meta).is12hrFormat"
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
@ -506,17 +528,20 @@ const timeCellMaxWidth = computed(() => {
</NcDropdown>
<GeneralIcon
v-if="localState && (isExpandedForm || isForm || !isGrid)"
v-if="localState && (isExpandedForm || isForm || !isGrid || isEditColumn) && !readOnly"
icon="closeCircle"
class="h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-date-time-icon nc-action-icon h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
.nc-cell-field {
&:hover .nc-clear-date-time-icon {
@apply visible;
}
}
</style>

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

@ -40,6 +40,8 @@ const displayValue = computed(() => {
if (isNaN(Number(_vModel.value))) return null
if (meta.value.isLocaleString) return (+Number(_vModel.value).toFixed(meta.value.precision ?? 1)).toLocaleString()
return Number(_vModel.value).toFixed(meta.value.precision ?? 1)
})
@ -102,7 +104,7 @@ watch(isExpandedFormOpen, () => {
class="nc-cell-field outline-none py-1 border-none rounded-md w-full h-full"
type="number"
:step="precision"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder"
style="letter-spacing: 0.06rem"
@blur="editEnabled = false"
@keydown.down.stop="onKeyDown"

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

@ -10,8 +10,6 @@ const { modelValue, showValidationError = true } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const { showNull } = useGlobal()
const column = inject(ColumnInj)
@ -30,9 +28,7 @@ const isEdited = ref(false)
const durationType = computed(() => parseProp(column?.value?.meta)?.duration || 0)
const durationPlaceholder = computed(() =>
isEditColumn.value ? `(${t('labels.optional')})` : durationOptions[durationType.value].title,
)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title)
const localState = computed({
get: () => convertMS2Duration(modelValue, durationType.value),

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

@ -81,7 +81,6 @@ watch(
:ref="focus"
v-model="vModel"
class="nc-cell-field w-full outline-none py-1"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -98,7 +97,7 @@ watch(
<nuxt-link
v-else-if="validEmail"
no-ref
class="py-1 underline hover:opacity-75 inline-block nc-cell-field-link max-w-full"
class="py-1 underline inline-block nc-cell-field-link max-w-full"
:href="`mailto:${vModel}`"
target="_blank"
:tabindex="readOnly ? -1 : 0"

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

@ -53,7 +53,6 @@ const focus: VNodeRef = (el) =>
class="nc-cell-field outline-none px-1 border-none w-full h-full"
type="number"
step="0.1"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

35
packages/nc-gui/components/cell/GeoData.vue

@ -151,25 +151,33 @@ const openInOSM = () => {
v-if="isLoading"
:class="{ 'animate-infinite animate-spin text-gray-500': isLoading }"
/>
<a-button class="ml-2" @click="onClickSetCurrentLocation"
><component :is="iconMap.currentLocation" class="mr-2" />{{ $t('labels.currentLocation') }}</a-button
>
<a-button class="ml-2 !rounded-lg" @click="onClickSetCurrentLocation">
<div class="flex items-center gap-1">
<component :is="iconMap.currentLocation" />{{ $t('labels.currentLocation') }}
</div>
</a-button>
</div>
</a-form-item>
<a-form-item v-if="vModel">
<div class="mr-2 flex flex-row items-end gap-1 text-left">
<a-button @click="openInOSM"
><component :is="iconMap.openInNew" class="mr-2" />{{ $t('activity.map.openInOpenStreetMap') }}</a-button
>
<a-button @click="openInGoogleMaps"
><component :is="iconMap.openInNew" class="mr-2" />{{ $t('activity.map.openInGoogleMaps') }}</a-button
>
<a-button class="!rounded-lg" @click="openInOSM">
<div class="flex items-center gap-1">
<component :is="iconMap.openInNew" />{{ $t('activity.map.openInOpenStreetMap') }}
</div>
</a-button>
<a-button class="!rounded-lg" @click="openInGoogleMaps">
<div class="flex items-center gap-1">
<component :is="iconMap.openInNew" />{{ $t('activity.map.openInGoogleMaps') }}
</div>
</a-button>
</div>
</a-form-item>
<a-form-item>
<div class="ml-auto mr-2 w-auto">
<a-button type="text" @click="clear">{{ $t('general.cancel') }}</a-button>
<a-button type="primary" html-type="submit" data-testid="nc-geo-data-save">{{ $t('general.submit') }}</a-button>
<a-button type="text" class="!rounded-lg" @click="clear">{{ $t('general.cancel') }}</a-button>
<a-button type="primary" html-type="submit" data-testid="nc-geo-data-save" class="!rounded-lg">{{
$t('general.submit')
}}</a-button>
</div>
</a-form-item>
</a-form>
@ -179,7 +187,10 @@ const openInOSM = () => {
<style scoped lang="scss">
input[type='number']:focus {
@apply ring-transparent;
@apply ring-transparent shadow-selected;
}
input[type='number'] {
@apply !border-1 !pr-1 rounded-lg;
}
input[type='number'] {

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

@ -28,6 +28,8 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const column = inject(ColumnInj, null)!
const _vModel = useVModel(props, 'modelValue', emits)
const displayValue = computed(() => {
@ -35,6 +37,8 @@ const displayValue = computed(() => {
if (isNaN(Number(_vModel.value))) return null
if (parseProp(column.value.meta).isLocaleString) return Number(_vModel.value).toLocaleString()
return Number(_vModel.value)
})
@ -96,7 +100,6 @@ function onKeyDown(e: any) {
class="nc-cell-field outline-none py-1 border-none w-full h-full"
:type="inputType"
style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown="onKeyDown"
@keydown.down.stop

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

@ -19,6 +19,8 @@ const editEnabled = inject(EditModeInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
@ -133,6 +135,27 @@ onClickOutside(inputWrapperRef, (e) => {
watch(isExpanded, () => {
_isExpanded.value = isExpanded.value
})
const stopPropagation = (event: MouseEvent) => {
event.stopPropagation()
}
watch(inputWrapperRef, () => {
if (!isEditColumn.value) return
// stop event propogation in edit to prevent close edit modal on clicking expanded modal overlay
const modal = document.querySelector('.nc-json-expanded-modal') as HTMLElement
if (isExpanded.value && modal?.parentElement) {
modal.parentElement.addEventListener('click', stopPropagation)
modal.parentElement.addEventListener('mousedown', stopPropagation)
modal.parentElement.addEventListener('mouseup', stopPropagation)
} else if (modal?.parentElement) {
modal.parentElement.removeEventListener('click', stopPropagation)
modal.parentElement.removeEventListener('mousedown', stopPropagation)
modal.parentElement.removeEventListener('mouseup', stopPropagation)
}
})
</script>
<template>
@ -142,7 +165,7 @@ watch(isExpanded, () => {
:closable="false"
centered
:footer="null"
:wrap-class-name="isExpanded ? '!z-1051' : null"
:wrap-class-name="isExpanded ? '!z-1051 nc-json-expanded-modal' : null"
>
<div v-if="editEnabled && !readOnly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
<div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
@ -152,12 +175,21 @@ watch(isExpanded, () => {
<CilFullscreen v-else class="h-2.5" />
</a-button>
<div v-if="!isForm || isExpanded" class="flex flex-row my-1">
<a-button type="text" size="small" :onclick="clear"
<div v-if="!isForm || isExpanded" class="flex flex-row my-1 space-x-1">
<a-button type="text" size="small" class="!rounded-lg" @click="clear"
><div class="text-xs">{{ $t('general.cancel') }}</div></a-button
>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel" @click="onSave">
<a-button
:type="!isExpanded ? 'text' : 'primary'"
size="small"
class="nc-save-json-value-btn !rounded-lg"
:class="{
'nc-edit-modal': !isExpanded,
}"
:disabled="!!error || localValue === vModel"
@click="onSave"
>
<div class="text-xs">{{ $t('general.save') }}</div>
</a-button>
</div>
@ -170,7 +202,7 @@ watch(isExpanded, () => {
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
:hide-minimap="true"
:disable-deep-compare="true"
:auto-focus="!isForm"
:auto-focus="!isForm && !isEditColumn"
@update:model-value="localValue = $event"
@keydown.enter.stop
/>
@ -182,7 +214,7 @@ watch(isExpanded, () => {
<span v-else-if="vModel === null && showNull" class="nc-cell-field nc-null uppercase">{{ $t('general.null') }}</span>
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" class="nc-cell-field" />
<LazyCellClampedText v-else :value="vModel ? stringifyProp(vModel) : ''" :lines="rowHeight" class="nc-cell-field" />
</component>
</template>
@ -194,4 +226,10 @@ watch(isExpanded, () => {
.editor {
min-height: min(200px, 10vh);
}
.nc-save-json-value-btn {
&.nc-edit-modal:not(:disabled) {
@apply !text-brand-500 !hover:text-brand-600;
}
}
</style>

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

@ -469,7 +469,6 @@ const onFocus = () => {
v-model:value="vModel"
mode="multiple"
class="w-full overflow-hidden"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:bordered="false"
clear-icon
:show-search="!isMobileMode"

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

@ -145,7 +145,7 @@ const onTabPress = (e: KeyboardEvent) => {
v-model="vModel"
class="nc-cell-field w-full !border-none !outline-none focus:ring-0 py-1"
:type="inputType"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder"
@blur="onBlur"
@focus="onFocus"
@keydown.down.stop

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

@ -65,7 +65,6 @@ watch(
:ref="focus"
v-model="vModel"
class="nc-cell-field w-full outline-none py-1"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@ -80,7 +79,7 @@ watch(
<a
v-else-if="validPhoneNumber"
class="py-1 underline hover:opacity-75 inline-block nc-cell-field-link"
class="py-1 underline inline-block nc-cell-field-link"
:href="`tel:${vModel}`"
target="_blank"
rel="noopener noreferrer"

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

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { UserFieldRecordType } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
modelValue?: UserFieldRecordType[] | UserFieldRecordType | string | null
}
defineProps<Props>()

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

@ -40,16 +40,28 @@ const rowHeight = inject(RowHeightInj, ref(1 as const))
const readOnlyCell = inject(ReadonlyInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isFocused = ref(false)
const keys = useMagicKeys()
const localRowHeight = computed(() => {
if (readOnlyCell.value && !isExpandedFormOpen.value && (isGallery.value || isKanban.value)) return 6
return rowHeight.value
})
const shouldShowLinkOption = computed(() => {
return isFormField.value ? isFocused.value : true
})
@ -155,7 +167,7 @@ const editor = useEditor({
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = isFormField.value && markdown === '<br />' ? '' : markdown
vModel.value = markdown === '<br />' ? '' : markdown
},
editable: !props.readOnly,
autofocus: props.autofocus,
@ -220,7 +232,7 @@ if (isFormField.value) {
}
onMounted(() => {
if (fullMode.value || isFormField.value || isForm.value) {
if (fullMode.value || isFormField.value || isForm.value || isEditColumn.value) {
setEditorContent(vModel.value, true)
if (fullMode.value || isSurveyForm.value) {
@ -320,8 +332,8 @@ onClickOutside(editorDom, (e) => {
'mt-2.5 flex-grow': fullMode,
'scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent': !fullMode || (!fullMode && isExpandedFormOpen),
'flex-grow': isExpandedFormOpen,
[`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(rowHeight)}`]:
!fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm,
[`!overflow-hidden nc-truncate nc-line-clamp-${rowHeightTruncateLines(localRowHeight)}`]:
!fullMode && readOnly && localRowHeight && !isExpandedFormOpen && !isForm,
}"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
@ -393,6 +405,17 @@ onClickOutside(editorDom, (e) => {
}
}
}
&.allow-vertical-resize:not(.readonly) {
.ProseMirror {
@apply nc-scrollbar-thin;
overflow-y: auto;
overflow-x: hidden;
resize: vertical;
min-width: 100%;
max-height: min(800px, calc(100vh - 200px)) !important;
}
}
}
.nc-rich-text-full {

12
packages/nc-gui/components/cell/RichText/LinkOptions.vue

@ -11,6 +11,7 @@ const emits = defineEmits(['blur'])
interface Props {
editor: Editor
isFormField?: boolean
isComment?: boolean
}
const { editor, isFormField } = toRefs(props)
@ -164,6 +165,9 @@ const openLink = () => {
const onMountLinkOptions = (e) => {
if (e?.popper?.style) {
if (props.isComment) {
e.popper.style.left = '-10%'
}
e.popper.style.width = '95%'
}
}
@ -233,14 +237,6 @@ const tabIndex = computed(() => {
<MdiDeleteOutline />
</NcButton>
</NcTooltip>
<div class="absolute -bottom-1.5 left-0 right-0 w-full flex flex-row justify-center">
<div
class="flex h-2.5 w-2.5 bg-white border-gray-200 border-r-1 border-b-1 transform rotate-45"
:style="{
boxShadow: '1px 1px 3px rgba(231, 231, 233, 1)',
}"
></div>
</div>
</div>
</div>
</BubbleMenu>

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

@ -27,6 +27,8 @@ const props = withDefaults(defineProps<Props>(), {
const { editor, embedMode, isFormField, hiddenOptions } = toRefs(props)
const isEditColumn = inject(EditColumnInj, ref(false))
const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL'
})
@ -108,6 +110,7 @@ const showDivider = (options: RichTextBubbleMenuOptions[]) => {
'flex bg-gray-100 px-1 py-1': !isFormField,
'embed-mode': embedMode,
'full-mode': !embedMode,
'edit-column-mode': isEditColumn,
}"
>
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
@ -172,7 +175,7 @@ const showDivider = (options: RichTextBubbleMenuOptions[]) => {
<MdiFormatUnderline />
</NcButton>
</NcTooltip>
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<NcTooltip v-if="embedMode && !isEditColumn" :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -283,7 +286,10 @@ const showDivider = (options: RichTextBubbleMenuOptions[]) => {
<div class="divider"></div>
</template>
<NcTooltip v-if="embedMode && isOptionVisible(RichTextBubbleMenuOptions.blockQuote)" :placement="tooltipPlacement">
<NcTooltip
v-if="embedMode && !isEditColumn && isOptionVisible(RichTextBubbleMenuOptions.blockQuote)"
:placement="tooltipPlacement"
>
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"

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

@ -385,9 +385,8 @@ const onFocus = () => {
v-else
ref="aselect"
v-model:value="vModel"
class="w-full overflow-hidden xs:min-h-12"
class="w-full overflow-hidden"
:class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed"
:bordered="false"
:open="isOpen && editAllowed"

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

@ -35,7 +35,6 @@ const focus: VNodeRef = (el) =>
:ref="focus"
v-model="vModel"
class="nc-cell-field h-full w-full outline-none py-1 bg-transparent"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

@ -21,6 +21,10 @@ const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
const { showNull } = useGlobal()
@ -60,6 +64,12 @@ const height = computed(() => {
return rowHeight.value * 36
})
const localRowHeight = computed(() => {
if (readOnly.value && !isExpandedFormOpen.value && (isGallery.value || isKanban.value)) return 6
return rowHeight.value
})
const isVisible = ref(false)
const inputWrapperRef = ref<HTMLElement | null>(null)
@ -199,16 +209,15 @@ watch(inputWrapperRef, () => {
>
<div v-if="isForm && isRichMode" class="w-full">
<div
class="w-full relative w-full px-0 pb-1"
class="w-full relative w-full px-0"
:class="{
'pt-11': !readOnly,
}"
>
<LazyCellRichText
v-model:value="vModel"
class="!max-h-50"
:class="{
'border-t-1 border-gray-100': !readOnly,
'border-t-1 border-gray-100 allow-vertical-resize': !readOnly,
}"
:autofocus="false"
show-menu
@ -222,11 +231,11 @@ watch(inputWrapperRef, () => {
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,
'nc-readonly-rich-text-sort-height': localRowHeight === 1 && !isExpandedFormOpen && !isForm,
}"
:style="{
maxHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(rowHeight)}px`,
minHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(rowHeight)}px`,
maxHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(localRowHeight)}px`,
minHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(localRowHeight)}px`,
}"
@dblclick="onExpand"
@keydown.enter="onExpand"
@ -246,8 +255,8 @@ watch(inputWrapperRef, () => {
}"
:style="{
minHeight: isForm ? '117px' : `${height}px`,
maxHeight: 'min(800px, calc(100vh - 200px))',
}"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:disabled="readOnly"
@blur="editEnabled = false"
@keydown.alt.enter.stop
@ -266,12 +275,12 @@ watch(inputWrapperRef, () => {
<LazyCellClampedText
v-else-if="rowHeight"
:value="vModel"
:lines="rowHeightTruncateLines(rowHeight)"
:lines="rowHeightTruncateLines(localRowHeight)"
class="nc-text-area-clamped-text"
:style="{
'word-break': 'break-word',
'max-height': `${25 * rowHeightTruncateLines(rowHeight)}px`,
'my-auto': rowHeightTruncateLines(rowHeight) === 1,
'max-height': `${25 * rowHeightTruncateLines(localRowHeight)}px`,
'my-auto': rowHeightTruncateLines(localRowHeight) === 1,
}"
@click="onTextClick"
/>
@ -281,7 +290,7 @@ watch(inputWrapperRef, () => {
<NcTooltip
v-if="!isVisible && !isForm"
placement="bottom"
class="!absolute !hidden nc-text-area-expand-btn group-hover:block z-3"
class="nc-action-icon !absolute !hidden nc-text-area-expand-btn group-hover:block z-3"
:class="{
'right-1': isForm,
'right-0': !isForm,

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn, isValidTimeFormat } from 'nocodb-sdk'
import { isSystemColumn } from 'nocodb-sdk'
interface Props {
modelValue?: string | null | undefined
@ -140,11 +140,10 @@ watch(editable, (nextValue) => {
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isTimeInvalid.value) ||
(isGrid.value && !showNull.value && !isTimeInvalid.value && !isSystemColumn(column.value) && active.value)
(isGrid.value && !showNull.value && !isTimeInvalid.value && !isSystemColumn(column.value) && active.value) ||
isEditColumn.value
) {
return 'HH:mm'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
return parseProp(column.value.meta).is12hrFormat ? 'hh:mm AM' : 'HH:mm'
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isTimeInvalid.value) {
@ -212,6 +211,12 @@ const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
default:
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
const targetEl = e.target as HTMLInputElement
const value = targetEl.value
nextTick(() => {
targetEl.value = value
})
}
}
}
@ -256,12 +261,18 @@ const handleUpdateValue = (e: Event) => {
return
}
if (targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
targetValue = parseProp(column.value.meta).is12hrFormat
? targetValue
.trim()
.toUpperCase()
.replace(/(AM|PM)$/, ' $1')
.replace(/\s+/g, ' ')
: targetValue.trim()
if (isValidTimeFormat(targetValue, 'HH:mm')) {
tempDate.value = dayjs(`${dayjs().format('YYYY-MM-DD')} ${targetValue}`)
const parsedDate = dayjs(targetValue, parseProp(column.value.meta).is12hrFormat ? 'hh:mm A' : 'HH:mm')
if (parsedDate.isValid()) {
tempDate.value = dayjs(`${dayjs().format('YYYY-MM-DD')} ${parsedDate.format('HH:mm')}`)
}
}
@ -284,6 +295,8 @@ function handleSelectTime(value?: dayjs.Dayjs) {
open.value = false
}
const cellValue = computed(() => localState.value?.format(parseProp(column.value.meta).is12hrFormat ? 'hh:mm A' : 'HH:mm') ?? '')
</script>
<template>
@ -296,13 +309,14 @@ function handleSelectTime(value?: dayjs.Dayjs) {
:overlay-class-name="`${randomClass} nc-picker-time ${isOpen ? 'active' : ''} !min-w-[0]`"
>
<div
v-bind="$attrs"
:title="localState?.format('HH:mm')"
class="nc-time-picker h-full flex items-center justify-between ant-picker-input relative group"
class="nc-time-picker h-full flex items-center justify-between ant-picker-input relative"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format('HH:mm') ?? ''"
:value="cellValue"
:placeholder="placeholder"
class="nc-time-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@ -316,19 +330,20 @@ function handleSelectTime(value?: dayjs.Dayjs) {
/>
<GeneralIcon
v-if="localState"
v-if="localState && !readOnly"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-time-icon nc-action-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectTime()"
/>
</div>
<template #overlay>
<div class="w-[72px]">
<div class="min-w-[72px]">
<NcTimeSelector
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is12hr-format="!!parseProp(column.meta).is12hrFormat"
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
@ -337,3 +352,11 @@ function handleSelectTime(value?: dayjs.Dayjs) {
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>
<style scoped>
.nc-cell-field {
&:hover .nc-clear-time-icon {
@apply visible;
}
}
</style>

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

@ -85,7 +85,6 @@ watch(
v-if="!readOnly && editEnabled"
:ref="focus"
v-model="vModel"
:placeholder="isEditColumn ? $t('labels.enterDefaultUrlOptional') : ''"
class="nc-cell-field outline-none w-full py-1 bg-transparent h-full"
@blur="editEnabled = false"
@keydown.down.stop
@ -103,7 +102,7 @@ watch(
v-else-if="isValid && !cellUrlOptions?.overlay"
no-prefetch
no-rel
class="py-1 z-3 underline hover:opacity-75 nc-cell-field-link max-w-full"
class="py-1 z-3 underline nc-cell-field-link max-w-full"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
:tabindex="readOnly ? -1 : 0"
@ -115,7 +114,7 @@ watch(
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
no-prefetch
no-rel
class="py-1 z-3 w-full h-full text-center !no-underline hover:opacity-75 nc-cell-field-link max-w-full"
class="py-1 z-3 w-full h-full text-center !no-underline nc-cell-field-link max-w-full"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
:tabindex="readOnly ? -1 : 0"

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

@ -127,11 +127,10 @@ watch(editable, (nextValue) => {
const placeholder = computed(() => {
if (
((isForm.value || isExpandedForm.value) && !isYearInvalid.value) ||
(isGrid.value && !showNull.value && !isYearInvalid.value && !isSystemColumn(column.value) && active.value)
(isGrid.value && !showNull.value && !isYearInvalid.value && !isSystemColumn(column.value) && active.value) ||
isEditColumn.value
) {
return 'YYYY'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
} else if (isYearInvalid.value) {
@ -266,8 +265,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
:overlay-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
v-bind="$attrs"
:title="localState?.format('YYYY')"
class="nc-year-picker flex items-center justify-between ant-picker-input relative group"
class="nc-year-picker flex items-center justify-between ant-picker-input relative"
>
<input
ref="datePickerRef"
@ -285,9 +285,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
/>
<GeneralIcon
v-if="localState"
v-if="localState && !readOnly"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
class="nc-clear-year-icon nc-action-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
@ -309,7 +309,9 @@ function handleSelectDate(value?: dayjs.Dayjs) {
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
.nc-cell-field {
&:hover .nc-clear-year-icon {
@apply visible;
}
}
</style>

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

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

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

@ -103,7 +103,6 @@ const handleFileDelete = (i: number) => {
</div>
<div class="flex items-center gap-2">
<div v-if="readOnly" class="text-gray-400">[{{ $t('labels.readOnly') }}]</div>
{{ $t('labels.viewingAttachmentsOf') }}
<div class="font-semibold underline">{{ column?.title }}</div>
</div>

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

@ -157,7 +157,7 @@ const onExpand = () => {
const onImageClick = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) return
if (!isMobileMode.value && (isGallery.value || (isKanban.value && !isExpandedForm.value))) return
if (!isMobileMode.value && (isGallery.value || isKanban.value) && !isExpandedForm.value) return
selectedImage.value = item
}

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

@ -55,127 +55,139 @@ onMounted(() => {
<template>
<div class="flex w-full flex-col py-0.9 px-1 border-gray-200 gap-y-1">
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64">
<div
class="flex flex-row py-1 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-8"
data-testid="nc-sidebar-userinfo"
>
<GeneralUserIcon :email="user?.email" size="auto" :name="user?.display_name" />
<div class="flex truncate">
{{ name ? name : user?.email }}
</div>
<GeneralIcon icon="chevronDown" class="flex-none !min-w-5 transform rotate-180 !text-gray-500" />
</div>
<template #overlay>
<NcMenu data-testid="nc-sidebar-userinfo">
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout">
<div v-e="['c:user:logout']" class="flex gap-2 items-center">
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" />
<GeneralIcon v-else icon="signout" class="menu-icon" />
<span class="menu-btn"> {{ $t('general.logout') }}</span>
<div class="flex items-center pr-2 justify-between">
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64">
<div
class="flex flex-row py-1 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-8"
data-testid="nc-sidebar-userinfo"
>
<GeneralUserIcon :email="user?.email" size="auto" :name="user?.display_name" />
<NcTooltip>
<div class="flex max-w-32 truncate">
{{ name ? name : user?.email }}
</div>
</NcMenuItem>
<NcDivider />
<a
v-e="['c:nocodb:discord']"
href="https://discord.gg/5RgZmkW"
target="_blank"
class="!underline-transparent"
rel="noopener noreferrer"
>
<NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="ncDiscord" />
<span class="menu-btn"> {{ $t('labels.community.joinDiscord') }} </span>
</NcMenuItem>
</a>
<a
v-e="['c:nocodb:reddit']"
href="https://www.reddit.com/r/NocoDB"
target="_blank"
class="!underline-transparent"
rel="noopener noreferrer"
>
<NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="ncReddit" />
<span class="menu-btn"> {{ $t('labels.community.joinReddit') }} </span>
</NcMenuItem>
</a>
<a
v-e="['c:nocodb:twitter']"
href="https://twitter.com/nocodb"
target="_blank"
class="!underline-transparent"
rel="noopener noreferrer"
>
<NcMenuItem class="social-icon-wrapper group">
<GeneralIcon class="social-icon text-gray-500 group-hover:text-gray-800" icon="ncTwitter" />
<span class="menu-btn"> {{ $t('labels.twitter') }} </span>
</NcMenuItem>
</a>
<template v-if="!appInfo.ee">
<NcDivider />
<a-popover key="language" class="lang-menu !py-1.5" placement="rightBottom">
<NcMenuItem>
<div v-e="['c:translate:open']" class="flex gap-2 items-center">
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" />
{{ $t('labels.language') }}
<div class="flex items-center text-gray-400 text-xs">{{ $t('labels.community.communityTranslated') }}</div>
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</NcMenuItem>
<template #content>
<div class="bg-white max-h-50vh scrollbar-thin-dull min-w-64 !overflow-auto">
<LazyGeneralLanguageMenu />
</div>
</template>
</a-popover>
</template>
<template #title>
<span>
{{ name ? name : user?.email }}
</span>
</template>
</NcTooltip>
<template v-if="!isMobileMode">
<GeneralIcon icon="chevronDown" class="flex-none !min-w-5 transform rotate-180 !text-gray-500" />
</div>
<template #overlay>
<NcMenu data-testid="nc-sidebar-userinfo">
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout">
<div v-e="['c:user:logout']" class="flex gap-2 items-center">
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" />
<GeneralIcon v-else icon="signout" class="menu-icon" />
<span class="menu-btn"> {{ $t('general.logout') }}</span>
</div>
</NcMenuItem>
<NcDivider />
<a
v-e="['c:nocodb:forum-open']"
href="https://community.nocodb.com"
v-e="['c:nocodb:discord']"
href="https://discord.gg/5RgZmkW"
target="_blank"
class="!underline-transparent"
rel="noopener"
rel="noopener noreferrer"
>
<NcMenuItem>
<GeneralIcon icon="ncHelp" class="menu-icon mt-0.5" />
<span class="menu-btn"> {{ $t('title.forum') }} </span>
<NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="ncDiscord" />
<span class="menu-btn"> {{ $t('labels.community.joinDiscord') }} </span>
</NcMenuItem>
</a>
<a
v-e="['c:nocodb:docs-open']"
href="https://docs.nocodb.com"
v-e="['c:nocodb:reddit']"
href="https://www.reddit.com/r/NocoDB"
target="_blank"
class="!underline-transparent"
rel="noopener"
rel="noopener noreferrer"
>
<NcMenuItem>
<GeneralIcon icon="file" class="menu-icon mt-0.5" />
<span class="menu-btn"> {{ $t('title.docs') }} </span>
<NcMenuItem class="social-icon-wrapper">
<GeneralIcon class="social-icon" icon="ncReddit" />
<span class="menu-btn"> {{ $t('labels.community.joinReddit') }} </span>
</NcMenuItem>
</a>
<NcDivider />
<DashboardSidebarEEMenuOption v-if="isEeUI" />
<nuxt-link v-e="['c:user:settings']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="ncSettings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem>
</nuxt-link>
</template>
</NcMenu>
</template>
</NcDropdown>
<a
v-e="['c:nocodb:twitter']"
href="https://twitter.com/nocodb"
target="_blank"
class="!underline-transparent"
rel="noopener noreferrer"
>
<NcMenuItem class="social-icon-wrapper group">
<GeneralIcon class="social-icon text-gray-500 group-hover:text-gray-800" icon="ncTwitter" />
<span class="menu-btn"> {{ $t('labels.twitter') }} </span>
</NcMenuItem>
</a>
<template v-if="!appInfo.ee">
<NcDivider />
<a-popover key="language" class="lang-menu !py-1.5" placement="rightBottom">
<NcMenuItem>
<div v-e="['c:translate:open']" class="flex gap-2 items-center">
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" />
{{ $t('labels.language') }}
<div class="flex items-center text-gray-400 text-xs">{{ $t('labels.community.communityTranslated') }}</div>
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</NcMenuItem>
<template #content>
<div class="bg-white max-h-50vh scrollbar-thin-dull min-w-64 !overflow-auto">
<LazyGeneralLanguageMenu />
</div>
</template>
</a-popover>
</template>
<template v-if="!isMobileMode">
<NcDivider />
<a
v-e="['c:nocodb:forum-open']"
href="https://community.nocodb.com"
target="_blank"
class="!underline-transparent"
rel="noopener"
>
<NcMenuItem>
<GeneralIcon icon="ncHelp" class="menu-icon mt-0.5" />
<span class="menu-btn"> {{ $t('title.forum') }} </span>
</NcMenuItem>
</a>
<a
v-e="['c:nocodb:docs-open']"
href="https://docs.nocodb.com"
target="_blank"
class="!underline-transparent"
rel="noopener"
>
<NcMenuItem>
<GeneralIcon icon="file" class="menu-icon mt-0.5" />
<span class="menu-btn"> {{ $t('title.docs') }} </span>
</NcMenuItem>
</a>
<NcDivider />
<DashboardSidebarEEMenuOption v-if="isEeUI" />
<nuxt-link v-e="['c:user:settings']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="ncSettings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem>
</nuxt-link>
</template>
</NcMenu>
</template>
</NcDropdown>
<LazyNotificationMenu />
</div>
<template v-if="isMobileMode || appInfo.ee"></template>
<div v-else class="flex flex-row w-full justify-between pt-0.5 truncate">

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

@ -206,6 +206,13 @@ const deleteTable = () => {
isOptionsOpen.value = false
isTableDeleteDialogVisible.value = true
}
// TODO: Should find a way to render the components without using the `nextTick` function
const refreshViews = async () => {
isExpanded.value = false
await nextTick()
isExpanded.value = true
}
</script>
<template>
@ -397,7 +404,7 @@ const deleteTable = () => {
:base-id="base.id"
/>
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" />
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" @deleted="refreshViews" />
</div>
</template>

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

@ -137,6 +137,16 @@ function onResize(widthPercent: any) {
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
// If the viewport width is less than 1560px, the max sidebar width should be 20rem
if (viewportWidth.value <= 1560) {
if (width > remToPx(20)) {
sideBarSize.value.old = ((20 * fontSize) / viewportWidth.value) * 100
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
}
}
const widthRem = width / fontSize
if (widthRem < 16) {

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

@ -48,8 +48,6 @@ const { $e } = useNuxtApp()
const { t } = useI18n()
const { isDataSourceLimitReached } = storeToRefs(useBases())
const dataSourcesReload = ref(false)
const tabsInfo: TabGroup = {

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

@ -54,7 +54,7 @@ const syncSource = ref({
syncLookup: true,
syncFormula: false,
syncAttachment: true,
syncUsers: true,
syncUsers: false,
},
},
})
@ -210,7 +210,7 @@ async function loadSyncSrc() {
syncLookup: true,
syncFormula: false,
syncAttachment: true,
syncUsers: true,
syncUsers: false,
},
},
}
@ -403,22 +403,30 @@ function downloadLogs(filename: string) {
</a-checkbox>
</div>
<!-- Import Users Columns -->
<!-- Import Formula Columns -->
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncUsers">
{{ $t('labels.importUsers') }}
</a-checkbox>
<a-tooltip placement="top">
<template #title>
<span>{{ $t('title.comingSoon') }}</span>
</template>
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>
{{ $t('labels.importFormulaColumns') }}
</a-checkbox>
</a-tooltip>
</div>
<!-- Import Formula Columns -->
<a-tooltip placement="top">
<template #title>
<span>{{ $t('title.comingSoon') }}</span>
</template>
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>
{{ $t('labels.importFormulaColumns') }}
</a-checkbox>
</a-tooltip>
<!-- Invite Users
<div class="my-2">
<a-tooltip placement="top">
<template #title>
<span>{{ $t('title.comingSoon') }}</span>
</template>
<a-checkbox v-model:checked="syncSource.details.options.syncUsers" disabled>
{{ $t('labels.importUsers') }}
</a-checkbox>
</a-tooltip>
</div>
-->
</a-form>
<a-divider />

46
packages/nc-gui/components/dlg/ColumnUpdateConfirm.vue

@ -0,0 +1,46 @@
<script setup lang="ts">
const props = defineProps<{
visible?: boolean
saving?: boolean
}>()
const emit = defineEmits(['submit', 'cancel', 'update:visible'])
const visible = useVModel(props, 'visible', emit)
</script>
<template>
<GeneralModal v-model:visible="visible" size="small">
<div class="flex flex-col p-6" @click.stop>
<div class="flex flex-row pb-2 mb-4 font-medium text-lg border-b-1 border-gray-50 text-gray-800">Field Type Change</div>
<div class="mb-3 text-gray-800">
<div class="flex item-center gap-2 mb-4">
<component :is="iconMap.warning" id="nc-selected-item-icon" class="text-yellow-500 w-10 h-10" />
This action cannot be undone. Converting data types may result in data loss. Proceed with caution!
</div>
</div>
<slot name="entity-preview"></slot>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton type="secondary" @click="visible = false">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
key="submit"
autofocus
type="primary"
html-type="submit"
:loading="saving"
data-testid="nc-delete-modal-delete-btn"
@click="emit('submit')"
>
Update
<template #loading> Saving... </template>
</NcButton>
</div>
</div>
</GeneralModal>
</template>

6
packages/nc-gui/components/dlg/InviteDlg.vue

@ -12,8 +12,6 @@ const props = defineProps<{
}>()
const emit = defineEmits(['update:modelValue'])
const { baseRoles, workspaceRoles } = useRoles()
const basesStore = useBases()
const workspaceStore = useWorkspace()
@ -28,10 +26,6 @@ const orderedRoles = computed(() => {
return props.type === 'base' ? ProjectRoles : WorkspaceUserRoles
})
const userRoles = computed(() => {
return props.type === 'base' ? baseRoles.value : workspaceRoles.value
})
const inviteData = reactive({
email: '',
roles: orderedRoles.value.NO_ACCESS,

197
packages/nc-gui/components/general/AdvanceColorPicker.vue

@ -0,0 +1,197 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import windiColors from 'windicss/colors'
import { themeV3Colors } from '../../utils/colorsUtils'
interface Props {
modelValue?: string | any
isOpen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isOpen: false,
})
const emit = defineEmits(['input', 'closeModal'])
const { isOpen } = toRefs(props)
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
emit('input', val || null)
},
})
const showActiveColorTab = ref<boolean>(false)
const picked = ref<string>(props.modelValue || enumColor.light[0])
const defaultColors = computed<string[][]>(() => {
const colors = [
'gray',
'red',
'green',
'yellow',
'orange',
'pink',
'maroon',
'purple',
'blue',
] as (keyof typeof themeV3Colors)[] & (keyof typeof windiColors)[]
const allColors = []
for (const color of colors) {
if (themeV3Colors[color]) {
allColors.push(color === 'gray' ? Object.values(themeV3Colors[color]).slice(1) : Object.values(themeV3Colors[color]))
} else if (windiColors[color]) {
allColors.push(Object.values(windiColors[color]))
}
}
return allColors
})
const localIsDefaultColorTab = ref<'true' | 'false'>('true')
const isDefaultColorTab = computed({
get: () => {
if (showActiveColorTab.value && vModel.value) {
for (const colorGrp of defaultColors.value) {
if (colorGrp.includes(vModel.value)) {
return 'true'
}
}
return 'false'
}
return localIsDefaultColorTab.value
},
set: (val: 'true' | 'false') => {
localIsDefaultColorTab.value = val
if (showActiveColorTab.value) {
showActiveColorTab.value = false
}
},
})
const selectColor = (color: string, closeModal = false) => {
picked.value = color
if (closeModal) {
emit('closeModal')
}
}
const compare = (colorA: string, colorB: string) => {
if (!colorA || !colorB) return false
return colorA.toLowerCase() === colorB.toLowerCase() || colorA.toLowerCase() === tinycolor(colorB).toHex8String().toLowerCase()
}
watch(picked, (n, _o) => {
vModel.value = n
})
watch(
isOpen,
(newValue) => {
if (newValue) {
showActiveColorTab.value = true
}
},
{
immediate: true,
},
)
</script>
<template>
<div class="nc-advance-color-picker w-[336px] pt-2" click.stop>
<NcTabs v-model:activeKey="isDefaultColorTab" class="nc-advance-color-picker-tab w-full">
<a-tab-pane key="true">
<template #tab>
<div class="tab" data-testid="nc-default-colors-tab">Default colors</div>
</template>
<div class="h-full p-2">
<div class="flex flex-col gap-1">
<div v-for="(colorGroup, i) of defaultColors" :key="i" class="flex">
<div v-for="(color, j) of colorGroup" :key="`color-${i}-${j}`" class="p-1 rounded-md flex h-8 hover:bg-gray-200">
<button
class="color-selector"
:class="{ selected: compare(picked, color) }"
:style="{
backgroundColor: `${color}`,
}"
@click="selectColor(color, true)"
></button>
</div>
</div>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="false">
<template #tab>
<div class="tab" data-testid="nc-custom-colors-tab">
<div>Custom colours</div>
</div>
</template>
<div class="h-full p-2">
<LazyGeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div>
</a-tab-pane>
</NcTabs>
</div>
</template>
<style lang="scss" scoped>
.color-picker {
@apply flex flex-col items-center justify-center bg-white p-2.5;
}
.color-picker-row {
@apply flex flex-row space-x-1;
}
.color-selector {
@apply h-6 w-6 rounded;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector:focus,
.color-selector.selected,
.nc-more-colors-trigger:focus {
outline: none;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
:deep(.vc-chrome-toggle-icon) {
@apply !ml-3;
}
:deep(.ant-tabs) {
@apply !overflow-visible;
.ant-tabs-nav {
@apply px-1;
.ant-tabs-nav-list {
@apply w-[99%] mx-auto gap-6;
.ant-tabs-tab {
@apply flex-1 flex items-center justify-center pt-2 pb-2 text-xs font-semibold;
& + .ant-tabs-tab {
@apply !ml-0;
}
}
}
}
.ant-tabs-content-holder {
.ant-tabs-content {
@apply h-full;
}
}
}
</style>

5
packages/nc-gui/components/general/EmojiPicker.vue

@ -6,7 +6,7 @@ import { EmojiIndex, Picker } from 'emoji-mart-vue-fast/src'
const props = defineProps<{
emoji?: string | undefined
size?: 'small' | 'medium' | 'large' | 'xlarge'
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'
readonly?: boolean
disableClearing?: boolean
}>()
@ -80,10 +80,11 @@ const showClearButton = computed(() => {
<template>
<a-dropdown v-model:visible="isOpen" :trigger="['click']" :disabled="readonly">
<div
class="flex flex-row justify-center items-center select-none rounded-md nc-emoji"
class="flex-none flex flex-row justify-center items-center select-none rounded-md nc-emoji"
:class="{
'hover:bg-gray-500 hover:bg-opacity-15 cursor-pointer': !readonly,
'bg-gray-500 bg-opacity-15': isOpen,
'h-4 w-4 text-[16px] leading-4': size === 'xsmall',
'h-6 w-6 text-lg': size === 'small',
'h-8 w-8 text-xl': size === 'medium',
'h-10 w-10 text-2xl': size === 'large',

8
packages/nc-gui/components/general/Loader.vue

@ -2,7 +2,7 @@
import { LoadingOutlined } from '@ant-design/icons-vue'
const props = defineProps<{
size?: 'small' | 'medium' | 'large' | 'xlarge'
size?: 'small' | 'medium' | 'large' | 'xlarge' | 'regular'
loaderClass?: string
}>()
@ -18,17 +18,19 @@ function getFontSize() {
return 'text-xl'
case 'xlarge':
return 'text-3xl'
case 'regular':
return 'text-[16px] leading-4'
}
}
const indicator = h(LoadingOutlined, {
class: `!${getFontSize()} flex flex-row items-center !bg-inherit !hover:bg-inherit !text-inherit ${props.loaderClass}}`,
class: `!${getFontSize()} flex flex-row items-center !bg-inherit !hover:bg-inherit !text-inherit ${props.loaderClass || ''}}`,
spin: true,
})
</script>
<template>
<a-spin class="nc-loader flex flex-row items-center" :indicator="indicator" />
<a-spin class="nc-loader !flex flex-row items-center" :indicator="indicator" />
</template>
<style lang="scss" scoped>

20
packages/nc-gui/components/general/TruncateText.vue

@ -1,18 +1,8 @@
<script lang="ts" setup>
import type { TooltipPlacement } from 'ant-design-vue/es/tooltip'
interface Props {
placement?:
| 'top'
| 'left'
| 'right'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom'
placement?: TooltipPlacement
length?: number
}
@ -30,12 +20,12 @@ const shortName = computed(() =>
</script>
<template>
<a-tooltip v-if="enableTooltip" :placement="placement">
<NcTooltip v-if="enableTooltip" :placement="placement">
<template #title>
<slot />
</template>
<div class="w-full">{{ shortName }}</div>
</a-tooltip>
</NcTooltip>
<div v-else class="w-full" data-testid="truncate-label">
<slot />
</div>

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

@ -1,5 +1,4 @@
<script lang="ts" setup>
import { Icon as IcIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk'
const props = defineProps<{
@ -11,11 +10,12 @@ const viewMeta = toRef(props, 'meta')
</script>
<template>
<IcIcon
<LazyGeneralEmojiPicker
v-if="viewMeta?.meta?.icon"
:data-testid="`nc-icon-${viewMeta?.meta?.icon}`"
class="text-[16px]"
:icon="viewMeta?.meta?.icon"
:data-testid="`nc-emoji-${viewMeta.meta?.icon}`"
size="xsmall"
:emoji="viewMeta.meta?.icon"
readonly
/>
<component
:is="viewIcons[viewMeta.type]?.icon"

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

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { ButtonType } from 'ant-design-vue/lib/button'
import { useSlots } from 'vue'
import type { NcButtonSize } from '~/lib/types'
/**
* @description
@ -76,11 +75,12 @@ useEventListener(NcButton, 'mousedown', () => {
<a-button
ref="NcButton"
:class="{
small: size === 'small',
medium: size === 'medium',
xsmall: size === 'xsmall',
xxsmall: size === 'xxsmall',
focused: isFocused,
'small': size === 'small',
'medium': size === 'medium',
'xsmall': size === 'xsmall',
'xxsmall': size === 'xxsmall',
'size-xs': size === 'xs',
'focused': isFocused,
}"
:disabled="props.disabled"
:loading="loading"
@ -168,6 +168,13 @@ useEventListener(NcButton, 'mousedown', () => {
@apply py-2 px-4 h-10 min-w-10 xs:(h-10.5 max-h-10.5 min-w-10.5 !px-3);
}
.nc-button.ant-btn.size-xs {
@apply px-2 py-0 h-7 min-w-7 rounded-lg text-small leading-[18px];
& > div {
@apply gap-x-2;
}
}
.nc-button.ant-btn.xsmall {
@apply p-0.25 h-6.25 min-w-6.25 rounded-md;
}
@ -179,7 +186,7 @@ useEventListener(NcButton, 'mousedown', () => {
.nc-button.ant-btn[disabled],
.ant-btn-text.nc-button.ant-btn[disabled] {
box-shadow: none !important;
@apply bg-gray-50 border-0 text-gray-300 cursor-not-allowed md:(hover:bg-gray-50);
@apply bg-gray-50 border-0 text-gray-300 !cursor-not-allowed md:(hover:bg-gray-50);
}
.nc-button.ant-btn-text.ant-btn[disabled] {

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

@ -191,7 +191,7 @@ const paginate = (action: 'next' | 'prev') => {
<div v-if="!hideCalendar" class="max-w-[320px] rounded-y-xl">
<div class="py-1 px-2.5 h-10">
<div
class="flex gap-1"
class="flex justify-between gap-1"
:class="{
'border-b-1 border-gray-200 ': isCellInputField,
}"

18
packages/nc-gui/components/nc/MenuItem.vue

@ -1,9 +1,17 @@
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<a-menu-item class="nc-menu-item">
<div class="nc-menu-item-inner">
<slot />
</div>
</a-menu-item>
<div class="w-full">
<a-menu-item v-bind="$attrs" class="nc-menu-item">
<div class="nc-menu-item-inner">
<slot />
</div>
</a-menu-item>
</div>
</template>
<style lang="scss">

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

@ -93,10 +93,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template>
<div class="flex flex-col">
<div
class="flex border-b-1 justify-between items-center"
class="flex border-b-1 nc-month-picker-pagination justify-between items-center"
:class="{
'px-2 py-1 h-10': isCellInputField,
'px-3 py-0.5': !isCellInputField,
'px-2 py-0.5': !isCellInputField,
}"
>
<div class="flex">
@ -137,10 +137,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</div>
<div
v-if="!hideCalendar"
class="rounded-y-xl py-1 max-w-[350px]"
class="rounded-y-xl max-w-[350px]"
:class="{
'px-2': isCellInputField,
'px-2.5': !isCellInputField,
'px-2 py-1': isCellInputField,
'px-2.5 py-2': !isCellInputField,
}"
>
<div class="grid grid-cols-4 gap-2">
@ -153,10 +153,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
'bg-gray-300 !font-weight-600 ': isMonthSelected(month) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !isMonthSelected(month),
'!text-brand-500': dayjs().isSame(month, 'month'),
'font-weight-400 rounded': isCellInputField,
'font-medium rounded-lg': !isCellInputField,
'font-weight-400': isCellInputField,
'font-medium': !isCellInputField,
}"
class="nc-month-item h-8 flex items-center transition-all justify-center text-gray-700 cursor-pointer"
class="nc-month-item h-8 flex items-center rounded transition-all justify-center text-gray-700 cursor-pointer"
:title="isCellInputField ? month.format('YYYY-MM') : undefined"
@click="selectedDate = month"
>
@ -168,14 +168,14 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years"
:key="id"
:class="{
'bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate) && !isCellInputField,
'bg-gray-300 !font-weight-600 ': compareYear(year, selectedDate) && isCellInputField,
'bg-gray-200 !font-bold ': compareYear(year, selectedDate) && !isCellInputField,
'bg-gray-300 !text-brand-500 !font-weight-600 ': compareYear(year, selectedDate) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'),
'font-weight-400 text-gray-700 rounded': isCellInputField,
'font-medium text-gray-900 rounded-lg': !isCellInputField,
'font-weight-400 text-gray-700': isCellInputField,
'font-medium text-gray-900': !isCellInputField,
}"
class="nc-year-item h-8 flex items-center transition-all justify-center cursor-pointer"
class="nc-year-item h-8 flex items-center rounded transition-all justify-center cursor-pointer"
:title="isCellInputField ? year.format('YYYY') : undefined"
@click="selectedDate = year"
>

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

@ -1,20 +1,50 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{ checked: boolean; disabled?: boolean; size?: 'default' | 'small' | 'xsmall' }>(), {
size: 'small',
})
const props = withDefaults(
defineProps<{
checked: boolean
disabled?: boolean
size?: 'default' | 'small' | 'xsmall'
placement?: 'left' | 'right'
loading?: boolean
}>(),
{
size: 'small',
placement: 'left',
loading: false,
},
)
const emit = defineEmits(['change', 'update:checked'])
const checked = useVModel(props, 'checked', emit)
const { loading } = toRefs(props)
const switchSize = computed(() => (['default', 'small'].includes(props.size) ? props.size : undefined))
const onChange = (e: boolean) => {
const onChange = (e: boolean, updateValue = false) => {
if (loading.value) return
if (updateValue) {
checked.value = e
}
emit('change', e)
}
</script>
<template>
<span
v-if="placement === 'right' && $slots.default"
class="pr-2"
:class="{
'cursor-not-allowed': disabled,
'cursor-pointer': !disabled,
}"
@click="onChange(!checked, true)"
>
<slot />
</span>
<a-switch
v-model:checked="checked"
:disabled="disabled"
@ -22,12 +52,21 @@ const onChange = (e: boolean) => {
:class="{
'size-xsmall': size === 'xsmall',
}"
:loading="loading"
v-bind="$attrs"
:size="switchSize"
@change="onChange"
>
</a-switch>
<span v-if="$slots.default" class="cursor-pointer pl-2" @click="checked = !checked">
<span
v-if="placement === 'left' && $slots.default"
class="pl-2"
:class="{
'cursor-not-allowed': disabled,
'cursor-pointer': !disabled,
}"
@click="onChange(!checked, true)"
>
<slot />
</span>
</template>

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

@ -42,7 +42,7 @@ const handleSelectTime = (time: dayjs.Dayjs) => {
// TODO: 12hr time format & regular time picker
const timeOptions = computed(() => {
return Array.from({ length: is12hrFormat.value ? 12 : 24 }).flatMap((_, h) => {
return Array.from({ length: 24 }).flatMap((_, h) => {
return (isMinGranularityPicker.value ? [0, minGranularity.value] : Array.from({ length: 60 })).map((_m, m) => {
const time = dayjs()
.set('hour', h)
@ -89,7 +89,7 @@ onMounted(() => {
:data-testid="`time-option-${time.format('HH:mm')}`"
@click="handleSelectTime(time)"
>
{{ time.format('HH:mm') }}
{{ time.format(is12hrFormat ? 'hh:mm A' : 'HH:mm') }}
</div>
</div>
<div v-else></div>

169
packages/nc-gui/components/notification/Card.vue

@ -4,83 +4,126 @@ import InfiniteLoading from 'v3-infinite-loading'
const notificationStore = useNotification()
const { notifications, isRead, pageInfo } = storeToRefs(notificationStore)
const { isMobileMode } = useGlobal()
/*
const groupType = computed({
get() {
return isRead.value ? 'read' : 'unread'
},
set(value) {
isRead.value = value === 'read'
notificationStore.loadNotifications()
},
})
*/
const container = ref()
const { height } = useElementSize(container)
const { loadUnReadNotifications, loadReadNotifications, markAllAsRead } = notificationStore
const { unreadNotifications, readNotifications, readPageInfo, unreadPageInfo, notificationTab } = storeToRefs(notificationStore)
</script>
<template>
<div class="min-w-[350px] max-w-[350px] min-h-[400px] !rounded-2xl bg-white rounded-xl nc-card">
<div class="p-3" @click.stop>
<div class="flex items-center">
<span class="text-md font-medium text-[#212121]">
{{ $t('general.notification') }}
</span>
<div class="flex-grow"></div>
<div
v-if="!isRead && notifications?.length"
class="cursor-pointer text-xs text-gray-500 hover:text-primary"
@click.stop="notificationStore.markAllAsRead"
>
{{ $t('activity.markAllAsRead') }}
</div>
</div>
</div>
<a-divider class="!my-0" />
<div
class="overflow-y-auto max-h-[max(60vh,500px)] min-h-100"
:class="{
'flex items-center justify-center': !notifications?.length,
}"
>
<template v-if="!notifications?.length">
<div class="flex flex-col gap-2 items-center justify-center">
<div class="text-sm text-gray-400">{{ $t('msg.noNewNotifications') }}</div>
<GeneralIcon icon="inbox" class="!text-40px text-gray-400" />
</div>
</template>
<template v-else>
<template v-for="item in notifications" :key="item.id">
<NotificationItem class="" :item="item" />
<a-divider class="!my-0" />
</template>
<div
ref="container"
style="box-shadow: 0px -12px 16px -4px rgba(0, 0, 0, 0.1), 0px -4px 6px -2px rgba(0, 0, 0, 0.06)"
:style="!isMobileMode ? 'width: min(80svw, 520px);' : ''"
:class="{
'max-h-[70vh] h-[620px]': !isMobileMode,
'h-[100svh] w-[100svw]': isMobileMode,
}"
class="!rounded-lg pt-4"
>
<div class="space-y-3">
<div class="flex px-6 justify-between items-center">
<span class="text-md font-bold text-gray-800" @click.stop> {{ $t('general.notification') }}s </span>
<InfiniteLoading
v-if="notifications && pageInfo && pageInfo.totalRows > notifications.length"
@infinite="notificationStore.loadNotifications(true)"
>
<template #spinner>
<div class="flex flex-row w-full justify-center mt-2">
<a-spin />
</div>
<NcButton v-if="isMobileMode" size="small" type="secondary">
<GeneralIcon icon="close" class="text-gray-700" />
</NcButton>
</div>
<div
v-if="notificationTab !== 'read'"
:class="{
'text-gray-400': !unreadNotifications?.length,
}"
class="cursor-pointer right-5 pointer-events-auto top-12.5 z-2 absolute text-[13px] text-gray-600 font-weight-semibold"
@click.stop="markAllAsRead"
>
{{ $t('activity.markAllAsRead') }}
</div>
<NcTabs v-model:activeKey="notificationTab">
<a-tab-pane key="unread">
<template #tab>
<span
:class="{
'font-semibold': notificationTab === 'unread',
}"
class="text-xs"
>
Unread
</span>
</template>
<template #complete>
<span></span>
<div
class="overflow-y-auto"
:style="`height: ${height - 72}px`"
:class="{
'flex flex-col items-center min-h-[48svh] justify-center': !unreadNotifications?.length,
}"
>
<template v-if="!unreadNotifications?.length">
<div class="text-sm !text-gray-500">{{ $t('msg.noNewNotifications') }}</div>
<GeneralIcon icon="inbox" class="!text-40px !text-gray-500" />
</template>
<template v-else>
<NotificationItem v-for="item in unreadNotifications" :key="item.id" :item="item" />
<InfiniteLoading
v-if="unreadNotifications && unreadPageInfo && unreadPageInfo.totalRows > unreadNotifications.length"
@infinite="loadUnReadNotifications(true)"
>
</InfiniteLoading>
</template>
</div>
</a-tab-pane>
<a-tab-pane key="read">
<template #tab>
<span
:class="{
'font-semibold': notificationTab === 'read',
}"
class="text-xs"
>
Read
</span>
</template>
</InfiniteLoading>
</template>
<div
class="overflow-y-auto"
:style="!isMobileMode ? `height: ${height - 72}px` : ''"
:class="{
'flex flex-col items-center min-h-[48svh] justify-center': !readNotifications?.length,
}"
>
<template v-if="!readNotifications?.length">
<div class="text-sm text-gray-500">{{ $t('msg.noNewNotifications') }}</div>
<GeneralIcon icon="inbox" class="!text-40px text-gray-500" />
</template>
<template v-else>
<NotificationItem v-for="item in readNotifications" :key="item.id" :item="item" />
<InfiniteLoading
v-if="readNotifications && readPageInfo && readPageInfo.totalRows > readNotifications.length"
@infinite="loadReadNotifications(true)"
>
</InfiniteLoading>
</template>
</div>
</a-tab-pane>
</NcTabs>
</div>
</div>
</template>
<style scoped>
.nc-card {
border: solid 1px #e1e3e6;
:deep(.ant-tabs-nav-wrap) {
@apply px-3;
}
:deep(.ant-tabs-nav-wrap) {
@apply px-6;
:deep(.ant-tabs-tab) {
@apply pb-1.5 pt-1;
}
:deep(.ant-tabs-nav) {

28
packages/nc-gui/components/notification/Item.vue

@ -1,42 +1,24 @@
<script setup lang="ts">
import { AppEvents } from 'nocodb-sdk'
import type { NotificationType } from 'nocodb-sdk'
const props = defineProps<{
item: any
item: NotificationType
}>()
const item = toRef(props, 'item')
const notificationStore = useNotification()
const { markAsRead } = notificationStore
const { toggleRead } = notificationStore
</script>
<template>
<div class="select-none" @click="markAsRead(item)">
<div class="select-none" @click="toggleRead(item, item.is_read)">
<NotificationItemWelcome v-if="item.type === AppEvents.WELCOME" :item="item" />
<NotificationItemProjectInvite v-else-if="item.type === AppEvents.PROJECT_INVITE" :item="item" />
<NotificationItemWorkspaceInvite v-else-if="item.type === AppEvents.WORKSPACE_INVITE" :item="item" />
<NotificationItemProjectEvent
v-else-if="[AppEvents.PROJECT_CREATE, AppEvents.PROJECT_DELETE, AppEvents.PROJECT_UPDATE].includes(item.type)"
:item="item"
/>
<NotificationItemTableEvent
v-else-if="[AppEvents.TABLE_CREATE, AppEvents.TABLE_DELETE, AppEvents.TABLE_UPDATE].includes(item.type)"
:item="item"
/>
<NotificationItemViewEvent
v-else-if="[AppEvents.VIEW_CREATE, AppEvents.VIEW_DELETE, AppEvents.VIEW_UPDATE].includes(item.type)"
:item="item"
/>
<NotificationItemSharedViewEvent
v-else-if="[AppEvents.SHARED_VIEW_CREATE, AppEvents.SHARED_VIEW_DELETE, AppEvents.SHARED_VIEW_UPDATE].includes(item.type)"
:item="item"
/>
<NotificationItemWorkspaceEvent
v-else-if="[AppEvents.WORKSPACE_CREATE, AppEvents.WORKSPACE_DELETE, AppEvents.WORKSPACE_UPDATE].includes(item.type)"
:item="item"
/>
<NotificationItemMentionEvent v-else-if="['mention'].includes(item.type)" :item="item" />
<span v-else />
</div>
</template>

17
packages/nc-gui/components/notification/Item/ColumnEvent.vue

@ -1,17 +0,0 @@
<script setup lang="ts">
import type { ProjectEventType } from 'nocodb-sdk'
const props = defineProps<{
item: ProjectEventType
}>()
const item = toRef(props, 'item')
</script>
<template>
<NotificationItemWrapper :item="item">
<div class="text-xs">
<strong>{{ item.data?.user?.name }}</strong> {{ item.data?.action }} <strong>{{ item.data?.table?.name }}</strong>
</div>
</NotificationItemWrapper>
</template>

17
packages/nc-gui/components/notification/Item/FilterViewEvent.vue

@ -1,17 +0,0 @@
<script setup lang="ts">
import type { ProjectEventType } from 'nocodb-sdk'
const props = defineProps<{
item: ProjectEventType
}>()
const item = toRef(props, 'item')
</script>
<template>
<NotificationItemWrapper :item="item">
<div class="text-xs">
<strong>{{ item.data?.user?.name }}</strong> {{ item.data?.action }} <strong>{{ item.data?.table?.name }}</strong>
</div>
</NotificationItemWrapper>
</template>

38
packages/nc-gui/components/notification/Item/ProjectEvent.vue

@ -1,38 +0,0 @@
<script setup lang="ts">
import type { ProjectEventType } from 'nocodb-sdk'
import { AppEvents } from 'nocodb-sdk'
const props = defineProps<{
item: ProjectEventType
}>()
const item = toRef(props, 'item')
const { navigateToProject } = useGlobal()
const action = computed(() => {
switch (item.value.type) {
case AppEvents.PROJECT_CREATE:
return 'created'
case AppEvents.PROJECT_UPDATE:
return 'updated'
case AppEvents.PROJECT_DELETE:
return 'deleted'
}
})
const onClick = () => {
if (item.value.type === AppEvents.PROJECT_DELETE) return
navigateToProject({ baseId: item.value.body.id })
}
</script>
<template>
<NotificationItemWrapper :item="item" @click="onClick">
<div class="text-xs gap-2">
Base
<GeneralProjectIcon style="vertical-align: middle" :type="item.body.type" /> <strong>{{ item.body.title }}</strong>
{{ action }} successfully
</div>
</NotificationItemWrapper>
</template>

12
packages/nc-gui/components/notification/Item/ProjectInvite.vue

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { ProjectInviteEventType } from 'nocodb-sdk'
const props = defineProps<{
item: any
item: ProjectInviteEventType
}>()
const { navigateToProject } = useGlobal()
@ -9,10 +11,10 @@ const item = toRef(props, 'item')
</script>
<template>
<NotificationItemWrapper :item="item" @click="navigateToProject({ baseId: item.body.id })">
<div class="text-xs">
<strong>{{ item.body.invited_by }}</strong> has invited you to collaborate on
<!-- <GeneralProjectIcon style="vertical-align: middle" :type="item.body.type" /> <strong>{{ item.body.title }}</strong> base. -->
<NotificationItemWrapper :item="item" @click="navigateToProject({ baseId: item.body.base.id })">
<div>
<span class="font-semibold">{{ item.body.user.display_name ?? item.body.user.email }}</span> has invited you to collaborate
on <span class="font-semibold">{{ item.body.base.title }}</span> base.
</div>
</NotificationItemWrapper>
</template>

38
packages/nc-gui/components/notification/Item/SharedViewEvent.vue

@ -1,38 +0,0 @@
<script setup lang="ts">
import type { ProjectEventType } from 'nocodb-sdk'
import { AppEvents } from 'nocodb-sdk'
const props = defineProps<{
item: ProjectEventType
}>()
const item = toRef(props, 'item')
const { navigateToProject } = useGlobal()
const action = computed(() => {
switch (item.value.type) {
case AppEvents.VIEW_CREATE:
return 'created'
case AppEvents.VIEW_UPDATE:
return 'updated'
case AppEvents.VIEW_DELETE:
return 'deleted'
}
})
const onClick = () => {
if (item.value.type === AppEvents.VIEW_DELETE) return
navigateToProject({ baseId: item.value.body.id })
}
</script>
<template>
<NotificationItemWrapper :item="item" @click="onClick">
<div class="text-xs gap-2">
Shared view
<strong>{{ item.body.title }}</strong>
{{ action }} successfully
</div>
</NotificationItemWrapper>
</template>

17
packages/nc-gui/components/notification/Item/SortViewEvent.vue

@ -1,17 +0,0 @@
<script setup lang="ts">
import type { ProjectEventType } from 'nocodb-sdk'
const props = defineProps<{
item: ProjectEventType
}>()
const item = toRef(props, 'item')
</script>
<template>
<NotificationItemWrapper :item="item">
<div class="text-xs">
<strong>{{ item.data?.user?.name }}</strong> {{ item.data?.action }} <strong>{{ item.data?.table?.name }}</strong>
</div>
</NotificationItemWrapper>
</template>

38
packages/nc-gui/components/notification/Item/TableEvent.vue

@ -1,38 +0,0 @@
<script setup lang="ts">
import type { TableEventType } from 'nocodb-sdk'
import { AppEvents } from 'nocodb-sdk'
const props = defineProps<{
item: TableEventType
}>()
const item = toRef(props, 'item')
const { navigateToProject } = useGlobal()
const action = computed(() => {
switch (item.value.type) {
case AppEvents.TABLE_CREATE:
return 'created'
case AppEvents.TABLE_UPDATE:
return 'updated'
case AppEvents.TABLE_DELETE:
return 'deleted'
}
})
const onClick = () => {
if (item.value.type === AppEvents.TABLE_DELETE) return
navigateToProject({ baseId: item.value.body.id })
}
</script>
<template>
<NotificationItemWrapper :item="item" @click="onClick">
<div class="text-xs gap-2">
Table
<strong>{{ item.body.title }}</strong>
{{ action }} successfully
</div>
</NotificationItemWrapper>
</template>

38
packages/nc-gui/components/notification/Item/ViewEvent.vue

@ -1,38 +0,0 @@
<script setup lang="ts">
import type { ProjectEventType } from 'nocodb-sdk'
import { AppEvents } from 'nocodb-sdk'
const props = defineProps<{
item: ProjectEventType
}>()
const item = toRef(props, 'item')
const { navigateToProject } = useGlobal()
const action = computed(() => {
switch (item.value.type) {
case AppEvents.VIEW_CREATE:
return 'created'
case AppEvents.VIEW_UPDATE:
return 'updated'
case AppEvents.VIEW_DELETE:
return 'deleted'
}
})
const onClick = () => {
if (item.value.type === AppEvents.VIEW_DELETE) return
navigateToProject({ baseId: item.value.body.id })
}
</script>
<template>
<NotificationItemWrapper :item="item" @click="onClick">
<div class="text-xs gap-2">
View
<strong>{{ item.body.title }}</strong>
{{ action }} successfully
</div>
</NotificationItemWrapper>
</template>

21
packages/nc-gui/components/notification/Item/Welcome.vue

@ -1,26 +1,15 @@
<script setup lang="ts">
import type { WelcomeEventType } from 'nocodb-sdk'
const props = defineProps<{
item: any
item: WelcomeEventType
}>()
const router = useRouter()
const route = router.currentRoute
const item = toRef(props, 'item')
const navigateToHome = () => {
if (route.value.path !== '/') {
navigateTo(`/`)
}
}
</script>
<template>
<NotificationItemWrapper :item="item" @click="navigateToHome">
<template #avatar>
<img src="~/assets/img/icons/64x64.png" class="w-6" />
</template>
<div class="text-xs">Welcome to <strong>NocoHUB!</strong> Were excited to have you onboard.</div>
<NotificationItemWrapper :item="item">
<div>Welcome to <span class="font-semibold">NocoDB!</span> Were excited to have you onboard.</div>
</NotificationItemWrapper>
</template>

119
packages/nc-gui/components/notification/Item/Wrapper.vue

@ -1,94 +1,73 @@
<script setup lang="ts">
import { timeAgo } from 'nocodb-sdk'
import type { NotificationType } from 'nocodb-sdk'
import { timeAgo } from '~/utils/datetimeUtils'
const props = defineProps<{
item: {
created_at: any
}
item: NotificationType
}>()
const item = toRef(props, 'item')
const { isMobileMode } = useGlobal()
const notificationStore = useNotification()
const { markAsRead } = notificationStore
const { toggleRead, deleteNotification } = notificationStore
</script>
<template>
<div
class="flex items-center gap-1 cursor-pointer nc-notification-item-wrapper"
:class="{
active: !item.is_read,
}"
>
<div class="nc-notification-dot" :class="{ active: !item.is_read }"></div>
<div class="nc-avatar-wrapper">
<div class="flex pl-6 pr-4 w-full overflow-x-hidden group py-4 hover:bg-gray-50 gap-3 relative cursor-pointer">
<div class="w-9.625">
<slot name="avatar">
<div class="nc-notification-avatar"></div>
<img src="~assets/img/brand/nocodb-logo.svg" alt="NocoDB" class="w-8" />
</slot>
</div>
<div class="flex-grow ml-3">
<div class="flex items-center">
<slot />
</div>
<div
v-if="item"
class="text-xs text-gray-500 mt-1"
<div class="text-[13px] min-h-12 w-full leading-5">
<slot />
</div>
<div v-if="item" class="text-xs whitespace-nowrap absolute right-4.1 bottom-5 text-gray-600">
{{ timeAgo(item.created_at) }}
</div>
<div class="flex items-start">
<NcTooltip v-if="!item.is_read">
<template #title>
<span>Mark as read</span>
</template>
<NcButton
:class="{
'!opacity-100': isMobileMode,
}"
type="secondary"
class="!border-0 transition-all duration-100 opacity-0 !group-hover:opacity-100"
size="xsmall"
@click.stop="() => toggleRead(item)"
>
<GeneralIcon icon="check" class="text-gray-700" />
</NcButton>
</NcTooltip>
<NcDropdown
v-else
:class="{
'text-primary': !item.is_read,
'!opacity-100': isMobileMode,
}"
class="transition-all duration-100 opacity-0 !group-hover:opacity-100"
>
{{ timeAgo(item.created_at) }}
</div>
</div>
<div @click.stop>
<a-dropdown>
<GeneralIcon v-if="!item.is_read" icon="threeDotVertical" class="nc-notification-menu-icon" />
<NcButton size="xsmall" type="secondary" @click.stop>
<GeneralIcon icon="threeDotVertical" />
</NcButton>
<template #overlay>
<a-menu>
<a-menu-item @click="markAsRead(item)">
<div class="p-2 text-xs">Mark as read</div>
</a-menu-item>
</a-menu>
<NcMenu>
<NcMenuItem @click.stop="() => toggleRead(item)"> Mark as unread </NcMenuItem>
<NcDivider />
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click.stop="deleteNotification(item)"> Delete </NcMenuItem>
</NcMenu>
</template>
</a-dropdown>
</NcDropdown>
</div>
</div>
</template>
<style scoped lang="scss">
.nc-avatar-wrapper {
@apply min-w-6 h-6 flex items-center justify-center;
}
.nc-notification-avatar {
@apply w-6 h-6 rounded-full text-white font-weight-bold uppercase bg-gray-100;
font-size: 0.7rem;
}
.nc-notification-dot {
@apply min-w-2 min-h-2 mr-1 rounded-full;
&.active {
@apply bg-accent bg-opacity-100;
}
}
.nc-notification-item-wrapper {
.nc-notification-menu-icon {
@apply !text-12px text-gray-500 opacity-0 transition-opacity duration-200 cursor-pointer;
}
&:hover {
.nc-notification-menu-icon {
@apply opacity-100;
}
}
&.active {
@apply bg-primary bg-opacity-4;
}
@apply py-3 px-3;
}
</style>
<style scoped lang="scss"></style>

36
packages/nc-gui/components/notification/Menu.vue

@ -1,38 +1,24 @@
<script lang="ts" setup>
const notificationStore = useNotification()
const { loadNotifications, markAsOpened } = notificationStore
onMounted(async () => {
await loadNotifications()
})
const onOpen = (visible: boolean) => {
if (visible) {
markAsOpened()
}
}
const { unreadCount } = toRefs(notificationStore)
</script>
<template>
<div class="cursor-pointer flex items-center">
<a-dropdown :trigger="['click']" @visible-change="onOpen">
<div class="relative leading-none">
<NcDropdown overlay-class-name="!shadow-none" placement="bottomRight" :trigger="['click']">
<NcButton size="small" class="!border-none !bg-gray-50" type="secondary">
<span
v-if="unreadCount"
:key="unreadCount"
class="bg-red-500 w-2 h-2 border-1 border-white rounded-[6px] absolute top-[5px] left-[15px]"
></span>
<GeneralIcon icon="notification" />
<GeneralIcon icon="menuDown" />
<span v-if="!notificationStore.isOpened && notificationStore.unreadCount" class="nc-count-badge">{{
notificationStore.unreadCount
}}</span>
</div>
</NcButton>
<template #overlay>
<NotificationCard />
</template>
</a-dropdown>
</NcDropdown>
</div>
</template>
<style scoped>
.nc-count-badge {
@apply absolute flex items-center top-[-6px] right-[-6px] px-1 min-w-[14px] h-[14px] rounded-full bg-accent bg-opacity-100 text-white !text-[9px] !z-21;
}
</style>

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

@ -153,11 +153,11 @@ onMounted(async () => {
})
const selected = reactive<{
[key: number]: boolean
[key: string]: boolean
}>({})
const toggleSelectAll = (value: boolean) => {
filteredCollaborators.value.forEach((_, i) => {
filteredCollaborators.value.forEach((_) => {
selected[_.id] = value
})
}

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

@ -1,16 +1,13 @@
<script lang="ts" setup>
import type { SourceType, TableType } from 'nocodb-sdk'
import dayjs from 'dayjs'
import NcTooltip from '~/components/nc/Tooltip.vue'
const { activeTables } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore()
const { openedProject, isDataSourceLimitReached } = storeToRefs(useBases())
const { openedProject } = storeToRefs(useBases())
const { base } = useBase()
const isNewBaseModalOpen = ref(false)
const { isUIAllowed } = useRoles()
const { $e } = useNuxtApp()
@ -68,12 +65,6 @@ function openTableCreateDialog(baseIndex?: number | undefined) {
close(1000)
}
}
const onCreateBaseClick = () => {
if (isDataSourceLimitReached.value) return
isNewBaseModalOpen.value = true
}
</script>
<template>

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

@ -3,14 +3,14 @@ import { useTitle } from '@vueuse/core'
import NcLayout from '~icons/nc-icons/layout'
const props = defineProps<{
baseId: string
baseId?: string
}>()
const basesStore = useBases()
const { openedProject, activeProjectId, basesUser, bases } = storeToRefs(basesStore)
const { activeTables, activeTable } = storeToRefs(useTablesStore())
const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
const { activeWorkspace } = storeToRefs(useWorkspace())
const { navigateToProjectPage } = useBase()
@ -35,17 +35,11 @@ const currentBase = computedAsync(async () => {
const { isUIAllowed, baseRoles } = useRoles()
const { base } = storeToRefs(useBase())
const { projectPageTab } = storeToRefs(useConfigStore())
const { isMobileMode } = useGlobal()
const baseSettingsState = ref('')
const userCount = computed(() =>
isEeUI ? workspaceUserCount : activeProjectId.value ? basesUser.value.get(activeProjectId.value)?.length : 0,
)
const userCount = computed(() => (activeProjectId.value ? basesUser.value.get(activeProjectId.value)?.length : 0))
watch(
() => route.value.query?.page,

94
packages/nc-gui/components/shared-view/AskPassword.vue

@ -1,9 +1,15 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import type { InputPassword } from 'ant-design-vue'
import { ViewTypes } from 'nocodb-sdk'
import gridImage from '~/assets/img/views/grid.png'
import galleryImage from '~/assets/img/views/gallery.png'
import kanbanImage from '~/assets/img/views/kanban.png'
import calendarImage from '~/assets/img/views/calendar.png'
const props = defineProps<{
modelValue: boolean
viewType?: ViewTypes
}>()
const emit = defineEmits(['update:modelValue'])
@ -16,47 +22,103 @@ const { loadSharedView } = useSharedView()
const formState = ref({ password: undefined })
const passwordError = ref<string | null>(null)
const onFinish = async () => {
try {
await loadSharedView(route.params.viewId as string, formState.value.password)
vModel.value = false
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
const error = await extractSdkResponseErrorMsgv2(e)
console.error(error.message)
if (error.error === NcErrorType.INVALID_SHARED_VIEW_PASSWORD) {
passwordError.value = error.message
} else {
message.error(error.message)
}
}
}
const focus: VNodeRef = (el: typeof InputPassword) => el?.$el?.querySelector('input').focus()
const focus: VNodeRef = (el: typeof InputPassword) => {
return el && el?.focus?.()
}
watch(
() => formState.value.password,
() => {
passwordError.value = null
},
)
const bgImageName = computed(() => {
switch (props.viewType) {
case ViewTypes.GRID:
return gridImage
case ViewTypes.GALLERY:
return galleryImage
case ViewTypes.KANBAN:
return kanbanImage
case ViewTypes.CALENDAR:
return calendarImage
default:
return gridImage
}
})
</script>
<template>
<NcModal v-model:visible="vModel" c size="small" :class="{ active: vModel }" :mask-closable="false">
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="key" />
<NcModal
v-model:visible="vModel"
c
size="small"
:class="{ active: vModel }"
:mask-closable="false"
:mask-style="{
backgroundColor: 'rgba(255, 255, 255, 0.64)',
backdropFilter: 'blur(8px)',
}"
>
<div class="flex flex-col gap-5">
<div class="flex flex-row items-center gap-x-2 text-base font-weight-700 text-gray-800">
<GeneralIcon icon="ncKey" class="!text-base w-5 h-5" />
{{ $t('msg.thisSharedViewIsProtected') }}
</div>
</template>
<div class="mt-2">
<a-form ref="formRef" :model="formState" name="create-new-table-form" @finish="onFinish">
<a-form-item name="password" :rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]">
<a-form-item
name="password"
:rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]"
class="!mb-0"
>
<a-input-password
ref="focus"
:ref="focus"
v-model:value="formState.password"
class="nc-input-md"
class="!rounded-lg !text-small"
hide-details
size="large"
:placeholder="$t('msg.enterPassword')"
/>
<Transition name="layout">
<div v-if="passwordError" class="mb-2 text-sm text-red-500">{{ passwordError }}</div>
</Transition>
</a-form-item>
</a-form>
<div class="flex flex-row justify-end gap-x-2 mt-6">
<NcButton type="primary" html-type="submit" @click="onFinish"
>{{ $t('general.unlock') }}
<div class="flex flex-row justify-end gap-x-2">
<NcButton
:disabled="!formState.password"
type="primary"
size="small"
html-type="submit"
class="!px-2"
data-testid="nc-shared-view-password-submit-btn"
@click="onFinish"
>
{{ $t('objects.view') }}
<template #loading> {{ $t('msg.verifyingPassword') }}</template>
</NcButton>
</div>
</div>
</NcModal>
<img alt="view image" :src="bgImageName" class="fixed inset-0 w-full h-full" />
</template>

4
packages/nc-gui/components/shared-view/Calendar.vue

@ -25,10 +25,10 @@ useProvideCalendarViewStore(meta, sharedView, true, nestedFilters)
</script>
<template>
<div class="nc-container h-full mt-1.5 px-12">
<div class="nc-container h-full">
<div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar />
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">
<div class="h-full flex-1 min-w-0 min-h-0">
<LazySmartsheetCalendar />
</div>
</div>

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -23,7 +23,7 @@ useProvideKanbanViewStore(meta, sharedView)
</script>
<template>
<div class="nc-container h-full mt-1.5 px-12">
<div class="nc-container h-full">
<div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar />
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">

2
packages/nc-gui/components/shared-view/Grid.vue

@ -45,7 +45,7 @@ watch(
</script>
<template>
<div class="nc-container flex flex-col h-full mt-1.5 px-12">
<div class="nc-container flex flex-col h-full">
<LazySmartsheetToolbar />
<LazySmartsheetGrid />
</div>

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -23,7 +23,7 @@ useProvideKanbanViewStore(meta, sharedView, true)
</script>
<template>
<div class="nc-container h-full mt-1.5 px-12">
<div class="nc-container h-full">
<div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar />
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">

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

@ -186,7 +186,7 @@ const onContextmenu = (e: MouseEvent) => {
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div
v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)"
v-if="((isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column) && !isTextArea(column)"
class="nc-locked-overlay"
/>
</template>

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

@ -76,7 +76,7 @@ watch(openedSubTab, () => {
<template #tab>
<div class="tab" data-testid="nc-apis-tab">
<GeneralIcon icon="code" class="tab-icon" :class="{}" />
<div>{{ $t('labels.apis') }}</div>
<div>{{ $t('labels.apiSnippet') }}</div>
</div>
</template>
<SmartsheetDetailsApi v-if="base && meta && view" />

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

@ -85,6 +85,7 @@ const {
validateInfos,
validate,
clearValidate,
fieldMappings,
} = useProvideFormViewStore(meta, view, formViewData, updateFormView, isEditable)
const { preFillFormSearchParams } = storeToRefs(useViewsStore())
@ -198,9 +199,9 @@ async function submitForm() {
}
try {
await validate([...Object.keys(formState.value)])
await validate(Object.keys(formState.value).map((title) => fieldMappings.value[title]))
} catch (e: any) {
if (e.errorFields.length) {
if (e?.errorFields?.length) {
message.error(t('msg.error.someOfTheRequiredFieldsAreEmpty'))
return
}
@ -586,7 +587,7 @@ watch(
updatePreFillFormSearchParams()
try {
await validate([...Object.keys(formState.value)])
await validate(Object.keys(formState.value).map((title) => fieldMappings.value[title]))
} catch {}
},
{
@ -1103,9 +1104,10 @@ useEventListener(
<div class="nc-form-field-body">
<div class="mt-2">
<a-form-item
:name="element.title"
v-if="fieldMappings[element.title]"
:name="fieldMappings[element.title]"
class="!my-0 nc-input-required-error nc-form-input-item"
v-bind="validateInfos[element.title]"
v-bind="validateInfos[fieldMappings[element.title]]"
>
<LazySmartsheetDivDataCell class="relative" @click.stop>
<LazySmartsheetVirtualCell

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

@ -46,6 +46,8 @@ const router = useRouter()
const { getPossibleAttachmentSrc } = useAttachment()
const { isMobileMode } = useGlobal()
const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
const displayField = computed(() => meta.value?.columns?.find((c) => c.pv && fields.value.includes(c)) ?? null)
@ -56,12 +58,12 @@ const coverImageColumn: any = computed(() =>
: {},
)
const isRowEmpty = (record: any, col: any) => {
const val = record.row[col.title]
if (!val) return true
const coverImageObjectFitClass = computed(() => {
const fk_cover_image_object_fit = parseProp(galleryData.value?.meta)?.fk_cover_image_object_fit || CoverImageObjectFit.FIT
return Array.isArray(val) && val.length === 0
}
if (fk_cover_image_object_fit === CoverImageObjectFit.FIT) return '!object-contain'
if (fk_cover_image_object_fit === CoverImageObjectFit.COVER) return '!object-cover'
})
const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
@ -75,9 +77,9 @@ const contextMenu = computed({
}
},
})
const contextMenuTarget = ref<{ row: number } | null>(null)
const contextMenuTarget = ref<{ row: RowType; index: number } | null>(null)
const showContextMenu = (e: MouseEvent, target?: { row: number }) => {
const showContextMenu = (e: MouseEvent, target?: { row: RowType; index: number }) => {
if (isSqlView.value) return
e.preventDefault()
if (target) {
@ -100,6 +102,7 @@ const attachments = (record: any): Attachment[] => {
const expandForm = (row: RowType, state?: Record<string, any>) => {
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
expandedFormRowState.value = state
if (rowId && !isPublic.value) {
router.push({
@ -110,7 +113,6 @@ const expandForm = (row: RowType, state?: Record<string, any>) => {
})
} else {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
}
@ -181,61 +183,75 @@ watch(
</script>
<template>
<a-dropdown
<NcDropdown
v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']"
overlay-class-name="nc-dropdown-grid-context-menu"
>
<template #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
<div v-e="['a:row:delete']" class="nc-base-menu-item">
<NcMenu @click="contextMenu = false">
<NcMenuItem v-if="contextMenuTarget" @click="expandForm(contextMenuTarget.row)">
<div v-e="['a:row:expand-record']" class="flex items-center gap-2">
<component :is="iconMap.expand" class="flex" />
<!-- Expand Record -->
{{ $t('activity.expandRecord') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="contextMenuTarget?.index !== undefined"
class="!text-red-600 !hover:bg-red-50"
@click="deleteRow(contextMenuTarget.index)"
>
<div v-e="['a:row:delete']" class="flex items-center gap-2">
<component :is="iconMap.delete" class="flex" />
<!-- Delete Row -->
{{ $t('activity.deleteRow') }}
</div>
</a-menu-item>
</NcMenuItem>
<!-- <a-menu-item v-if="contextMenuTarget" @click="openNewRecordFormHook.trigger()"> -->
<!-- <div v-e="['a:row:insert']" class="nc-base-menu-item"> -->
<!-- <NcMenuItem v-if="contextMenuTarget" @click="openNewRecordFormHook.trigger()"> -->
<!-- <div v-e="['a:row:insert']" class="flex items-center gap-2"> -->
<!-- &lt;!&ndash; Insert New Row &ndash;&gt; -->
<!-- {{ $t('activity.insertRow') }} -->
<!-- </div> -->
<!-- </a-menu-item> -->
</a-menu>
<!-- </NcMenuItem> -->
</NcMenu>
</template>
<div
class="flex flex-col w-full nc-gallery nc-scrollbar-md bg-[#fbfbfb]"
class="flex flex-col w-full nc-gallery nc-scrollbar-md bg-gray-50"
data-testid="nc-gallery-wrapper"
style="height: calc(100% - var(--topbar-height) + 0.7rem)"
:style="{ height: isMobileMode ? 'calc(100% - var(--topbar-height))' : 'calc(100% - var(--topbar-height) + 0.7rem)' }"
:class="{
'!overflow-hidden': isViewDataLoading,
}"
>
<div v-if="isViewDataLoading" class="flex flex-col h-full">
<div class="flex flex-row p-3 !pr-1 gap-x-2 flex-wrap gap-y-2">
<div class="nc-gallery-container-skeleton grid gap-3 p-3">
<a-skeleton-input v-for="index of Array(20)" :key="index" class="!min-w-60.5 !h-96 !rounded-md overflow-hidden" />
</div>
</div>
<div v-else class="nc-gallery-container grid gap-3 my-4 px-3">
<div v-else class="nc-gallery-container grid gap-3 p-3">
<div v-for="(record, rowIndex) in data" :key="`record-${record.row.id}`">
<LazySmartsheetRow :row="record">
<a-card
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer"
:body-style="{ padding: '0px' }"
class="!rounded-xl h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] cursor-pointer"
:body-style="{ padding: '16px !important' }"
:data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })"
@contextmenu="showContextMenu($event, { row: record, index: rowIndex })"
>
<template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel
v-if="!reloadAttachments && attachments(record).length"
class="gallery-carousel !border-b-1 !border-gray-200"
class="gallery-carousel !border-b-1 !border-gray-200 min-h-52"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div>
<div></div>
</div>
</a>
@ -243,17 +259,25 @@ watch(
<template #prevArrow>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
<NcButton
type="secondary"
size="xsmall"
class="!absolute !left-1.5 !bottom-[-90px] !opacity-0 !group-hover:opacity-100 !rounded-lg cursor-pointer"
>
<GeneralIcon icon="arrowLeft" class="text-gray-700 w-4 h-4" />
</NcButton>
</div>
</template>
<template #nextArrow>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
<NcButton
type="secondary"
size="xsmall"
class="!absolute !right-1.5 !bottom-[-90px] !opacity-0 !group-hover:opacity-100 !rounded-lg cursor-pointer"
>
<GeneralIcon icon="arrowRight" class="text-gray-700 w-4 h-4" />
</NcButton>
</div>
</template>
@ -261,7 +285,8 @@ watch(
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 !object-contain"
class="h-52"
:class="[`${coverImageObjectFitClass}`]"
:srcs="getPossibleAttachmentSrc(attachment)"
@click="expandFormClick($event, record)"
/>
@ -271,75 +296,87 @@ watch(
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template>
<h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-brand-500"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-brand-500"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col first:mt-3 ml-2 !pr-3.5 !mb-[0.75rem] rounded-lg w-full">
<div class="flex flex-row w-full justify-start scale-75">
<div class="w-full pb-1 text-gray-300">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
:hide-icon="true"
/>
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" :hide-icon="true" />
</div>
</div>
<div
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
>
<div class="flex flex-col gap-3 !children:pointer-events-none">
<h2 v-if="displayField" class="nc-card-display-value-wrapper">
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
:column="col"
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-brand-500"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[col.title]"
:column="col"
v-model="record.row[displayField.title]"
class="!text-brand-500"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</template>
<template v-else> - </template>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col rounded-lg w-full">
<div class="flex flex-row w-full justify-start">
<div class="nc-card-col-header w-full !children:text-gray-500">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div>
<div
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-800 items-center justify-start min-h-7 py-1"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
:column="col"
:row="record"
class="!text-gray-800"
/>
<LazySmartsheetCell
v-else
v-model="record.row[col.title]"
:column="col"
:edit-enabled="false"
:read-only="true"
class="!text-gray-800"
/>
</div>
<div v-else class="flex flex-row w-full h-7 pl-1 items-center justify-start">-</div>
</div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div>
</div>
</a-card>
</LazySmartsheetRow>
</div>
<template v-if="data.length <= 4">
<div v-for="index of Array(8 - data.length)" :key="index" class="nc-empty-card"></div>
</template>
</div>
</div>
</a-dropdown>
<LazySmartsheetPagination v-model:pagination-data="paginationData" show-api-timing :change-page="changePage" />
</NcDropdown>
<LazySmartsheetPagination
v-model:pagination-data="paginationData"
align-count-on-right
show-api-timing
:change-page="changePage"
/>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
:load-row="!isPublic"
:state="expandedFormRowState"
:meta="meta"
:view="view"
@ -350,19 +387,22 @@ watch(
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg && meta?.id"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:row="expandedFormRow ?? { row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:load-row="!isPublic"
:row-id="route.query.rowId"
:view="view"
show-next-prev-icons
:expand-form="expandForm"
@next="navigateToSiblingRow(NavigateDir.NEXT)"
@prev="navigateToSiblingRow(NavigateDir.PREV)"
/>
</Suspense>
</template>
<style scoped>
.nc-gallery-container {
<style lang="scss" scoped>
.nc-gallery-container,
.nc-gallery-container-skeleton {
@apply auto-rows-[1fr] grid-cols-[repeat(auto-fit,minmax(250px,1fr))];
}
@ -371,8 +411,7 @@ watch(
}
.ant-carousel.gallery-carousel :deep(.slick-dots) {
@apply !w-auto absolute h-auto bottom-[-15px] absolute h-auto;
height: auto;
@apply !w-full max-w-[calc(100%_-_36%)] absolute left-0 right-0 bottom-[-18px] h-6 overflow-x-auto nc-scrollbar-thin !mx-auto;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
@ -394,4 +433,124 @@ watch(
.ant-carousel.gallery-carousel :deep(.slick-next) {
@apply right-0;
}
:deep(.ant-card) {
@apply transition-all duration-0.3s;
box-shadow: 0px 2px 4px -2px rgba(0, 0, 0, 0.06), 0px 4px 4px -2px rgba(0, 0, 0, 0.02);
&:hover {
@apply !border-gray-300;
box-shadow: 0px 0px 24px 0px rgba(0, 0, 0, 0.1), 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
.nc-action-icon {
@apply invisible;
}
}
}
.nc-card-display-value-wrapper {
@apply my-0 text-xl leading-8 text-gray-600;
.nc-cell,
.nc-virtual-cell {
@apply text-xl leading-8;
:deep(.nc-cell-field),
:deep(input),
:deep(textarea),
:deep(.nc-cell-field-link) {
@apply !text-xl leading-8 text-gray-600;
}
}
}
.nc-card-col-header {
:deep(.nc-cell-icon),
:deep(.nc-virtual-cell-icon) {
@apply ml-0 !w-3.5 !h-3.5;
}
}
:deep(.nc-cell),
:deep(.nc-virtual-cell) {
@apply text-small leading-[18px];
.nc-cell-field,
input,
textarea,
.nc-cell-field-link {
@apply !text-small !leading-[18px];
}
}
:deep(.nc-cell) {
&.nc-cell-longtext {
.long-text-wrapper {
@apply min-h-1;
.nc-readonly-rich-text-wrapper {
@apply !min-h-1;
}
.nc-rich-text {
@apply pl-0;
.tiptap.ProseMirror {
@apply -ml-1 min-h-1;
}
}
}
}
&.nc-cell-checkbox {
@apply children:pl-0;
}
&.nc-cell-singleselect .nc-cell-field > div {
@apply flex items-center;
}
&.nc-cell-multiselect .nc-cell-field > div {
@apply h-5;
}
&.nc-cell-email,
&.nc-cell-phonenumber {
@apply flex items-center;
}
&.nc-cell-email,
&.nc-cell-phonenumber,
&.nc-cell-url {
.nc-cell-field-link {
@apply py-0;
}
}
}
:deep(.nc-virtual-cell) {
.nc-links-wrapper {
@apply py-0 children:min-h-4;
}
&.nc-virtual-cell-linktoanotherrecord {
.chips-wrapper {
@apply min-h-4 !children:min-h-4;
.chip.group {
@apply my-0;
}
}
}
&.nc-virtual-cell-lookup {
.nc-lookup-cell {
@apply !h-5.5;
.nc-cell-lookup-scroll {
@apply py-0 h-auto;
}
}
}
&.nc-virtual-cell-formula {
.nc-cell-field {
@apply py-0;
}
}
&.nc-virtual-cell-qrcode,
&.nc-virtual-cell-barcode {
@apply children:justify-start;
}
}
</style>

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

File diff suppressed because it is too large Load Diff

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

@ -51,7 +51,8 @@ const getMapCenterLocalStorageKey = (viewId: string) => `mapView.${viewId}.cente
const expandForm = (row: Row, state?: Record<string, any>) => {
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) {
if (rowId && !isPublic.value) {
router.push({
query: {
...route.query,
@ -236,6 +237,7 @@ const count = computed(() => paginationData.value.totalRows)
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
:load-row="!isPublic"
:state="expandedFormRowState"
:meta="meta"
:view="view"
@ -245,9 +247,11 @@ const count = computed(() => paginationData.value.totalRows)
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg && meta?.id"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:row="expandedFormRow ?? { row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:load-row="!isPublic"
:row-id="route.query.rowId"
:expand-form="expandForm"
:view="view"
/>
</Suspense>

7
packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue

@ -15,13 +15,6 @@ const { loadData } = useViewData(meta, view)
provide(IsFormInj, ref(false))
provide(IsGridInj, ref(false))
const isRowEmpty = (record: any, col: any) => {
const val = record.row[col.title]
if (!val) return true
return Array.isArray(val) && val.length === 0
}
reloadViewDataHook?.on(async () => {
await loadData()
})

12
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -1,8 +1,6 @@
<script lang="ts" setup>
const { isGrid, isGallery, isKanban, isMap, isCalendar } = useSmartsheetStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false))
const { isMobileMode } = useGlobal()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
@ -16,13 +14,11 @@ const isTab = computed(() => {
if (!isCalendar.value) return false
return width.value > 1200
})
const { allowCSVDownload } = useSharedView()
</script>
<template>
<div
v-if="!isMobileMode || isCalendar"
v-if="!isMobileMode"
ref="containerRef"
:class="{
'px-4': isMobileMode,
@ -47,10 +43,10 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarCalendarRange v-if="isCalendar" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban || isMap" :show-system-fields="false" />
<LazySmartsheetToolbarStackedBy v-if="isKanban" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban || isMap" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban || isMap" />
<LazySmartsheetToolbarGroupByMenu v-if="isGrid" />
@ -65,8 +61,6 @@ const { allowCSVDownload } = useSharedView()
<!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> -->
<LazySmartsheetToolbarExport v-if="isPublic && allowCSVDownload" />
<div class="flex-1" />
</template>

11
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -40,10 +40,13 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
<template>
<div
class="nc-virtual-cell w-full flex items-center"
:class="{
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
'nc-display-value-cell': isPrimary(column) && !isForm,
}"
:class="[
`nc-virtual-cell-${(column.uidt || 'default').toLowerCase()}`,
{
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
'nc-display-value-cell': isPrimary(column) && !isForm,
},
]"
@keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
>

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

@ -26,6 +26,8 @@ const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref())
const isPublic = inject(IsPublicInj, ref(false))
const { fields: _fields } = useViewColumnsOrThrow()
const fieldStyles = computed(() => {
@ -56,6 +58,29 @@ const hours = computed(() => {
return hours
})
const currTime = ref(dayjs())
const overlayTop = computed(() => {
const perRecordHeight = 52
const minutes = currTime.value.minute() + currTime.value.hour() * 60
const top = (perRecordHeight / 60) * minutes
return top
})
onMounted(() => {
const intervalId = setInterval(() => {
currTime.value = dayjs()
}, 10000) // 10000 ms = 10 seconds
// Clean up the interval when the component is unmounted
onUnmounted(() => {
clearInterval(intervalId)
})
})
const calculateNewDates = useMemoize(
({
endDate,
@ -166,12 +191,21 @@ const getMaxOverlaps = ({
return maxOverlaps
}
let maxOverlaps = 1
const id = row.rowMeta.id as string
if (graph.has(id)) {
maxOverlaps = dfs(id)
dfs(id)
}
return maxOverlaps
const overlapIterations: Array<number> = []
columnArray
.flat()
.filter((record) => visited.has(record.rowMeta.id!))
.forEach((record) => {
overlapIterations.push(record.rowMeta.overLapIteration!)
})
return Math.max(...overlapIterations)
}
const recordsAcrossAllRange = computed<{
@ -246,8 +280,8 @@ const recordsAcrossAllRange = computed<{
const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight)
const style: Partial<CSSStyleDeclaration> = {
height: `${heightInPixels - 8}px`,
top: `${topInPixels + 4}px`,
height: `${heightInPixels - 2}px`,
top: `${topInPixels + 1}px`,
}
// This property is used to determine which side the record should be rounded. It can be top, bottom, both or none
@ -298,8 +332,8 @@ const recordsAcrossAllRange = computed<{
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 52, perRecordHeight)
style = {
...style,
top: `${topInPixels + 4}px`,
height: `${heightInPixels - 8}px`,
top: `${topInPixels + 1}px`,
height: `${heightInPixels - 2}px`,
}
recordsByRange.push({
@ -681,13 +715,13 @@ const stopDrag = (event: MouseEvent) => {
}
const dragStart = (event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
let target = event.target as HTMLElement
isDragging.value = false
// We use a timeout to determine if the user is dragging or clicking on the record
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
@ -868,6 +902,24 @@ watch(
data-testid="nc-calendar-day-view"
@drop="dropEvent"
>
<div
v-if="!isPublic && dayjs().isSame(selectedDate, 'day')"
class="absolute ml-2 pointer-events-none w-full z-4"
:style="{
top: `${overlayTop}px`,
}"
>
<div class="flex w-full items-center">
<span
class="text-brand-500 text-xs rounded-md border-1 pointer-events-auto px-0.5 border-brand-200 cursor-pointer bg-brand-50"
@click="newRecord(dayjs())"
>
{{ dayjs().format('hh:mm A') }}
</span>
<div class="flex-1 border-b-1 border-brand-500"></div>
</div>
</div>
<div>
<div
v-for="(hour, index) in hours"
@ -895,7 +947,7 @@ watch(
@dblclick="newRecord(hour)"
>
<NcDropdown
v-if="calendarRange.length > 1"
v-if="calendarRange.length > 1 && !isPublic"
:class="{
'!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),
@ -903,7 +955,7 @@ watch(
auto-close
>
<NcButton
class="!group-hover:block mr-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
class="!group-hover:block mr-12 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
size="xsmall"
type="secondary"
>
@ -944,12 +996,12 @@ watch(
</template>
</NcDropdown>
<NcButton
v-else
v-else-if="!isPublic"
:class="{
'!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),
}"
class="!group-hover:block mr-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
class="!group-hover:block mr-12 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
size="xsmall"
type="secondary"
@click="

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

@ -14,6 +14,7 @@ const {
sideBarFilterOption,
displayField,
calendarRange,
viewMetaProperties,
showSideMenu,
updateRowProperty,
} = useCalendarViewStoreOrThrow()
@ -26,12 +27,24 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const maxVisibleDays = computed(() => {
return viewMetaProperties.value?.hide_weekend ? 5 : 7
})
const days = computed(() => {
let days = []
if (isMondayFirst.value) {
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
} else {
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
}
if (maxVisibleDays.value === 5) {
days = days.filter((day) => day !== 'Sat' && day !== 'Sun')
}
return days
})
const calendarGridContainer = ref()
@ -42,6 +55,14 @@ const isDayInPagedMonth = (date: dayjs.Dayjs) => {
return date.month() === selectedMonth.value.month()
}
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
@ -115,7 +136,7 @@ const recordsToDisplay = computed<{
}>(() => {
if (!dates.value || !calendarRange.value) return []
const perWidth = gridContainerWidth.value / 7
const perWidth = gridContainerWidth.value / maxVisibleDays.value
const perHeight = gridContainerHeight.value / dates.value.length
const perRecordHeight = 24
@ -176,6 +197,12 @@ const recordsToDisplay = computed<{
width: `${perWidth}px`,
}
if (maxVisibleDays.value === 5) {
if (dayIndex === 5 || dayIndex === 6) {
style.display = 'none'
}
}
// Number of records in that day
const recordIndex = recordsInDay[dateKey].count
@ -364,12 +391,15 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeC
if (!fromCol) return { newRow: null, updateProperty: [] }
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return { newRow: null, updateProperty: [] }
const fromDate = dayjs(dragRecord.value.row[fromCol.title!])
let fromDate = dayjs(dragRecord.value.row[fromCol.title!])
if (!fromDate.isValid()) {
fromDate = dayjs()
}
newStartDate = newStartDate.add(fromDate.hour(), 'hour').add(fromDate.minute(), 'minute').add(fromDate.second(), 'second')
@ -461,7 +491,7 @@ const onResize = (event: MouseEvent) => {
const toCol = resizeRecord.value.rowMeta.range?.fk_to_col
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
let updateProperty: string[] = []
let newRow: Row
@ -567,11 +597,12 @@ const stopDrag = (event: MouseEvent) => {
}
const dragStart = (event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit') || resizeInProgress.value || !record.rowMeta.id) return
if (resizeInProgress.value || !record.rowMeta.id) return
let target = event.target as HTMLElement
isDragging.value = false
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
@ -670,11 +701,17 @@ const addRecord = (date: dayjs.Dayjs) => {
<template>
<div v-if="calendarRange" class="h-full prevent-select relative" data-testid="nc-calendar-month-view">
<div class="grid grid-cols-7">
<div
class="grid"
:class="{
'grid-cols-7': maxVisibleDays === 7,
'grid-cols-5': maxVisibleDays === 5,
}"
>
<div
v-for="(day, index) in days"
:key="index"
class="text-center bg-gray-50 py-1 border-b-1 border-r-1 last:border-r-0 border-gray-200 font-regular uppercase text-xs text-gray-500"
class="text-center bg-gray-50 py-1 border-r-1 last:border-r-0 border-gray-200 font-semibold leading-4 uppercase text-[10px] text-gray-500"
>
{{ day }}
</div>
@ -690,50 +727,61 @@ const addRecord = (date: dayjs.Dayjs) => {
style="height: calc(100% - 1.59rem)"
@drop="dropEvent"
>
<div v-for="(week, weekIndex) in dates" :key="weekIndex" class="grid grid-cols-7 grow" data-testid="nc-calendar-month-week">
<div
v-for="(day, dateIndex) in week"
:key="`${weekIndex}-${dateIndex}`"
:class="{
'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 transition text-sm h-full border-r-1 border-b-1 border-gray-200 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
:class="{
block: !isDateSelected(day),
hidden: isDateSelected(day),
}"
class="group-hover:hidden"
></span>
<NcDropdown v-if="calendarRange.length > 1" auto-close>
<NcButton
<div
v-for="(week, weekIndex) in dates"
:key="weekIndex"
:class="{
'grid-cols-7': maxVisibleDays === 7,
'grid-cols-5': maxVisibleDays === 5,
}"
class="grid grow"
data-testid="nc-calendar-month-week"
>
<template v-for="(day, dateIndex) in week">
<div
v-if="maxVisibleDays === 5 ? day.get('day') !== 0 && day.get('day') !== 6 : true"
:key="`${weekIndex}-${dateIndex}`"
:class="{
'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 !hover:bg-gray-100': day.get('day') === 0 || day.get('day') === 6,
'border-t-1': weekIndex === 0,
}"
class="text-right relative group last:border-r-0 transition text-sm h-full border-r-1 border-b-1 border-gray-200 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
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
block: !isDateSelected(day),
hidden: isDateSelected(day),
}"
class="!group-hover:block rounded"
size="small"
type="secondary"
>
<component :is="iconMap.plus" class="h-4 w-4" />
</NcButton>
<template #overlay>
<NcMenu class="w-64">
<NcMenuItem> Select date field to add </NcMenuItem>
<NcMenuItem
v-for="(range, index) in calendarRange"
:key="index"
class="text-gray-800 font-semibold text-sm"
@click="
class="group-hover:hidden"
></span>
<NcDropdown v-if="calendarRange.length > 1" auto-close>
<NcButton
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block rounded"
size="small"
type="secondary"
>
<component :is="iconMap.plus" class="h-4 w-4" />
</NcButton>
<template #overlay>
<NcMenu class="w-64">
<NcMenuItem> Select date field to add </NcMenuItem>
<NcMenuItem
v-for="(range, index) in calendarRange"
:key="index"
class="text-gray-800 font-semibold text-sm"
@click="
() => {
const record = {
row: {
@ -743,25 +791,25 @@ const addRecord = (date: dayjs.Dayjs) => {
emit('newRecord', record)
}
"
>
<div class="flex items-center gap-1">
<LazySmartsheetHeaderCellIcon :column-meta="range.fk_from_col" />
<span class="ml-1">{{ range.fk_from_col!.title }}</span>
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-else
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block !w-6 !h-6 !rounded"
size="xsmall"
type="secondary"
@click="
>
<div class="flex items-center gap-1">
<LazySmartsheetHeaderCellIcon :column-meta="range.fk_from_col" />
<span class="ml-1">{{ range.fk_from_col!.title }}</span>
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-else
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block !w-6 !h-6 !rounded"
size="xsmall"
type="secondary"
@click="
() => {
const record = {
row: {
@ -771,35 +819,36 @@ const addRecord = (date: dayjs.Dayjs) => {
emit('newRecord', record)
}
"
>
<component :is="iconMap.plus" />
</NcButton>
<span
:class="{
'bg-brand-50 text-brand-500 !font-bold': day.isSame(dayjs(), 'date'),
}"
class="px-1.3 py-1 text-sm leading-3 font-medium rounded-lg"
>
{{ day.format('DD') }}
</span>
</div>
<div v-if="!isUIAllowed('dataEdit')" class="leading-3 p-3">{{ dayjs(day).format('DD') }}</div>
<NcButton
v-if="
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')] &&
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow &&
!draggingId
"
v-e="`['c:calendar:month-view-more']`"
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)"
>
<component :is="iconMap.plus" />
<span class="text-xs px-1"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} </span>
</NcButton>
<span
:class="{
'bg-brand-50 text-brand-500 !font-bold': day.isSame(dayjs(), 'date'),
}"
class="px-1.3 py-1 text-sm font-medium rounded-lg"
>
{{ day.format('DD') }}
</span>
</div>
<div v-if="!isUIAllowed('dataEdit')" class="p-3">{{ dayjs(day).format('DD') }}</div>
<NcButton
v-if="
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')] &&
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow &&
!draggingId
"
v-e="`['c:calendar:month-view-more']`"
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 px-1"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} </span>
</NcButton>
</div>
</template>
</div>
</div>
<div class="absolute inset-0 pointer-events-none mt-8 pb-7.5" data-testid="nc-calendar-month-record-container">
@ -825,7 +874,6 @@ const addRecord = (date: dayjs.Dayjs) => {
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id || resizeRecord?.rowMeta?.id === record.rowMeta.id"
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
<template v-if="calDataType === UITypes.DateTime" #time>
<span class="text-xs font-medium text-gray-400">
@ -857,4 +905,8 @@ const addRecord = (date: dayjs.Dayjs) => {
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
.grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
</style>

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

@ -9,6 +9,10 @@ const props = defineProps<{
const emit = defineEmits(['expandRecord', 'newRecord'])
interface Attachment {
url: string
}
const INFINITY_SCROLL_THRESHOLD = 100
const { isUIAllowed } = useRoles()
@ -21,6 +25,10 @@ const { height } = useWindowSize()
const meta = inject(MetaInj, ref())
const { fields } = useViewColumnsOrThrow()
const { getPossibleAttachmentSrc } = useAttachment()
const { t } = useI18n()
const {
@ -45,6 +53,23 @@ const {
const sideBarListRef = ref<VNodeRef | null>(null)
const coverImageColumns: any = computed(() => {
if (!fields.value || !meta.value?.columns) return
return meta.value.columns.find((c) => c.uidt === UITypes.Attachment && fields.value?.find((f) => f.fk_column_id === c.id).show)
})
const attachments = (record: any): Attachment[] => {
const col = coverImageColumns.value
try {
if (col?.title && record.row[col.title]) {
return typeof record.row[col.title] === 'string' ? JSON.parse(record.row[col.title]) : record.row[col.title]
}
return []
} catch (e) {
return []
}
}
const pushToArray = (arr: Array<Row>, record: Row, range) => {
arr.push({
...record,
@ -381,9 +406,14 @@ onClickOutside(searchRef, toggleSearch)
>
<div class="flex px-4 items-center gap-3">
<span class="capitalize font-medium text-gray-700">{{ $t('objects.records') }}</span>
<NcSelect v-model:value="sideBarFilterOption" class="w-full !text-gray-600" data-testid="nc-calendar-sidebar-filter">
<NcSelect
v-model:value="sideBarFilterOption"
size="small"
class="w-full !h-7 !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 w-full justify-between gap-2">
<div class="flex items-center h-7 w-full justify-between gap-2">
<div class="truncate">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
@ -429,7 +459,7 @@ onClickOutside(searchRef, toggleSearch)
v-if="!showSearch"
data-testid="nc-calendar-sidebar-search-btn"
size="small"
class="!h-7"
class="!h-7 !rounded-md"
type="secondary"
@click="clickSearch"
>
@ -444,7 +474,7 @@ onClickOutside(searchRef, toggleSearch)
v-if="isUIAllowed('dataEdit') && props.visible"
v-e="['c:calendar:calendar-sidemenu-new-record-btn']"
data-testid="nc-calendar-side-menu-new-btn"
class="!h-7"
class="!h-7 !rounded-md"
size="small"
type="secondary"
@click="newRecord"
@ -497,8 +527,8 @@ onClickOutside(searchRef, toggleSearch)
:from-date="
record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('DD MMM')
: dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('DD MMM • HH:mm A')
? dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM')
: dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM • h:mm a')
: null
"
:invalid="
@ -521,6 +551,50 @@ onClickOutside(searchRef, toggleSearch)
@dragstart="dragStart($event, record)"
@dragover.prevent
>
<template v-if="coverImageColumns" #image>
<a-carousel
v-if="attachments(record).length"
class="gallery-carousel rounded-md !border-1 !border-gray-200"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div></div>
</div>
</a>
</template>
<template #prevArrow>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template #nextArrow>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-10 !w-10 !object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<div v-else class="h-10 w-10 !flex flex-row !border-1 rounded-md !border-gray-200 items-center justify-center">
<img class="object-contain w-[40px] h-[40px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetPlainCell v-model="record.row[displayField!.title!]" :column="displayField" />
</template>
@ -582,4 +656,20 @@ onClickOutside(searchRef, toggleSearch)
</div>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
:deep(.nc-attachment-image) {
@apply rounded-md;
}
:deep(.ant-select-selector) {
@apply !h-7;
}
:deep(.nc-month-picker-pagination) {
@apply !border-b-0;
}
:deep(.nc-date-week-header) {
@apply !border-b-0;
}
</style>

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

@ -16,17 +16,7 @@ const props = withDefaults(defineProps<Props>(), {
</script>
<template>
<div
:class="{
'bg-maroon-50': props.color === 'maroon',
'bg-blue-50': props.color === 'blue',
'bg-green-50': props.color === 'green',
'bg-yellow-50': props.color === 'yellow',
'bg-pink-50': props.color === 'pink',
'bg-purple-50': props.color === 'purple',
}"
class="border-1 cursor-pointer h-14 border-gray-200 flex gap-2 items-center rounded-lg"
>
<div class="border-1 cursor-pointer h-14 border-gray-200 flex gap-2 items-center rounded-lg">
<div class="flex items-center pl-2 gap-2">
<span
:class="{
@ -39,11 +29,14 @@ const props = withDefaults(defineProps<Props>(), {
}"
class="block h-10 w-1 rounded"
></span>
<slot name="image" />
<div class="flex gap-1 flex-col">
<span class="text-sm max-w-56 font-medium truncate text-gray-800">
<span class="text-[13px] leading-4 max-w-56 font-medium truncate text-gray-800">
<slot />
</span>
<span v-if="showDate" class="text-xs font-medium text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span>
<span v-if="showDate" class="text-xs font-medium leading-4 text-gray-600"
>{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span
>
</div>
</div>

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

@ -5,8 +5,20 @@ import type { Row } from '~/lib/types'
const emits = defineEmits(['expandRecord', 'newRecord'])
const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
useCalendarViewStoreOrThrow()
const {
selectedDateRange,
formattedData,
formattedSideBarData,
calendarRange,
selectedDate,
displayField,
updateRowProperty,
viewMetaProperties,
} = useCalendarViewStoreOrThrow()
const maxVisibleDays = computed(() => {
return viewMetaProperties.value?.hide_weekend ? 5 : 7
})
const container = ref<null | HTMLElement>(null)
@ -41,12 +53,17 @@ const getFieldStyle = (field: ColumnType) => {
// Calculate the dates of the week
const weekDates = computed(() => {
let startOfWeek = dayjs(selectedDateRange.value.start)
const endOfWeek = dayjs(selectedDateRange.value.end)
let endOfWeek = dayjs(selectedDateRange.value.end)
if (maxVisibleDays.value === 5) {
endOfWeek = endOfWeek.subtract(2, 'day')
}
const datesArray = []
while (startOfWeek.isBefore(endOfWeek) || startOfWeek.isSame(endOfWeek, 'day')) {
datesArray.push(dayjs(startOfWeek))
startOfWeek = startOfWeek.add(1, 'day')
}
return datesArray
})
@ -111,7 +128,7 @@ const calendarData = computed(() => {
}
const recordsInRange: Array<Row> = []
const perDayWidth = containerWidth.value / 7
const perDayWidth = containerWidth.value / maxVisibleDays.value
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
@ -284,7 +301,7 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
let updateProperty: string[] = []
let updateRecord: Row
@ -370,7 +387,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
// Calculate the day index based on the percentage of the width
// The day index is a number between 0 and 6
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
// Calculate the new start date based on the day index by adding the day index to the start date of the selected date range
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
@ -467,13 +484,13 @@ const stopDrag = (event: MouseEvent) => {
}
const dragStart = (event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
if (resizeInProgress.value) return
let target = event.target as HTMLElement
isDragging.value = false
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
@ -557,8 +574,10 @@ const addRecord = (date: dayjs.Dayjs) => {
:key="weekIndex"
:class="{
'!border-brand-500 !border-b-gray-200': dayjs(date).isSame(selectedDate, 'day'),
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="w-1/7 cursor-pointer text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
class="cursor-pointer text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
@click="selectDate(date)"
@dblclick="addRecord(date)"
>
@ -572,8 +591,10 @@ const addRecord = (date: dayjs.Dayjs) => {
: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,
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="flex cursor-pointer flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
class="flex cursor-pointer flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center"
data-testid="nc-calendar-week-day"
@click="selectDate(date)"
@dblclick="addRecord(date)"

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

@ -11,6 +11,7 @@ const {
formattedSideBarData,
calendarRange,
displayField,
viewMetaProperties,
selectedTime,
updateRowProperty,
sideBarFilterOption,
@ -25,6 +26,8 @@ const scrollContainer = ref<null | HTMLElement>(null)
const { width: containerWidth } = useElementSize(container)
const isPublic = inject(IsPublicInj, ref(false))
const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
@ -47,6 +50,50 @@ const fieldStyles = computed(() => {
)
})
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const maxVisibleDays = computed(() => {
return viewMetaProperties.value?.hide_weekend ? 5 : 7
})
const currTime = ref(dayjs())
const overlayStyle = computed(() => {
if (!containerWidth.value)
return {
top: 0,
left: 0,
}
const left = (containerWidth.value / maxVisibleDays.value) * getDayIndex(currTime.value)
const minutes = currTime.value.hour() * 60 + currTime.value.minute()
const top = (52 / 60) * minutes
return {
width: `${containerWidth.value / maxVisibleDays.value}px`,
top: `${top}px`,
left: `${left}px`,
}
})
onMounted(() => {
const intervalId = setInterval(() => {
currTime.value = dayjs()
}, 10000) // 10000 ms = 10 seconds
// Clean up the interval when the component is unmounted
onUnmounted(() => {
clearInterval(intervalId)
})
})
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
@ -85,7 +132,11 @@ const calculateNewDates = useMemoize(
const datesHours = computed(() => {
const datesHours: Array<Array<dayjs.Dayjs>> = []
let startOfWeek = dayjs(selectedDateRange.value.start) ?? dayjs().startOf('week')
const endOfWeek = dayjs(selectedDateRange.value.end) ?? dayjs().endOf('week')
let endOfWeek = dayjs(selectedDateRange.value.end) ?? dayjs().endOf('week')
if (maxVisibleDays.value === 5) {
endOfWeek = endOfWeek.subtract(2, 'day')
}
while (startOfWeek.isSameOrBefore(endOfWeek)) {
const hours: Array<dayjs.Dayjs> = []
@ -107,14 +158,6 @@ const datesHours = computed(() => {
return datesHours
})
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const getGridTime = (date: dayjs.Dayjs, round = false) => {
const gridCalc = date.hour() * 60 + date.minute()
if (round) {
@ -201,8 +244,20 @@ const getMaxOverlaps = ({
let maxOverlaps = 1
if (graph.has(id)) {
maxOverlaps = dfs(id)
dfs(id)
}
const overlapIterations: Array<number> = []
columnArray[dayIndex]
.flat()
.filter((record) => visited.has(record.rowMeta.id!))
.forEach((record) => {
overlapIterations.push(record.rowMeta.overLapIteration!)
})
maxOverlaps = Math.max(...overlapIterations)
return { maxOverlaps, dayIndex, overlapIndex }
}
@ -224,11 +279,15 @@ const recordsAcrossAllRange = computed<{
records: [],
gridTimeMap: new Map(),
}
const perWidth = containerWidth.value / 7
const perWidth = containerWidth.value / maxVisibleDays.value
const perHeight = 52
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
let scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
if (maxVisibleDays.value === 5) {
scheduleEnd = scheduleEnd.subtract(2, 'day')
}
const columnArray: Array<Array<Array<Row>>> = [[[]]]
const gridTimeMap = new Map<
@ -489,17 +548,25 @@ const recordsAcrossAllRange = computed<{
})
const dayIndex = record.rowMeta.dayIndex ?? tDayIndex
let display = 'block'
if (maxVisibleDays.value === 5) {
if (dayIndex === 5 || dayIndex === 6) {
display = 'none'
}
}
record.rowMeta.numberOfOverlaps = maxOverlaps
let width = 0
let left = 100
const majorLeft = dayIndex * perWidth
let display = 'block'
if (record.rowMeta.overLapIteration! - 1 > 2) {
display = 'none'
} else {
width = 100 / Math.min(maxOverlaps, 3) / 7
width = 100 / Math.min(maxOverlaps, 3) / maxVisibleDays.value
left = width * (overlapIndex - 1)
}
record.rowMeta.style = {
@ -563,7 +630,7 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
const hour = Math.floor(percentY * 23)
const minutes = Math.round((percentY * 24 * 60) % 60)
@ -656,7 +723,7 @@ const calculateNewRow = (
if (!fromCol) return { newRow: null, updatedProperty: [] }
const day = Math.max(0, Math.min(6, Math.floor(percentX * 7)))
const day = Math.max(0, Math.min(6, Math.floor(percentX * maxVisibleDays.value)))
const hour = Math.max(0, Math.min(23, Math.floor(percentY * 24)))
const minutes = Math.round(((percentY * 24 * 60) % 60) / 15) * 15
@ -762,13 +829,13 @@ const stopDrag = (event: MouseEvent) => {
}
const dragStart = (event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
if (resizeInProgress.value) return
let target = event.target as HTMLElement
isDragging.value = false
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
@ -877,14 +944,32 @@ watch(
data-testid="nc-calendar-week-view"
@drop="dropEvent"
>
<div
v-if="!isPublic && dayjs().isBetween(selectedDateRange.start, selectedDateRange.end)"
class="absolute ml-16 mt-7 pointer-events-none z-4"
:style="overlayStyle"
>
<div class="flex w-full items-center">
<span
class="text-brand-500 rounded-md text-xs border-1 pointer-events-auto px-0.5 border-brand-200 cursor-pointer bg-brand-50"
@click="addRecord(dayjs())"
>
{{ dayjs().format('hh:mm A') }}
</span>
<div class="flex-1 border-b-1 border-brand-500"></div>
</div>
</div>
<div class="flex sticky h-6 z-1 top-0 pl-16 bg-gray-50 w-full">
<div
v-for="date in datesHours"
:key="date[0].toISOString()"
:class="{
'text-brand-500': date[0].isSame(dayjs(), 'date'),
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="w-1/7 text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
class="text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
>
{{ dayjs(date[0]).format('DD ddd') }}
</div>
@ -899,15 +984,27 @@ watch(
</div>
</div>
<div ref="container" class="absolute ml-16 flex w-[calc(100%-64px)]">
<div v-for="(date, index) in datesHours" :key="index" class="h-full w-1/7 mt-7.1" data-testid="nc-calendar-week-day">
<div
v-for="(date, index) in datesHours"
:key="index"
:class="{
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="h-full mt-5.95"
data-testid="nc-calendar-week-day"
>
<div
v-for="(hour, hourIndex) in date"
:key="hourIndex"
:class="{
'border-1 !border-brand-500 !bg-gray-100':
hour.isSame(selectedTime, 'hour') && (hour.get('day') === 6 || hour.get('day') === 0),
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
'bg-gray-50 hover:bg-gray-100': hour.get('day') === 0 || hour.get('day') === 6,
'hover:bg-gray-50': hour.get('day') !== 0 && hour.get('day') !== 6,
}"
class="text-center relative transition h-13 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 border-l-gray-200"
class="text-center relative transition h-13 text-sm text-gray-500 w-full py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100 border-l-gray-200"
data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click="
@ -934,17 +1031,18 @@ watch(
</div>
</div>
<div
class="absolute pointer-events-none inset-0 overflow-hidden !mt-[29px]"
data-testid="nc-calendar-week-record-container"
>
<div class="absolute pointer-events-none inset-0 overflow-hidden !mt-5.95" data-testid="nc-calendar-week-record-container">
<template v-for="(record, rowIndex) in recordsAcrossAllRange.records" :key="rowIndex">
<div
v-if="record.rowMeta.style?.display !== 'none'"
:data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style "
class="absolute transition draggable-record w-1/7 group cursor-pointer pointer-events-auto"
:class="{
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="absolute transition draggable-record group cursor-pointer pointer-events-auto"
@mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"

2
packages/nc-gui/components/smartsheet/calendar/YearView/index.vue

@ -22,7 +22,7 @@ const handleResize = () => {
if (width.value > 1250) {
size.value = 'medium'
cols.value = 4
} else if (width.value > 850) {
} else if (width.value > 950) {
size.value = 'medium'
cols.value = 3
} else if (width.value > 680) {

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

@ -8,11 +8,13 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const { isMobileMode } = useGlobal()
const reloadViewMetaHook = inject(ReloadViewMetaHookInj)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const { isMobileMode } = useGlobal()
const isPublic = inject(IsPublicInj, ref(false))
provide(IsFormInj, ref(false))
@ -45,14 +47,15 @@ const expandedFormOnRowIdDlg = computed({
get() {
return !!route.query.rowId
},
set(val) {
if (!val)
set(value) {
if (!value) {
router.push({
query: {
...route.query,
rowId: undefined,
},
})
}
},
})
@ -64,7 +67,10 @@ const expandedFormRowState = ref<Record<string, any>>()
const expandRecord = (row: RowType, state?: Record<string, any>) => {
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) {
expandedFormRowState.value = state
if (rowId && !isPublic.value) {
router.push({
query: {
...route.query,
@ -73,12 +79,12 @@ const expandRecord = (row: RowType, state?: Record<string, any>) => {
})
} else {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
}
const newRecord = (row: RowType) => {
if (isPublic.value) return
$e('c:calendar:new-record', activeCalendarView.value)
expandRecord({
row: {
@ -114,77 +120,97 @@ reloadViewDataHook?.on(async (params: void | { shouldShowLoading?: boolean }) =>
</script>
<template>
<div class="flex h-full relative flex-row" data-testid="nc-calendar-wrapper">
<div class="flex flex-col w-full">
<template v-if="calendarRange?.length && !isCalendarMetaLoading">
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" />
<template v-if="!isCalendarDataLoading">
<LazySmartsheetCalendarMonthView
v-if="activeCalendarView === 'month'"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekViewDateField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.Date"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekViewDateTimeField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.DateTime"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarDayViewDateField
v-else-if="activeCalendarView === 'day' && calDataType === UITypes.Date"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarDayViewDateTimeField
v-else-if="activeCalendarView === 'day' && calDataType === UITypes.DateTime"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<template v-if="isMobileMode">
<div class="pl-6 pr-[120px] py-6 bg-white flex-col justify-start items-start gap-2.5 inline-flex">
<div class="text-gray-500 text-5xl font-semibold leading-16">
{{ $t('general.available') }}<br />{{ $t('title.inDesktop') }}
</div>
<div class="text-gray-500 text-base font-medium leading-normal">
{{ $t('msg.calendarViewNotSupportedOnMobile') }}
</div>
</div>
</template>
<template v-else>
<div class="flex h-full relative flex-row" data-testid="nc-calendar-wrapper">
<div class="flex flex-col w-full">
<template v-if="calendarRange?.length && !isCalendarMetaLoading">
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" />
<template v-if="!isCalendarDataLoading">
<LazySmartsheetCalendarMonthView
v-if="activeCalendarView === 'month'"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekViewDateField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.Date"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekViewDateTimeField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.DateTime"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarDayViewDateField
v-else-if="activeCalendarView === 'day' && calDataType === UITypes.Date"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarDayViewDateTimeField
v-else-if="activeCalendarView === 'day' && calDataType === UITypes.DateTime"
@expand-record="expandRecord"
@new-record="newRecord"
/>
</template>
<div
v-if="isCalendarDataLoading && activeCalendarView !== 'year'"
class="flex w-full items-center h-full justify-center"
>
<GeneralLoader size="xlarge" />
</div>
</template>
<div v-if="isCalendarDataLoading && activeCalendarView !== 'year'" class="flex w-full items-center h-full justify-center">
<GeneralLoader size="xlarge" />
</div>
</template>
<template v-else-if="isCalendarMetaLoading">
<div class="flex w-full items-center h-full justify-center">
<GeneralLoader size="xlarge" />
</div>
</template>
<template v-else>
<div class="flex w-full items-center h-full justify-center">
{{ $t('activity.noRange') }}
</div>
</template>
<template v-else-if="isCalendarMetaLoading">
<div class="flex w-full items-center h-full justify-center">
<GeneralLoader size="xlarge" />
</div>
</template>
<template v-else>
<div class="flex w-full items-center h-full justify-center">
{{ $t('activity.noRange') }}
</div>
</template>
</div>
<LazySmartsheetCalendarSideMenu :visible="showSideMenu" @expand-record="expandRecord" @new-record="newRecord" />
</div>
<LazySmartsheetCalendarSideMenu :visible="showSideMenu" @expand-record="expandRecord" @new-record="newRecord" />
</div>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
close-after-save
:meta="meta"
:row="expandedFormRow"
:state="expandedFormRowState"
:view="view"
/>
</Suspense>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
:load-row="!isPublic"
:state="expandedFormRowState"
:meta="meta"
:view="view"
/>
</Suspense>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg && meta?.id"
v-model="expandedFormOnRowIdDlg"
close-after-save
:load-row="!isPublic"
:meta="meta"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:state="expandedFormRowState"
:row="{
row: {},
oldRow: {},
rowMeta: {},
}"
:row-id="route.query.rowId"
:expand-form="expandRecord"
:view="view"
/>
</Suspense>
</template>
</template>

17
packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue

@ -32,7 +32,7 @@ vModel.value.au = !!vModel.value.au */
</script>
<template>
<div class="p-4 border-[0.1px] radius-1 rounded-md border-grey w-full flex flex-col gap-2">
<div class="p-4 border-[0.1px] radius-1 rounded-lg border-grey w-full flex flex-col gap-2">
<template v-if="props.advancedDbOptions">
<div class="flex justify-between w-full gap-1">
<a-form-item label="NN">
@ -72,7 +72,11 @@ vModel.value.au = !!vModel.value.au */
</div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type " class="!mt-0.5" @change="onDataTypeChange">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type" @change="onDataTypeChange">
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700" />
</template>
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
<div class="flex gap-2 items-center justify-between">
{{ type }}
@ -85,19 +89,14 @@ vModel.value.au = !!vModel.value.au */
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input
v-model:value="vModel.dtxp"
class="!rounded-md !mt-0.5"
class="!rounded-lg"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@input="onAlter"
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input
v-model:value="vModel.dtxs"
class="!rounded-md !mt-0.5"
:disabled="!sqlUi.columnEditable(vModel)"
@input="onAlter"
/>
<a-input v-model:value="vModel.dtxs" class="!rounded-lg" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item>
<LazySmartsheetColumnPgBinaryOptions v-if="isPg(meta?.source_id) && vModel.dt === 'bytea'" v-model:value="vModel" />

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

Loading…
Cancel
Save