@conduction/nextcloud-vue 2.105.0 → 2.106.0

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 (153) hide show
  1. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  2. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +18 -0
  3. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -1
  5. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +18 -0
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +1 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +53 -4
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +9 -0
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js.map +1 -0
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +24 -0
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -0
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +49 -0
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -0
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +36 -18
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +17 -38
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +53 -2
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +119 -111
  32. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  34. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +62 -1
  35. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +159 -50
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +121 -98
  40. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  42. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +24 -0
  43. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +20 -4
  45. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -4
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +112 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +100 -44
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  53. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +67 -4
  54. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  56. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +295 -12
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +267 -144
  61. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +4 -0
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +3 -1
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +93 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +322 -246
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +122 -7
  74. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +189 -93
  76. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  78. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  81. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +42 -0
  82. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +38 -19
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  85. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js +9 -0
  86. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js.map +1 -0
  87. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js +258 -0
  88. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js.map +1 -0
  89. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js +69 -0
  90. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js.map +1 -0
  91. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +21 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +21 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +803 -453
  101. package/dist/esm/utils/dueText.js +18 -3
  102. package/dist/esm/utils/dueText.js.map +1 -1
  103. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  104. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  105. package/dist/esm/utils/sortOrder.js +11 -1
  106. package/dist/esm/utils/sortOrder.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +8281 -6350
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +803 -453
  112. package/l10n/en.json +21 -1
  113. package/l10n/nl.json +21 -1
  114. package/package.json +1 -1
  115. package/src/components/CnCardGrid/CnCardGrid.md +2 -0
  116. package/src/components/CnCardGrid/CnCardGrid.vue +20 -0
  117. package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -1
  118. package/src/components/CnDetailPage/CnDetailPage.md +2 -2
  119. package/src/components/CnDetailPage/CnDetailPage.vue +53 -4
  120. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +60 -0
  121. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +36 -29
  122. package/src/components/CnFactsWidget/CnFactsWidget.vue +4 -0
  123. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +70 -4
  124. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +62 -1
  125. package/src/components/CnIndexPage/CnIndexPage.md +7 -5
  126. package/src/components/CnIndexPage/CnIndexPage.vue +187 -58
  127. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +36 -1
  128. package/src/components/CnIndexPage/useSelfFetchList.js +22 -4
  129. package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
  130. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +148 -1
  131. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +68 -5
  132. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  133. package/src/components/CnObjectCard/CnObjectCard.vue +354 -13
  134. package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -0
  135. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +190 -4
  136. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +209 -9
  137. package/src/components/CnTabs/CnTabs.vue +11 -0
  138. package/src/components/CnTabsWidget/CnTabsWidget.vue +91 -18
  139. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.md +11 -0
  140. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue +382 -0
  141. package/src/components/CnTeamWorkloadWidget/index.js +14 -0
  142. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  143. package/src/components/index.js +1 -0
  144. package/src/css/index-page.css +14 -2
  145. package/src/css/look-board-detail.css +48 -4
  146. package/src/css/look-board-index.css +64 -2
  147. package/src/css/look-board.css +25 -2
  148. package/src/index.js +1 -1
  149. package/src/schemas/app-manifest-v2.schema.json +75 -6
  150. package/src/utils/dueText.js +18 -3
  151. package/src/utils/libraryWidgetKeys.js +1 -0
  152. package/src/utils/sortOrder.js +10 -0
  153. package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json CHANGED
@@ -1727,7 +1727,23 @@
1727
1727
  "On": "On",
1728
1728
  "Select a step on the canvas to see its settings.": "Select a step on the canvas to see its settings.",
1729
1729
  "Show runs": "Show runs",
1730
- "Back to the step settings": "Back to the step settings"
1730
+ "Back to the step settings": "Back to the step settings",
1731
+ "Sort": "Sort",
1732
+ "Direction": "Direction",
1733
+ "Ascending": "Ascending",
1734
+ "Descending": "Descending",
1735
+ "ascending": "ascending",
1736
+ "descending": "descending",
1737
+ "Not sorted": "Not sorted",
1738
+ "Current order: {order}": "Current order: {order}",
1739
+ "Soon": "Soon",
1740
+ "{name} (you)": "{name} (you)",
1741
+ "{present} of {total} present": "{present} of {total} present",
1742
+ "Absent today": "Absent today",
1743
+ "{message} until {time}": "{message} until {time}",
1744
+ "Away": "Away",
1745
+ "Busy": "Busy",
1746
+ "No one in the team": "No one in the team"
1731
1747
  },
