@conduction/nextcloud-vue 2.56.0-beta.31 → 2.56.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +55 -4
  2. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  3. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +51 -15
  4. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -2
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
  10. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
  11. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
  12. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
  13. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
  14. package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
  15. package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
  16. package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
  17. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
  18. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
  19. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
  20. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
  21. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
  22. package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
  23. package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
  24. package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -2
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -0
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +20 -3
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +7 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +77 -3
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +0 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
  51. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
  52. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
  53. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
  54. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
  55. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
  56. package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
  57. package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
  63. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
  64. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
  65. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
  66. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
  67. package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
  68. package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
  69. package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
  70. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  71. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
  73. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
  74. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
  75. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
  76. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
  77. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
  78. package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
  79. package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
  84. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
  85. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
  86. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
  87. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
  88. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
  89. package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
  90. package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
  91. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
  92. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  93. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
  94. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  95. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
  96. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  97. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
  98. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  99. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
  100. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
  101. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
  102. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
  103. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
  104. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
  105. package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
  106. package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  110. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +4 -2
  111. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  112. package/dist/esm/index.js +7 -0
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/l10n/en.json.js +1 -0
  115. package/dist/esm/l10n/en.json.js.map +1 -1
  116. package/dist/esm/l10n/nl.json.js +1 -0
  117. package/dist/esm/l10n/nl.json.js.map +1 -1
  118. package/dist/esm/nextcloud-vue.css +843 -4
  119. package/dist/esm/utils/contentHeaderLink.js +51 -0
  120. package/dist/esm/utils/contentHeaderLink.js.map +1 -0
  121. package/dist/esm/utils/fetchFilterCounts.js +40 -5
  122. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  123. package/dist/esm/utils/initials.js +32 -0
  124. package/dist/esm/utils/initials.js.map +1 -0
  125. package/dist/esm/utils/libraryWidgetKeys.js +7 -0
  126. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  127. package/dist/esm/utils/objectWrite.js +59 -0
  128. package/dist/esm/utils/objectWrite.js.map +1 -0
  129. package/dist/esm/utils/widgetDispatch.js +1 -1
  130. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  131. package/dist/nextcloud-vue.cjs.js +10510 -6830
  132. package/dist/nextcloud-vue.cjs.js.map +2 -2
  133. package/dist/nextcloud-vue.css +843 -4
  134. package/l10n/en.json +1 -0
  135. package/l10n/nl.json +1 -0
  136. package/package.json +1 -1
  137. package/src/components/CnBannerWidget/CnBannerWidget.vue +93 -13
  138. package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
  139. package/src/components/CnCellRenderer/CnCellRenderer.vue +43 -3
  140. package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
  141. package/src/components/CnChipsFieldWidget/index.js +25 -0
  142. package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
  143. package/src/components/CnChoicePickerWidget/index.js +26 -0
  144. package/src/components/CnDashboardPage/CnDashboardPage.vue +38 -2
  145. package/src/components/CnDataTable/CnDataTable.vue +2 -1
  146. package/src/components/CnDetailPage/CnDetailPage.vue +45 -2
  147. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -3
  148. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +265 -4
  149. package/src/components/CnIndexPage/CnIndexPage.vue +0 -1
  150. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
  151. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
  152. package/src/components/CnRadioCardsWidget/index.js +27 -0
  153. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +43 -1
  154. package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
  155. package/src/components/CnSavedViewsWidget/index.js +29 -0
  156. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
  157. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
  158. package/src/components/CnSettingsFormWidget/index.js +26 -0
  159. package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
  160. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
  161. package/src/components/CnSwitchRowsWidget/index.js +25 -0
  162. package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
  163. package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
  164. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
  165. package/src/components/CnToggleTableWidget/index.js +26 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
  167. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +28 -1
  168. package/src/components/index.js +7 -0
  169. package/src/css/look-board-detail.css +36 -2
  170. package/src/css/look-board-index.css +23 -0
  171. package/src/css/look-board.css +72 -0
  172. package/src/index.js +7 -0
  173. package/src/utils/contentHeaderLink.js +48 -0
  174. package/src/utils/fetchFilterCounts.js +39 -4
  175. package/src/utils/initials.js +29 -0
  176. package/src/utils/libraryWidgetKeys.js +7 -0
  177. package/src/utils/objectWrite.js +55 -0
  178. package/src/utils/widgetDispatch.js +1 -1
@@ -379,7 +379,7 @@
379
379
  :showDocumentation="showDocumentation && menuShowsHelpLinks"
380
380
  :actionsMenuLabel="boardMenuLabel"
381
381
  :variant="isBoard ? 'secondary' : undefined"
382
- :hasPrimaryItems="headerMenuGroups.length > 0 || showMarkUnread"
382
+ :hasPrimaryItems="headerMenuGroups.length > 0 || showMarkUnread || showOpenSidebar"
383
383
  :documentationUrl="documentationUrl"
384
384
  :docsAnchor="resolvedPageId"
385
385
  :documentationLabel="documentationLabel || undefined"
@@ -390,7 +390,7 @@
390
390
  testidBase="cn-detail-page"
391
391
  @refresh="onHeaderRefresh"
392
392
  @requestFeature="onHeaderRequestFeature">
393
- <template v-if="headerMenuGroups.length || showMarkUnread" #primary-items>
393
+ <template v-if="headerMenuGroups.length || showMarkUnread || showOpenSidebar" #primary-items>
394
394
  <!-- One block per group. A page that groups nothing has
395
395
  a single captionless group, and renders exactly the
396
396
  flat list it always did. -->
@@ -449,6 +449,20 @@
449
449
  </template>
450
450
  {{ markUnreadLabel }}
451
451
  </NcActionButton>
452
+ <!-- Board look: the sidebar's own open toggle is hidden (it sat
453
+ over the header's last button, where the board draws none), so
454
+ the menu opens it. -->
455
+ <NcActionButton
456
+ v-if="showOpenSidebar"
457
+ key="open-sidebar"
458
+ data-testid="cn-detail-page-open-sidebar"
459
+ :closeAfterClick="true"
460
+ @click="openSidebar">
461
+ <template #icon>
462
+ <DockRight :size="20" />
463
+ </template>
464
+ {{ openSidebarLabel }}
465
+ </NcActionButton>
452
466
  <!-- Divides the page's own actions from the help links
453
467
  below. With the help links switched off there is
454
468
  nothing below to divide from. -->
@@ -1191,6 +1205,7 @@ import Check from 'vue-material-design-icons/Check.vue'
1191
1205
  import ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'
1192
1206
  import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
1193
1207
  import Cog from 'vue-material-design-icons/Cog.vue'
1208
+ import DockRight from 'vue-material-design-icons/DockRight.vue'
1194
1209
  import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue'
1195
1210
  import FileQuestionOutline from 'vue-material-design-icons/FileQuestionOutline.vue'
1196
1211
  import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