1732
1748
  "plurals": {
1733
1749
  "{count} member": [
@@ -1845,6 +1861,10 @@
1845
1861
  "downloaded %n time": [
1846
1862
  "downloaded %n time",
1847
1863
  "downloaded %n times"
1864
+ ],
1865
+ "{count} open": [
1866
+ "{count} open",
1867
+ "{count} open"
1848
1868
  ]
1849
1869
  }
1850
1870
  }
package/l10n/nl.json CHANGED
@@ -1940,7 +1940,23 @@
1940
1940
  "On": "Aan",
1941
1941
  "Select a step on the canvas to see its settings.": "Kies een stap op het canvas om de instellingen te zien.",
1942
1942
  "Show runs": "Uitvoeringen tonen",
1943
- "Back to the step settings": "Terug naar de stapinstellingen"
1943
+ "Back to the step settings": "Terug naar de stapinstellingen",
1944
+ "Sort": "Sorteren",
1945
+ "Direction": "Richting",
1946
+ "Ascending": "Oplopend",
1947
+ "Descending": "Aflopend",
1948
+ "ascending": "oplopend",
1949
+ "descending": "aflopend",
1950
+ "Not sorted": "Niet gesorteerd",
1951
+ "Current order: {order}": "Huidige volgorde: {order}",
1952
+ "Soon": "Bijna",
1953
+ "{name} (you)": "{name} (u)",
1954
+ "{present} of {total} present": "{present} van {total} aanwezig",
1955
+ "Absent today": "Vandaag afwezig",
1956
+ "{message} until {time}": "{message} tot {time}",
1957
+ "Away": "Afwezig",
1958
+ "Busy": "Bezet",
1959
+ "No one in the team": "Niemand in het team"
1944
1960
  },