@@ -1440,6 +1455,7 @@ export default {
1440
1455
  CnSummaryAggregates,
1441
1456
  CnDetailHeaderChips,
1442
1457
  CnFollowToggle,
1458
+ DockRight,
1443
1459
  EyeOffOutline,
1444
1460
  CnRelatedCollections,
1445
1461
  CnBodySections,
@@ -3909,6 +3925,22 @@ export default {
3909
3925
  && this.register !== '' && this.schema !== '' && this.objectId !== ''
3910
3926
  },
3911
3927
 
3928
+ /**
3929
+ * Whether the header menu offers "Open sidebar": the board look, where the
3930
+ * sidebar's own toggle is hidden, on a page whose sidebar is active and
3931
+ * closed.
3932
+ *
3933
+ * @return {boolean}
3934
+ */
3935
+ showOpenSidebar() {
3936
+ return this.isBoard && this.hasExternalSidebar && this.sidebarActive && this.objectSidebarState.open !== true
3937
+ },
3938
+
3939
+ /** @return {string} Label of the Open sidebar entry. */
3940
+ openSidebarLabel() {
3941
+ return t('nextcloud-vue', 'Open sidebar')
3942
+ },
3943
+
3912
3944
  /** @return {string} Label of the Mark as unread entry. */
3913
3945
  markUnreadLabel() {
3914
3946
  return t('nextcloud-vue', 'Mark as unread')
@@ -4619,6 +4651,17 @@ export default {
4619
4651
  },
4620
4652
 
4621
4653
  methods: {
4654
+ /**
4655
+ * Open the object sidebar through the shared channel.
4656
+ *
4657
+ * @return {void}
4658
+ */
4659
+ openSidebar() {
4660
+ if (this.objectSidebarState) {
4661
+ this.objectSidebarState.open = true
4662
+ }
4663
+ },
4664
+
4622
4665
  /**
4623
4666
  * Start or finish arranging the chosen view. Finishing stores the
4624
4667
  * arrangement of every view the user moved, once.
@@ -169,7 +169,13 @@ function sharedRefLabels() {
169
169
  return refLabels
170
170
  }
171
171
 
172
- /** Key of a resolved reference in the widget's `labels` data. */
172
+ /**
173
+ * Key of a resolved reference in the widget's `labels` data.
174
+ *
175
+ * @param {{register: string, schema: string, labelField?: string}} resolve The entry's `resolve` config.
176
+ * @param {string} id The referenced object's id.
177
+ * @return {string} The key.
178
+ */
173
179
  function refKey(resolve, id) {
174
180
  return `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`
175
181
  }
@@ -1012,12 +1018,21 @@ export default {
1012
1018
  display: contents;
1013
1019
  }
1014
1020
 
1015
- .cn-facts-widget__label {
1021
+ /* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which
1022
+ pushed the label to the right of its column and added 12px around both. The
1023
+ class is doubled so these resets outrank that rule, and a dossiq-style
1024
+ `:deep()` override in an app is not needed. */
1025
+ .cn-facts-widget__label.cn-facts-widget__label {
1026
+ margin: 0;
1027
+ padding: 0;
1028
+ text-align: start;
1016
1029
  color: var(--color-text-maxcontrast);
1017
1030
  }
1018
1031
 
1019
- .cn-facts-widget__value {
1032
+ .cn-facts-widget__value.cn-facts-widget__value {
1020
1033
  margin: 0;
1034
+ padding: 0;
1035
+ text-align: start;
1021
1036
  font-weight: 500;
1022
1037
  min-width: 0;
1023
1038
  overflow-wrap: anywhere;
@@ -5,9 +5,123 @@
5
5
 
6
6
  <template>
7
7
  <div class="cn-field-list-widget" data-testid="cn-field-list-widget">
8
+ <div v-if="canAdd && addTrigger && addPlacement === 'header'" class="cn-field-list-widget__toolbar">
9
+ <NcButton
10
+ variant="secondary"
11
+ :disabled="busy !== ''"
12
+ data-testid="cn-field-list-open-add"
13
+ @click="adding = !adding">
14
+ + {{ addLabel }}
15
+ </NcButton>
16
+ </div>
17
+ <form
18
+ v-if="canAdd && addTrigger && addPlacement === 'header' && adding"
19
+ class="cn-field-list-widget__add"
20
+ data-testid="cn-field-list-add"
21
+ @submit.prevent="add">
22
+ <div
23
+ v-for="field in addFields"
24
+ :key="field.key"
25
+ class="cn-field-list-widget__field"
26
+ :data-testid="`cn-field-list-input-${field.key}`">
27
+ <NcSelect
28
+ v-if="field.type === 'select'"
29
+ v-model="draft[field.key]"
30
+ :inputLabel="field.label"
31
+ :options="field.options"
32
+ :reduce="(option) => option.value"
33
+ :clearable="false"
34
+ label="label" />
35
+ <NcTextField
36
+ v-else
37
+ v-model="draft[field.key]"
38
+ :label="field.label"
39
+ :error="Boolean(errors[field.key])"
40
+ :helperText="errors[field.key] || ''" />
41
+ </div>
42
+ <NcButton type="submit"
43
+ variant="primary"
44
+ :disabled="busy !== ''"
45
+ data-testid="cn-field-list-submit">
46
+ {{ addLabel }}
47
+ </NcButton>
48
+ </form>
8
49
  <p v-if="rows.length === 0" class="cn-field-list-widget__empty" data-testid="cn-field-list-empty">
9
50
  {{ emptyLabel }}
10
51
  </p>
52
+ <div v-else-if="isTable" class="cn-field-list-widget__table-wrap">
53
+ <table class="cn-field-list-widget__table" data-testid="cn-field-list-table">
54
+ <thead>
55
+ <tr>
56
+ <th
57
+ v-for="column in tableColumns"
58
+ :key="column.key"
59
+ scope="col"
60
+ class="cn-field-list-widget__th">
61
+ {{ column.header }}
62
+ </th>
63
+ <th v-if="hasMenu" scope="col" class="cn-field-list-widget__th cn-field-list-widget__th--menu">
64
+ <span class="cn-field-list-widget__sr">{{ tr('Actions') }}</span>
65
+ </th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ <tr
70
+ v-for="row in rows"
71
+ :key="row.key"
72
+ class="cn-field-list-widget__tr"
73
+ :data-testid="`cn-field-list-row-${row.key}`">
74
+ <td
75
+ v-for="(column, index) in tableColumns"
76
+ :key="column.key"
77
+ class="cn-field-list-widget__td">
78
+ <CnStatusBadge
79
+ v-if="column.pill && row.cells[index].pill"
80
+ :label="row.cells[index].pill.label"
81
+ :variant="row.cells[index].pill.variant"
82
+ size="small" />
83
+ <strong v-else-if="column.strong">{{ row.cells[index].text }}</strong>
84
+ <template v-else>
85
+ {{ row.cells[index].text }}
86
+ </template>
87
+ </td>
88
+ <td v-if="hasMenu" class="cn-field-list-widget__td cn-field-list-widget__td--menu">
89
+ <NcActions
90
+ v-if="menuItems(row).length > 0"
91
+ :forceMenu="true"
92
+ :disabled="busy !== ''"
93
+ :ariaLabel="`${tr('More actions')}: ${row.title}`"
94
+ :data-testid="`cn-field-list-menu-${row.key}`">
95
+ <NcActionButton
96
+ v-for="item in menuItems(row)"
97
+ :key="item.id"
98
+ :data-testid="`cn-field-list-menu-${item.id}-${row.key}`"
99
+ @click="item.run">
100
+ {{ item.label }}
101
+ </NcActionButton>
102
+ </NcActions>
103
+ </td>
104
+ </tr>
105
+ </tbody>
106
+ </table>
107
+ <div
108
+ v-for="row in bandRows"
109
+ :key="row.key"
110
+ class="cn-field-list-widget__band"
111
+ role="note"
112
+ :data-testid="`cn-field-list-band-${row.key}`">
113
+ <span class="cn-field-list-widget__band-text">{{ row.detail }}</span>
114
+ <NcButton
115
+ v-for="action in row.bandActions"
116
+ :key="action.id"
117
+ variant="secondary"
118
+ :disabled="busy !== ''"
119
+ :data-testid="`cn-field-list-band-action-${action.id}-${row.key}`"
120
+ @click="runAction(action, row)">
121
+ {{ action.label }}
122
+ </NcButton>
123
+ </div>
124
+ </div>
11
125
  <ul v-else class="cn-field-list-widget__rows">
12
126
  <li
13
127
  v-for="row in rows"
@@ -55,8 +169,17 @@
55
169
  {{ hint }}
56
170
  </p>
57
171
 
172
+ <div v-if="canAdd && addTrigger && addPlacement !== 'header' && !adding" class="cn-field-list-widget__toolbar">
173
+ <NcButton
174
+ variant="secondary"
175
+ :disabled="busy !== ''"
176
+ data-testid="cn-field-list-open-add"
177
+ @click="adding = true">
178
+ + {{ addLabel }}
179
+ </NcButton>
180
+ </div>
58
181
  <form
59
- v-if="canAdd"
182
+ v-if="canAdd && !(addTrigger && addPlacement === 'header') && (!addTrigger || adding)"
60
183
  class="cn-field-list-widget__add"
61
184
  data-testid="cn-field-list-add"
62
185
  @submit.prevent="add">
@@ -95,7 +218,7 @@
95
218
  </template>
96
219
 
97
220
  <script>
98
- import { NcButton, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
221
+ import { NcActionButton, NcActions, NcButton, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
99
222
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
100
223
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
101
224
  import { formatBoardDate } from '../../utils/boardDate.js'
@@ -125,7 +248,7 @@ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
125
248
  export default {
126
249
  name: 'CnFieldListWidget',
127
250
 
128
- components: { CnStatusBadge, NcButton, NcNoteCard, NcSelect, NcTextField },
251
+ components: { CnStatusBadge, NcActionButton, NcActions, NcButton, NcNoteCard, NcSelect, NcTextField },
129
252
 
130
253
  inject: {
131
254
  cnTranslate: { default: () => (key) => key },
@@ -140,8 +263,10 @@ export default {
140
263
  * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when? } }`.
141
264
  * - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.
142
265
  * - `remove` — `{ label? }`; omitted = no remove.
143
- * - `rowActions` — `[{ id, label, when?, request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.
266
+ * - `rowActions` — `[{ id, label, when?, placement?: 'band', request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.
144
267
  * - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.
268
+ * - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap? }]`: rows as a table; a per-row "..." menu holds the row actions and remove; `detail` lines become a warning band under the table, with the row actions that have `placement: 'band'`.
269
+ * - `add.trigger: 'button'`, `add.placement: 'header' | 'footer'` — the add form opens from a button.
145
270
  * - `readOnly`, `hint`, `emptyText`.
146
271
  *
147
272
  * @type {object}
@@ -185,6 +310,8 @@ export default {
185
310
  /** Live per-row state from `rowSource`, by key. */
186
311
  live: {},
187
312
  draft: {},
313
+ /** Whether the add form is open (only with `add.trigger: 'button'`). */
314
+ adding: false,
188
315
  errors: {},
189
316
  busy: '',
190
317
  notice: '',
@@ -232,6 +359,7 @@ export default {
232
359
  aside: spec.aside ? this.asideOf(entry, spec.aside) : '',
233
360
  detail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',
234
361
  actions: this.actionsFor(entry),
362
+ cells: this.isTable ? this.cellsOf(entry) : [],
235
363
  }
236
364
  })
237
365
  },
@@ -269,6 +397,44 @@ export default {
269
397
  }))
270
398
  },
271
399
 
400
+ /** Whether the rows are drawn as a table (`variant: 'table'`). */
401
+ isTable() {
402
+ return this.content.variant === 'table' && this.tableColumns.length > 0
403
+ },
404
+
405
+ /**
406
+ * The table columns, headers translated.
407
+ *
408
+ * @return {Array<object>}
409
+ */
410
+ tableColumns() {
411
+ return (Array.isArray(this.content.columns) ? this.content.columns : [])
412
+ .filter((column) => column && column.path)
413
+ .map((column, index) => ({ ...column, key: `${column.path}-${index}`, header: this.tr(column.header || '') }))
414
+ },
415
+
416
+ /** Whether each row has a "more actions" menu (row actions not placed in the band, and remove). */
417
+ hasMenu() {
418
+ return this.isTable && !this.readOnly && (this.canRemove || this.rows.some((row) => this.menuItems(row).length > 0))
419
+ },
420
+
421
+ /** Rows with a detail line: in the table variant it is a warning band under the table. */
422
+ bandRows() {
423
+ return this.rows
424
+ .filter((row) => row.detail !== '')
425
+ .map((row) => ({ ...row, bandActions: row.actions.filter((action) => action.placement === 'band') }))
426
+ },
427
+
428
+ /** Whether the add form opens from a button (`add.trigger: 'button'`). */
429
+ addTrigger() {
430
+ return Boolean(this.content.add && this.content.add.trigger === 'button')
431
+ },
432
+
433
+ /** Where the add button sits: `header` (above the rows) or `footer` (default). */
434
+ addPlacement() {
435
+ return this.content.add && this.content.add.placement === 'header' ? 'header' : 'footer'
436
+ },
437
+
272
438
  addLabel() {
273
439
  return this.tr((this.content.add && this.content.add.label) || 'Add')
274
440
  },
@@ -315,6 +481,36 @@ export default {
315
481
  return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
316
482
  },
317
483
 
484
+ /**
485
+ * The cells of one table row, one per column.
486
+ *
487
+ * @param {object} entry The entry.
488
+ * @return {Array<{text: string, pill: object|null}>}
489
+ */
490
+ cellsOf(entry) {
491
+ return this.tableColumns.map((column) => ({
492
+ text: this.text(entry, column),
493
+ pill: column.pill ? this.pillOf(entry, column) : null,
494
+ }))
495
+ },
496
+
497
+ /**
498
+ * The items of a row's menu in the table variant: the row actions not
499
+ * placed in the band, then remove.
500
+ *
501
+ * @param {object} row The row.
502
+ * @return {Array<{id: string, label: string, run: Function}>}
503
+ */
504
+ menuItems(row) {
505
+ const items = row.actions
506
+ .filter((action) => action.placement !== 'band')
507
+ .map((action) => ({ id: action.id, label: action.label, run: () => this.runAction(action, row) }))
508
+ if (this.canRemove) {
509
+ items.push({ id: 'remove', label: this.removeLabel, run: () => this.remove(row) })
510
+ }
511
+ return items
512
+ },
513
+
318
514
  /**
319
515
  * The identity of an entry.
320
516
  *
@@ -599,6 +795,7 @@ export default {
599
795
  }, '*')
600
796
  if (saved) {
601
797
  this.resetDraft()
798
+ this.adding = false
602
799
  this.loadLive()
603
800
  }
604
801
  },
@@ -744,4 +941,68 @@ export default {
744
941
  .cn-field-list-widget__add > button {
745
942
  flex: 0 0 auto;
746
943
  }
944
+
945
+ .cn-field-list-widget__toolbar {
946
+ display: flex;
947
+ justify-content: flex-end;
948
+ }
949
+
950
+ .cn-field-list-widget__table-wrap {
951
+ display: flex;
952
+ flex-direction: column;
953
+ gap: 12px;
954
+ overflow-x: auto;
955
+ }
956
+
957
+ .cn-field-list-widget__table {
958
+ width: 100%;
959
+ border-collapse: collapse;
960
+ }
961
+
962
+ .cn-field-list-widget__th {
963
+ padding: 8px 12px;
964
+ text-align: start;
965
+ font-size: 13px;
966
+ font-weight: 600;
967
+ white-space: nowrap;
968
+ color: var(--color-text-maxcontrast);
969
+ background: var(--color-background-hover);
970
+ }
971
+
972
+ .cn-field-list-widget__th--menu,
973
+ .cn-field-list-widget__td--menu {
974
+ width: 1%;
975
+ text-align: end;
976
+ }
977
+
978
+ .cn-field-list-widget__tr {
979
+ border-top: 1px solid var(--cn-board-hairline, var(--color-border));
980
+ }
981
+
982
+ .cn-field-list-widget__td {
983
+ padding: 10px 12px;
984
+ vertical-align: middle;
985
+ overflow-wrap: anywhere;
986
+ }
987
+
988
+ .cn-field-list-widget__sr {
989
+ position: absolute;
990
+ width: 1px;
991
+ height: 1px;
992
+ overflow: hidden;
993
+ clip-path: inset(50%);
994
+ }
995
+
996
+ .cn-field-list-widget__band {
997
+ display: flex;
998
+ flex-wrap: wrap;
999
+ align-items: center;
1000
+ justify-content: space-between;
1001
+ gap: 8px 12px;
1002
+ padding: 12px 14px;
1003
+ border: 1px solid var(--color-warning);
1004
+ border-radius: 10px;
1005
+ background: color-mix(in srgb, var(--color-warning) 14%, var(--color-main-background));
1006
+ font-size: 14px;
1007
+ }
747
1008
  </style>
@@ -1483,7 +1483,6 @@ export default {
1483
1483
  default: true,
1484
1484
  },
1485
1485
 
1486
- // eslint-disable-next-line vue/no-unused-properties
1487
1486
  /**
1488
1487
  * Draw this page in the `board` look (or `nextcloud`) whatever the app
1489
1488
  * does. Empty follows the `cnLook` CnAppRoot provides. Manifest key
@@ -151,6 +151,8 @@
151
151
  :sortKey="localSort.field || null"
152
152
  :sortOrder="localSort.dir || 'asc'"
153
153
  borderless
154
+ :fitWidth="fitWidthActive"
155
+ :fixedLayout="fixedLayoutActive"
154
156
  :hideHeader="content.hideHeader === true"
155
157
  @rowClick="onRowClick"
156
158
  @rowAuxClick="onRowAuxClick"
@@ -173,6 +175,8 @@
173
175
  :sortKey="localSort.field || null"
174
176
  :sortOrder="localSort.dir || 'asc'"
175
177
  borderless
178
+ :fitWidth="fitWidthActive"
179
+ :fixedLayout="fixedLayoutActive"
176
180
  :hideHeader="content.hideHeader === true"
177
181
  @rowClick="onRowClick"
178
182
  @rowAuxClick="onRowAuxClick"
@@ -448,6 +452,15 @@ export default {
448
452
  page: 1,
449
453
  /** Rows that fit the host cell; null = unconstrained (dashboards). */
450
454
  fitRows: null,
455
+ /**
456
+ * How far the table has been squeezed to the widget's width, so the
457
+ * card never scrolls sideways: 0 = its own auto layout, 1 = one column
458
+ * takes what is left and is cut with an ellipsis while the rest keep
459
+ * their content on one line, 2 = a fixed layout, every column an equal
460
+ * share with its text wrapped. Each step is taken only while the table
461
+ * still scrolls sideways. `content.fitWidth` forces stage 1 on or off.
462
+ */
463
+ fitStage: 0,
451
464
  /** Whether the create dialog is open. */
452
465
  showCreate: false,
453
466
  /**
@@ -480,6 +493,27 @@ export default {
480
493
  },
481
494
 
482
495
  computed: {
496
+ /**
497
+ * Whether the table fits the widget's width (stage 1): `content.fitWidth`
498
+ * when it says, else once the columns were measured not to fit.
499
+ *
500
+ * @return {boolean}
501
+ */
502
+ fitWidthActive() {
503
+ const forced = this.content?.fitWidth
504
+ return typeof forced === 'boolean' ? forced : this.fitStage === 1
505
+ },
506
+
507
+ /**
508
+ * Whether the table takes a fixed layout, the last resort when even the
509
+ * fitted table is wider than the widget (not with `content.fitWidth` set).
510
+ *
511
+ * @return {boolean}
512
+ */
513
+ fixedLayoutActive() {
514
+ return typeof this.content?.fitWidth !== 'boolean' && this.fitStage >= 2
515
+ },
516
+
483
517
  /**
484
518
  * The unwrapped detail-page object context for token resolution, or null
485
519
  * on surfaces (dashboards) that don't provide one.
@@ -619,7 +653,7 @@ export default {
619
653
  // `secondary` (a second, muted line under the cell) rides
620
654
  // along too: the table draws it, and dropping it here took the
621
655
  // list rows' second line away (zuiddrecht-pixel-gaps-3).
622
- for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap']) {
656
+ for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow']) {
623
657
  if (c[k] !== undefined) {
624
658
  out[k] = c[k]
625
659
  }
@@ -1165,6 +1199,12 @@ export default {
1165
1199
  mounted() {
1166
1200
  this.fetchRows()
1167
1201
  this.loadHeadingsIfNeeded()
1202
+ // A table wider than the widget fits its columns to the width instead of
1203
+ // scrolling sideways (no inner scrollbars).
1204
+ if (typeof ResizeObserver !== 'undefined') {
1205
+ this._widthObserver = new ResizeObserver(() => this.measureWidthFit())
1206
+ this._widthObserver.observe(this.$el)
1207
+ }
1168
1208
  // Re-read on a page-level refresh. This list fetches its own rows from
1169
1209
  // OpenRegister and subscribed to nothing, so after a write elsewhere on
1170
1210
  // the page it went on rendering the result set it fetched on mount —
@@ -1203,6 +1243,9 @@ export default {
1203
1243
  if (this._fitObserver) {
1204
1244
  this._fitObserver.disconnect()
1205
1245
  }
1246
+ if (this._widthObserver) {
1247
+ this._widthObserver.disconnect()
1248
+ }
1206
1249
  },
1207
1250
 
1208
1251
  methods: {
@@ -1277,7 +1320,10 @@ export default {
1277
1320
  const res = await axios.get(url, { params })
1278
1321
  this.rows = (res && res.data && res.data.results) || []
1279
1322
  this.total = (res && res.data && typeof res.data.total === 'number') ? res.data.total : this.rows.length
1280
- this.$nextTick(() => this.measureFit())
1323
+ this.$nextTick(() => {
1324
+ this.measureFit()
1325
+ this.measureWidthFit()
1326
+ })
1281
1327
  } catch (e) {
1282
1328
  // Keep the raw message OUT of the template — log it for
1283
1329
  // debugging, surface only the quiet `loadErrorLabel` line.
@@ -1291,6 +1337,27 @@ export default {
1291
1337
  }
1292
1338
  },
1293
1339
 
1340
+ /**
1341
+ * Squeeze the table to the widget's width while it scrolls sideways: its
1342
+ * `.cn-data-table__scroll` wrapper is wider than it is visible. First the
1343
+ * fit (stage 1), then, if that is still too wide, a fixed layout
1344
+ * (stage 2). It never goes back (a squeezed table does not overflow, so
1345
+ * there is nothing to measure to relax it). No-op when `content.fitWidth`
1346
+ * is set.
1347
+ *
1348
+ * @return {void}
1349
+ */
1350
+ measureWidthFit() {
1351
+ if (this.fitStage >= 2 || typeof this.content?.fitWidth === 'boolean') {
1352
+ return
1353
+ }
1354
+ const scroller = this.$el && this.$el.querySelector ? this.$el.querySelector('.cn-data-table__scroll') : null
1355
+ if (scroller && scroller.scrollWidth > scroller.clientWidth + 1) {
1356
+ this.fitStage += 1
1357
+ this.$nextTick(() => this.measureWidthFit())
1358
+ }
1359
+ },
1360
+
1294
1361
  /**
1295
1362
  * Fit the visible row count to the host grid cell (ADR-062 — the cell
1296
1363
  * is the budget). Measures the cell's remaining height below the table