1945
1961
  "plurals": {
1946
1962
  "{count} member": [
@@ -2058,6 +2074,10 @@
2058
2074
  "downloaded %n time": [
2059
2075
  "%n keer gedownload",
2060
2076
  "%n keer gedownload"
2077
+ ],
2078
+ "{count} open": [
2079
+ "{count} open",
2080
+ "{count} open"
2061
2081
  ]
2062
2082
  }
2063
2083
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.105.0",
3
+ "version": "2.106.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -113,6 +113,8 @@ Custom card slot — replace the default CnObjectCard with your own rendering:
113
113
  | Prop | Type | Default | Description |
114
114
  |---|---|---|---|
115
115
  | `cardFields` | Array | `null` | Property keys each card's facts list shows |
116
+ | `cardFieldColumns` | Object | `null` | The page's columns by key, passed to each CnObjectCard (see CnObjectCard `cardFieldColumns`) |
117
+ | `cardLayout` | Object | `null` | Board look: the record layout each card draws, passed to CnObjectCard `layout` |
116
118
  | `statusOf` | Function | `null` | `(object) => status` for each card's status pill |
117
119
  | `leadingOf` | Function | `null` | `(object) => { initials | icon }` for each card's leading element |
118
120
  | `footerActionOf` | Function | `null` | `(object) => { label, meta? }` for each card's footer action |
@@ -37,6 +37,8 @@
37
37
  :clickToView="clickToView"
38
38
  :selected="isSelected(object)"
39
39
  :cardFields="cardFields"
40
+ :layout="cardLayout"
41
+ :cardFieldColumns="cardFieldColumns"
40
42
  :status="statusOf ? statusOf(object) : null"
41
43
  :leading="leadingOf ? leadingOf(object) : null"
42
44
  :footerAction="footerActionOf ? footerActionOf(object) : null"
@@ -165,6 +167,24 @@ export default {
165
167
  default: null,
166
168
  },
167
169
 
170
+ /**
171
+ * Board look: the record layout each card draws (manifest key
172
+ * `config.cardLayout`), passed through to CnObjectCard `layout`.
173
+ *
174
+ * @type {object}
175
+ * @spec openspec/changes/board-record-card-layout/specs/board-record-card-layout/spec.md#requirement-a-board-card-can-draw-the-record-layout-from-config
176
+ */
177
+ cardLayout: {
178
+ type: Object,
179
+ default: null,
180
+ },
181
+
182
+ /** The page's columns by key, passed through to CnObjectCard. */
183
+ cardFieldColumns: {
184
+ type: Object,
185
+ default: null,
186
+ },
187
+
168
188
  /** Board look: `(object) => status` for each card's status pill. See CnObjectCard `status`. */
169
189
  statusOf: {
170
190
  type: Function,
@@ -194,7 +194,7 @@
194
194
  <time
195
195
  v-if="dateTimestamp"
196
196
  class="cn-cell-renderer__date"
197
- :class="dueResult.tone === 'danger' ? 'cn-cell-renderer__date--error' : null"
197
+ :class="dueClass"
198
198
  :datetime="dateTimestamp.toISOString()"
199
199
  :title="absoluteDateLabel"
200
200
  data-testid="cn-cell-renderer-due">{{ dueResult.text }}</time>
@@ -780,6 +780,24 @@ export default {
780
780
  return Number.isFinite(size) && size > 0 ? size : (this.isBoardLook ? 32 : 24)
781
781
  },
782
782
 
783
+ /**
784
+ * `date` widget with `widgetProps.due`: the colour class. The
785
+ * `variantWhen` rules win when they match; else an overdue date reads
786
+ * as an error and a soon one (`due.soonWithin`) as a warning.
787
+ *
788
+ * @return {string|null} The class, or null for the plain colour.
789
+ * @spec openspec/changes/due-text-soon/specs/due-text-soon/spec.md#requirement-a-due-date-within-a-few-days-reads-as-soon
790
+ */
791
+ dueClass() {
792
+ if (this.dateVariant) {
793
+ return 'cn-cell-renderer__date--' + this.dateVariant
794
+ }
795
+ if (this.dueResult && this.dueResult.tone === 'danger') {
796
+ return 'cn-cell-renderer__date--error'
797
+ }
798
+ return this.dueResult && this.dueResult.soon ? 'cn-cell-renderer__date--warning' : null
799
+ },
800
+
783
801
  /**
784
802
  * `date` widget: the variant of the first `widgetProps.variantWhen`
785
803
  * rule the date matches, or '' for none.
@@ -421,11 +421,11 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
421
421
  | `stageField` | String | `'status'` | Dot-path to the record's stage. |
422
422
  | `quickActions` | Array | `[]` | At most three always visible header buttons. |
423
423
  | `actionsMenu` | Object \| null | `null` | `{ showRefresh?, showHelpLinks?, label? }` for the overflow menu. |
424
- | `nextStep` | Object \| null | `null` | The "what now" card per stage, rendered by `CnNextStepCard`. |
424
+ | `nextStep` | Object \| null | `null` | The "what now" card per stage, rendered by `CnNextStepCard`. `stagesFrom` (a field path such as `@context.caseType.nextStep`) reads the stages from the record when it holds them. |
425
425
  | `typePill` | Object \| null | `null` | `{ field, colorMap?, labels?, variant? }`, a pill above the title. Instead of `field`: `text` (a constant) or `countFrom` + `countLabel` (a count of a related list). |
426
426
  | `statusPill` | Object \| null | `null` | A second pill, same shape. |
427
427
  | `headerFields` | Array | `[]` | Fields shown as chips under the title (manifest `config.headerFields`). Each entry is a property key, or `{ key, format, labelField, colorField, warnWhenPast }`; `format` is `text` (default), `mono`, `badge`, `user` or `date`. A `$ref` value shows `labelField` of the referenced object and falls back to the raw id in mono; a badge maps `colorField` (`success`, `warning`, `error`, `info`, `neutral`) to a variant; `warnWhenPast` gives a past date the error variant. An empty value shows no chip, a page without the key shows no row. Each chip reads "Title: value" to a screen reader, and the row prints as plain text. |
428
- | `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. |
428
+ | `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. An entry with `tabs` shows only while one of those body tabs is open. |
429
429
  | `sideColumnFrom` | String | `''` | A field path, read like a `visibleWhen` field (e.g. `@context.caseType.sideCards`), whose list of ids picks the `sideColumn` entries this record shows and their order. Any other value keeps `sideColumn` as declared. |
430
430
  | `tabsFrom` | String | `''` | A field path, read like a `visibleWhen` field, whose list of tab ids picks the tabs of the page's `tabs` widgets and their order for this record: `@context.caseType.tabs` reads the list a case type keeps (DqAanbesteding: Inschrijvingen, Publicaties, Bestanden, Historie). A tab's id is its `id`, else its `widgetId`, or the string entry. An empty or missing list keeps the declared tabs; edit mode shows them all. |
431
431
  | `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
@@ -1540,6 +1540,12 @@ export default {
1540
1540
  },
1541
1541
  },
1542
1542
 
1543
+ // The open body tab, reported by a `tabs` widget, so a side entry
1544
+ // with `tabs` shows only beside those tabs.
1545
+ cnDetailReportTab: (id) => {
1546
+ this.activeBodyTab = typeof id === 'string' ? id : ''
1547
+ },
1548
+
1543
1549
  // The records `visibilityContext` names, keyed `@context.<name>`, so a tab's
1544
1550
  // `visibleWhen` reads the case type as a grid widget's does. Null
1545
1551
  // while they are on their way.
@@ -2462,8 +2468,12 @@ export default {
2462
2468
 
2463
2469
  /**
2464
2470
  * The "what now" card (manifest `config.nextStep`):
2465
- * `{ field?, stages: { <stage>: { title?, checklist, after? } } }`.
2471
+ * `{ field?, stagesFrom?, stages: { <stage>: { title?, checklist, after? } } }`.
2466
2472
  * Each checklist item is `{ label, doneField?, doneWhen?, hint? }`.
2473
+ * `stagesFrom` is a field path read like a `visibleWhen` field
2474
+ * (`@context.caseType.nextStep`): when it holds a stages map (or an
2475
+ * object with `stages`), that map replaces `stages` for this record, so
2476
+ * a case type keeps its own stage texts. Otherwise `stages` stays.
2467
2477
  * Rendered above the body for a stage that declares a checklist, with
2468
2478
  * the primary button inside it. See `CnNextStepCard`.
2469
2479
  *
@@ -2589,7 +2599,9 @@ export default {
2589
2599
  * entry is a widget definition (`{ id?, type, title?, content? }`) or
2590
2600
  * the id of a widget declared in `widgets`. The column stacks under
2591
2601
  * the body on a narrow page. Empty (the default) leaves the layout as
2592
- * it is.
2602
+ * it is. An entry with `tabs` (tab ids of the body's `tabs` widget)
2603
+ * shows only while one of those tabs is open (DqZaaktypeVelden: the
2604
+ * option list beside Velden).
2593
2605
  *
2594
2606
  * @type {Array<string|object>}
2595
2607
  */
@@ -2902,6 +2914,8 @@ export default {
2902
2914
 
2903
2915
  data() {
2904
2916
  return {
2917
+ /** The id of the open body tab a `tabs` widget reported, '' before one did. */
2918
+ activeBodyTab: '',
2905
2919
  /**
2906
2920
  * The records `visibilityContext` names, keyed `@context.<name>`: the row,
2907
2921
  * `null` when it could not be read, absent while it is on its way.
@@ -3402,6 +3416,25 @@ export default {
3402
3416
  return !this.objectNotFound && !this.showLoadingState && !this.error && !this.empty
3403
3417
  },
3404
3418
 
3419
+ /**
3420
+ * `nextStep` with its stages read from the record when `stagesFrom`
3421
+ * names a stages map there (a case type's own texts), else as declared.
3422
+ *
3423
+ * @spec openspec/changes/detail-next-step-from-record/specs/detail-page/spec.md#requirement-the-next-step-stages-can-come-from-the-record
3424
+ * @return {object|null}
3425
+ */
3426
+ effectiveNextStep() {
3427
+ const config = this.nextStep
3428
+ if (!config || typeof config !== 'object' || typeof config.stagesFrom !== 'string' || config.stagesFrom === '' || !this.widgetVisibilityData) {
3429
+ return config
3430
+ }
3431
+ const found = readVisibleWhenPath(this.widgetVisibilityData, config.stagesFrom)
3432
+ const stages = found && typeof found === 'object' && !Array.isArray(found)
3433
+ ? (found.stages && typeof found.stages === 'object' && !Array.isArray(found.stages) ? found.stages : found)
3434
+ : null
3435
+ return stages && Object.keys(stages).length > 0 ? { ...config, stages } : config
3436
+ },
3437
+
3405
3438
  /**
3406
3439
  * The "what now" card for the record's stage, labels translated.
3407
3440
  *
@@ -3409,7 +3442,7 @@ export default {
3409
3442
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card
3410
3443
  */
3411
3444
  nextStepCard() {
3412
- const card = resolveNextStep(this.nextStep, this.currentObject, this.stageField)
3445
+ const card = resolveNextStep(this.effectiveNextStep, this.currentObject, this.stageField)
3413
3446
  if (!card) {
3414
3447
  return null
3415
3448
  }
@@ -3524,7 +3557,7 @@ export default {
3524
3557
  * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds
3525
3558
  */
3526
3559
  sideColumnWidgets() {
3527
- const resolved = this.pickedSideWidgets.filter((widget) => this.sideWidgetVisible(widget))
3560
+ const resolved = this.pickedSideWidgets.filter((widget) => this.sideWidgetOnTab(widget) && this.sideWidgetVisible(widget))
3528
3561
  if (!this.isBoard) {
3529
3562
  return resolved
3530
3563
  }
@@ -6216,6 +6249,22 @@ export default {
6216
6249
  return compactLayoutRows(drawable)
6217
6250
  },
6218
6251
 
6252
+ /**
6253
+ * Whether a side entry belongs beside the open body tab: an entry with
6254
+ * `tabs` (tab ids) shows only while one of them is open; an entry
6255
+ * without shows beside every tab. Edit mode shows them all.
6256
+ *
6257
+ * @spec openspec/changes/detail-side-cards-per-tab/specs/detail-page/spec.md#requirement-a-side-card-can-follow-the-open-tab
6258
+ * @param {object} widget The side entry's definition.
6259
+ * @return {boolean}
6260
+ */
6261
+ sideWidgetOnTab(widget) {
6262
+ if (this.editingBody || !Array.isArray(widget.tabs) || widget.tabs.length === 0) {
6263
+ return true
6264
+ }
6265
+ return widget.tabs.map((id) => String(id)).includes(this.activeBodyTab)
6266
+ },
6267
+
6219
6268
  /**
6220
6269
  * Whether a side column widget shows: always in edit mode or without a
6221
6270
  * `visibleWhen`, else while it holds for the record and the records
@@ -0,0 +1,60 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <span class="cn-detail-widget-header-items">
7
+ <CnStatusBadge
8
+ v-if="header.pill"
9
+ :label="header.pill.label"
10
+ :variant="header.pill.variant"
11
+ data-testid="cn-detail-widget-header-pill" />
12
+ <span
13
+ v-if="header.note"
14
+ class="cn-detail-page__catalog-note"
15
+ data-testid="cn-detail-widget-header-note">{{ header.note }}</span>
16
+ <button
17
+ v-for="(action, index) in header.actions"
18
+ :key="`${index}-${action.label}`"
19
+ type="button"
20
+ class="cn-detail-page__catalog-action"
21
+ data-testid="cn-detail-widget-header-action"
22
+ @click="action.run()">
23
+ <CnIcon v-if="action.icon" :name="action.icon" :size="16" />
24
+ {{ action.label }}
25
+ </button>
26
+ </span>
27
+ </template>
28
+
29
+ <script>
30
+ import CnIcon from '../CnIcon/CnIcon.vue'
31
+ import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
32
+
33
+ /**
34
+ * The extras a widget reports for its card's title row through its `header`
35
+ * event: a status pill, a note and its actions. Drawn by CnDetailWidgetHost in
36
+ * the `#actions` slot of the card it wraps a widget in.
37
+ */
38
+ export default {
39
+ name: 'CnDetailWidgetHeaderItems',
40
+
41
+ components: { CnIcon, CnStatusBadge },
42
+
43
+ props: {
44
+ /** The reported header: `{ pill, note, actions: [{ label, icon?, run }] }`. */
45
+ header: {
46
+ type: Object,
47
+ required: true,
48
+ },
49
+ },
50
+ }
51
+ </script>
52
+
53
+ <style scoped>
54
+ .cn-detail-widget-header-items {
55
+ display: inline-flex;
56
+ flex-wrap: wrap;
57
+ align-items: center;
58
+ gap: 12px;
59
+ }
60
+ </style>
@@ -184,27 +184,10 @@
184
184
  <CnIcon :name="widget.icon" :size="20" />
185
185
  </template>
186
186
  <!-- A renderer that reports its own header (`header` event:
187
- `{ link, note, action, pill }`, e.g. record-list's "3 in totaal",
188
- a facts card's status pill). -->
189
- <template v-if="rendererHeader.note || rendererHeader.action || rendererHeader.pill" #actions>
190
- <CnStatusBadge
191
- v-if="rendererHeader.pill"
192
- :label="rendererHeader.pill.label"
193
- :variant="rendererHeader.pill.variant"
194
- data-testid="cn-detail-widget-header-pill" />
195
- <span
196
- v-if="rendererHeader.note"
197
- class="cn-detail-page__catalog-note"
198
- data-testid="cn-detail-widget-header-note">{{ rendererHeader.note }}</span>
199
- <button
200
- v-if="rendererHeader.action"
201
- type="button"
202
- class="cn-detail-page__catalog-action"
203
- data-testid="cn-detail-widget-header-action"
204
- @click="rendererHeader.action.run()">
205
- <CnIcon v-if="rendererHeader.action.icon" :name="rendererHeader.action.icon" :size="16" />
206
- {{ rendererHeader.action.label }}
207
- </button>
187
+ `{ link, note, action, actions, pill }`, e.g. record-list's "3 in
188
+ totaal", a facts card's status pill). -->
189
+ <template v-if="hasRendererHeaderItems" #actions>
190
+ <CnDetailWidgetHeaderItems :header="rendererHeader" />
208
191
  </template>
209
192
  <template v-if="catalogAddEnabled" #action-items>
210
193
  <NcActionButton @click="invokeCatalogAdd">
@@ -264,10 +247,16 @@
264
247
  <template v-if="widget.icon" #title-icon>
265
248
  <CnIcon :name="widget.icon" :size="20" />
266
249
  </template>
250
+ <!-- The app widget reports its title row the way the built-in
251
+ cards do (`header` event). -->
252
+ <template v-if="hasRendererHeaderItems" #actions>
253
+ <CnDetailWidgetHeaderItems :header="rendererHeader" />
254
+ </template>
267
255
  <component
268
256
  :is="renderer"
269
257
  ref="renderer"
270
258
  v-bind="rendererProps"
259
+ @header="onRendererHeader"
271
260
  @selectObject="onSelectObject" />
272
261
  </CnWidgetWrapper>
273
262
 
@@ -304,7 +293,7 @@ import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
304
293
  import CnObjectFilesWidget from '../CnObjectFilesWidget/CnObjectFilesWidget.vue'
305
294
  import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
306
295
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
307
- import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
296
+ import CnDetailWidgetHeaderItems from './CnDetailWidgetHeaderItems.vue'
308
297
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
309
298
  import { normalizeLook } from '../../composables/useLook.js'
310
299
  import { isAppInstalled } from '../../utils/appInstalled.js'
@@ -378,12 +367,12 @@ export default {
378
367
  name: 'CnDetailWidgetHost',
379
368
 
380
369
  components: {
370
+ CnDetailWidgetHeaderItems,
381
371
  CnIcon,
382
372
  CnLeafMountHost,
383
373
  CnObjectDataWidget,
384
374
  CnObjectGeoWidget,
385
375
  CnRelatedObjectsWidget,
386
- CnStatusBadge,
387
376
  CnWidgetWrapper,
388
377
  NcActionButton,
389
378
  CnEmptyContent,
@@ -639,7 +628,7 @@ export default {
639
628
  * `header` event (screens-relation-pages): a link and a note,
640
629
  * with the list's total already filled in.
641
630
  */
642
- rendererHeader: { link: null, note: '', action: null, pill: null },
631
+ rendererHeader: { link: null, note: '', actions: [], pill: null },
643
632
  }
644
633
  },
645
634
 
@@ -672,6 +661,12 @@ export default {
672
661
  return this.isBare ? undefined : widgetTitleOf(this.widget)
673
662
  },
674
663
 
664
+ /** @return {boolean} Whether the reported header has anything for the title row. */
665
+ hasRendererHeaderItems() {
666
+ const h = this.rendererHeader
667
+ return !!(h.note || h.pill || (h.actions && h.actions.length > 0))
668
+ },
669
+
675
670
  /**
676
671
  * The card header's link: the one the renderer reports, else, with
677
672
  * `showHeaderLink`, the one the manifest declares for the widget.
@@ -1072,11 +1067,20 @@ export default {
1072
1067
  * override a value the widget computes there; the whole content still
1073
1068
  * arrives as `content`.
1074
1069
  *
1070
+ * `content.chartKind` is read as the chart's `type`, as the dashboard
1071
+ * dispatcher reads it, so a chart placed on a detail page draws.
1072
+ *
1075
1073
  * @return {object} The declared content keys.
1076
1074
  * @spec openspec/changes/declared-props-detail-index-sidebar/specs/widget-hosts/spec.md#requirement-a-widget-host-passes-only-the-keys-a-widget-declares
1075
+ * @spec openspec/changes/detail-chart-kind/specs/widget-hosts/spec.md#requirement-a-detail-chart-reads-chartkind-as-its-type
1077
1076
  */
1078
1077
  declaredContent() {
1079
- return pickDeclaredProps(this.renderer, this.content, this.widget?.type || '')
1078
+ let bindings = this.content
1079
+ if (bindings && typeof bindings === 'object' && bindings.chartKind !== undefined && bindings.type === undefined) {
1080
+ const { chartKind, ...rest } = bindings
1081
+ bindings = { ...rest, type: chartKind }
1082
+ }
1083
+ return pickDeclaredProps(this.renderer, bindings, this.widget?.type || '')
1080
1084
  },
1081
1085
 
1082
1086
  /**
@@ -1217,17 +1221,20 @@ export default {
1217
1221
 
1218
1222
  methods: {
1219
1223
  /**
1220
- * Take the header a content-only renderer reports.
1224
+ * Take the header a content-only renderer, or an app widget in the
1225
+ * titled card, reports.
1221
1226
  *
1222
- * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null}} header The header parts.
1227
+ * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null, actions?: Array<{label: string, icon?: string, run: () => unknown}>}} header The header parts.
1223
1228
  * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
1224
1229
  */
1225
1230
  onRendererHeader(header) {
1226
1231
  const link = header && header.link && typeof header.link === 'object' ? header.link : null
1227
1232
  const note = header && typeof header.note === 'string' ? header.note : ''
1228
- const action = header && header.action && typeof header.action.run === 'function' && header.action.label ? header.action : null
1233
+ const usable = (action) => action && typeof action.run === 'function' && action.label
1234
+ // `action` (one) and `actions` (several) both count, `action` first.
1235
+ const actions = [header && header.action, ...((header && Array.isArray(header.actions)) ? header.actions : [])].filter(usable)
1229
1236
  const pill = header && header.pill && typeof header.pill.label === 'string' && header.pill.label !== '' ? header.pill : null
1230
- this.rendererHeader = { link, note, action, pill }
1237
+ this.rendererHeader = { link, note, actions, pill }
1231
1238
  },
1232
1239
 
1233
1240
  /**
@@ -1206,11 +1206,15 @@ export default {
1206
1206
  min-width: 0;
1207
1207
  }
1208
1208
 
1209
+ /* `padding: 0`: Nextcloud's core stylesheet pads every `dl` 12px at the top
1210
+ and bottom, which put a side card's facts 24px under its title, against
1211
+ the board's 12 (DqZaak "Behandeling", DqZaakDocumenten). */
1209
1212
  .cn-facts-widget__rows {
1210
1213
  display: grid;
1211
1214
  grid-template-columns: max-content 1fr;
1212
1215
  gap: 8px 16px;
1213
1216
  margin: 0;
1217
+ padding: 0;
1214
1218
  font-size: 14px;
1215
1219
  }
1216
1220
 
@@ -5,7 +5,7 @@
5
5
  <template>
6
6
  <div
7
7
  class="cn-files-browser"
8
- :class="{ 'cn-files-browser--dragging': dragging }"
8
+ :class="{ 'cn-files-browser--dragging': dragging, 'cn-files-browser--drop-panel': dropOverlay && dropDetail !== '' }"
9
9
  data-testid="cn-files-browser"
10
10
  @dragenter.prevent="onDragEnter"
11
11
  @dragover.prevent="onDragOver"
@@ -22,7 +22,10 @@
22
22
  aria-hidden="true"
23
23
  data-testid="cn-files-browser-drop-overlay">
24
24
  <span class="cn-files-browser__drop-icon"><Upload :size="28" /></span>
25
- <strong class="cn-files-browser__drop-label">{{ dropLabel }}</strong>
25
+ <strong class="cn-files-browser__drop-label">{{ cnTranslate(dropLabel) }}</strong>
26
+ <span v-if="dropDetailText"
27
+ class="cn-files-browser__drop-detail"
28
+ data-testid="cn-files-browser-drop-detail">{{ dropDetailText }}</span>
26
29
  </div>
27
30
  <p v-if="dropOverlay"
28
31
  class="hidden-visually"
@@ -1026,12 +1029,26 @@ export default {
1026
1029
  default: false,
1027
1030
  },
1028
1031
 
1029
- /** The line `dropOverlay` draws and announces. */
1032
+ /** The line `dropOverlay` draws and announces, through the host translate. */
1030
1033
  dropLabel: {
1031
1034
  type: String,
1032
1035
  default: () => t('nextcloud-vue', 'Drop to add'),
1033
1036
  },
1034
1037
 
1038
+ /**
1039
+ * A second line under `dropLabel` in the drop state, through the host
1040
+ * translate: where the files go ("{count} bestanden komen bij zaak
1041
+ * 2026-0082, als nog te beoordelen document."). `{count}` is the number
1042
+ * of files dragged when the browser can tell, and the line is left out
1043
+ * when it cannot. The overlay then also fills the panel.
1044
+ *
1045
+ * @spec openspec/changes/files-drop-panel/specs/files-browser/spec.md#requirement-the-drop-state-fills-the-panel-and-says-where-the-files-go
1046
+ */
1047
+ dropDetail: {
1048
+ type: String,
1049
+ default: '',
1050
+ },
1051
+
1035
1052
  /** Show a line under the list saying files can be dropped on it. */
1036
1053
  dropHint: {
1037
1054
  type: Boolean,
@@ -1259,6 +1276,8 @@ export default {
1259
1276
  */
1260
1277
  visibleColumnKeys: null,
1261
1278
  dragging: false,
1279
+ /** The number of files the current drag carries, null when unknown. */
1280
+ dragCount: null,
1262
1281
  /** Nested dragenter/dragleave pairs, so a child does not end the drag. */
1263
1282
  dragDepth: 0,
1264
1283
  /** What the live region says: the drop label, then what was added. */
@@ -1294,6 +1313,24 @@ export default {
1294
1313
  },
1295
1314
 
1296
1315
  computed: {
1316
+ /**
1317
+ * `dropDetail` through the host translate with `{count}` filled, '' when
1318
+ * there is none or the count it needs is unknown.
1319
+ *
1320
+ * @spec openspec/changes/files-drop-panel/specs/files-browser/spec.md#requirement-the-drop-state-fills-the-panel-and-says-where-the-files-go
1321
+ * @return {string}
1322
+ */
1323
+ dropDetailText() {
1324
+ if (!this.dropDetail) {
1325
+ return ''
1326
+ }
1327
+ const text = this.cnTranslate(this.dropDetail)
1328
+ if (!text.includes('{count}')) {
1329
+ return text
1330
+ }
1331
+ return this.dragCount === null ? '' : text.replace(/\{count\}/g, String(this.dragCount))
1332
+ },
1333
+
1297
1334
  /**
1298
1335
  * Whether this person may add to and change the open folder. Over
1299
1336
  * WebDAV the server answers each write on its own, so this is true;
@@ -2247,7 +2284,8 @@ export default {
2247
2284
  this.dragDepth++
2248
2285
  if (!this.dragging) {
2249
2286
  this.dragging = true
2250
- this.dropStatus = this.dropOverlay ? this.dropLabel : ''
2287
+ this.dragCount = this.draggedFileCount(event)
2288
+ this.dropStatus = this.dropOverlay ? [this.cnTranslate(this.dropLabel), this.dropDetailText].filter(Boolean).join(' ') : ''
2251
2289
  }
2252
2290
  },
2253
2291
 
@@ -2277,6 +2315,22 @@ export default {
2277
2315
  }
2278
2316
  },
2279
2317
 
2318
+ /**
2319
+ * How many files a drag carries, or null when the browser does not
2320
+ * say (it lists the items only once they are dropped in some browsers).
2321
+ *
2322
+ * @param {DragEvent} event The drag.
2323
+ * @return {number|null}
2324
+ */
2325
+ draggedFileCount(event) {
2326
+ const items = event?.dataTransfer?.items
2327
+ if (!items || typeof items.length !== 'number') {
2328
+ return null
2329
+ }
2330
+ const count = Array.from(items).filter((item) => item && item.kind === 'file').length
2331
+ return count > 0 ? count : null
2332
+ },
2333
+
2280
2334
  /**
2281
2335
  * Whether a drag carries files (not text or a link from the page).
2282
2336
  * A drag whose types cannot be read counts as files, as before.
@@ -2888,6 +2942,18 @@ export default {
2888
2942
  outline-offset: 4px;
2889
2943
  }
2890
2944
 
2945
+ /* With a `dropDetail` the drop state fills the panel it sits in, as the
2946
+ board draws it (DqZaakDocumentenSlepen), not only the list's own height. */
2947
+ .cn-files-browser--dragging.cn-files-browser--drop-panel {
2948
+ min-height: max(320px, 100%);
2949
+ }
2950
+
2951
+ .cn-files-browser__drop-detail {
2952
+ max-width: 520px;
2953
+ font-size: 14px;
2954
+ color: var(--color-main-text);
2955
+ }
2956
+
2891
2957
  .cn-files-browser__drop-overlay {
2892
2958
  position: absolute;
2893
2959
  inset: 0;