@conduction/nextcloud-vue 2.100.0 → 2.102.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 (168) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +22 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +29 -4
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  11. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
  12. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +70 -6
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +105 -74
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
  21. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
  22. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
  23. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
  24. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
  25. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
  32. package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
  33. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
  34. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
  36. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
  38. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
  40. package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +320 -74
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +323 -213
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/useNamedSource.js +30 -3
  46. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  47. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  48. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
  49. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
  52. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
  53. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  56. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  66. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
  67. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  68. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
  69. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  70. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
  71. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
  72. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  73. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
  74. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
  76. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
  80. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
  81. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  82. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
  83. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  84. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  85. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
  86. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  90. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
  91. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  92. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  93. package/dist/esm/composables/indexSources.js +176 -20
  94. package/dist/esm/composables/indexSources.js.map +1 -1
  95. package/dist/esm/composables/useEndpointSource.js +28 -6
  96. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  97. package/dist/esm/index.js +1 -0
  98. package/dist/esm/index.js.map +1 -1
  99. package/dist/esm/l10n/en.json.js +41 -1
  100. package/dist/esm/l10n/en.json.js.map +1 -1
  101. package/dist/esm/l10n/nl.json.js +39 -1
  102. package/dist/esm/l10n/nl.json.js.map +1 -1
  103. package/dist/esm/library-style.js +1 -1
  104. package/dist/esm/nextcloud-vue.css +939 -376
  105. package/dist/esm/utils/builtInFormatters.js +24 -4
  106. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  107. package/dist/esm/utils/languageMapText.js +50 -0
  108. package/dist/esm/utils/languageMapText.js.map +1 -0
  109. package/dist/esm/utils/libraryWidgetKeys.js +3 -0
  110. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  111. package/dist/esm/utils/mapList.js +71 -0
  112. package/dist/esm/utils/mapList.js.map +1 -0
  113. package/dist/esm/utils/rowClassRules.js +43 -0
  114. package/dist/esm/utils/rowClassRules.js.map +1 -0
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/esm/utils/visibleWhen.js +19 -2
  118. package/dist/esm/utils/visibleWhen.js.map +1 -1
  119. package/dist/nextcloud-vue.cjs.js +9681 -7136
  120. package/dist/nextcloud-vue.cjs.js.map +2 -2
  121. package/dist/nextcloud-vue.css +939 -376
  122. package/l10n/en.json +41 -1
  123. package/l10n/nl.json +39 -1
  124. package/package.json +1 -1
  125. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  126. package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
  127. package/src/components/CnChartWidget/CnChartWidget.md +3 -2
  128. package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
  129. package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
  130. package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
  131. package/src/components/CnDashboardPage/CnDashboardPage.vue +110 -21
  132. package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
  133. package/src/components/CnExportsWidget/index.js +7 -0
  134. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
  135. package/src/components/CnFilesBrowser/filesReview.js +57 -0
  136. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
  137. package/src/components/CnGroupedCardsWidget/index.js +28 -0
  138. package/src/components/CnIndexPage/CnIndexPage.md +5 -1
  139. package/src/components/CnIndexPage/CnIndexPage.vue +313 -9
  140. package/src/components/CnIndexPage/useNamedSource.js +30 -3
  141. package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
  142. package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
  143. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
  144. package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
  145. package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
  146. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
  147. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
  148. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
  149. package/src/components/CnSavedViewAlertWidget/index.js +7 -0
  150. package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
  151. package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
  152. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  153. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
  154. package/src/components/index.js +3 -0
  155. package/src/composables/indexSources.js +175 -19
  156. package/src/composables/useEndpointSource.js +28 -6
  157. package/src/css/index-page.css +117 -0
  158. package/src/css/look-board-index.css +10 -0
  159. package/src/css/look-board.css +44 -0
  160. package/src/index.js +1 -0
  161. package/src/schemas/app-manifest-v2.schema.json +85 -3
  162. package/src/utils/builtInFormatters.js +24 -4
  163. package/src/utils/languageMapText.js +46 -0
  164. package/src/utils/libraryWidgetKeys.js +3 -0
  165. package/src/utils/mapList.js +67 -0
  166. package/src/utils/rowClassRules.js +39 -0
  167. package/src/utils/validateManifestV2.compiled.js +3 -3
  168. package/src/utils/visibleWhen.js +19 -2
@@ -0,0 +1,433 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-grouped-cards" data-testid="cn-grouped-cards">
8
+ <p v-if="error" class="cn-grouped-cards__state" data-testid="cn-grouped-cards-error">
9
+ {{ errorLabel }}
10
+ </p>
11
+ <div v-else-if="loading && groups.length === 0" class="cn-grouped-cards__loading">
12
+ <NcLoadingIcon :size="20" :name="loadingLabel" />
13
+ </div>
14
+ <p v-else-if="groups.length === 0 && !extra" class="cn-grouped-cards__state" data-testid="cn-grouped-cards-empty">
15
+ {{ emptyLabel }}
16
+ </p>
17
+ <div
18
+ v-else
19
+ class="cn-grouped-cards__grid"
20
+ :style="{ '--cn-grouped-cards-columns': columns }">
21
+ <section
22
+ v-for="group in groups"
23
+ :key="group.key"
24
+ class="cn-grouped-cards__group"
25
+ :aria-labelledby="group.headingId"
26
+ data-testid="cn-grouped-cards-group">
27
+ <header class="cn-grouped-cards__group-header">
28
+ <h4 :id="group.headingId" class="cn-grouped-cards__group-title">
29
+ {{ group.title }}
30
+ </h4>
31
+ <span v-if="group.count" class="cn-grouped-cards__count" data-testid="cn-grouped-cards-count">{{ group.count }}</span>
32
+ </header>
33
+ <ul v-if="group.items.length > 0" class="cn-grouped-cards__items">
34
+ <li
35
+ v-for="item in group.items"
36
+ :key="item.key"
37
+ class="cn-grouped-cards__item"
38
+ data-testid="cn-grouped-cards-item">
39
+ <span class="cn-grouped-cards__item-title">{{ item.title }}</span>
40
+ <span v-if="item.meta" class="cn-grouped-cards__item-meta">{{ item.meta }}</span>
41
+ </li>
42
+ </ul>
43
+ <p v-else class="cn-grouped-cards__group-empty">
44
+ {{ groupEmptyLabel }}
45
+ </p>
46
+ </section>
47
+ <section
48
+ v-if="extra"
49
+ class="cn-grouped-cards__group cn-grouped-cards__group--extra"
50
+ :aria-labelledby="extra.headingId"
51
+ data-testid="cn-grouped-cards-extra">
52
+ <header class="cn-grouped-cards__group-header">
53
+ <h4 :id="extra.headingId" class="cn-grouped-cards__group-title">
54
+ {{ extra.title }}
55
+ </h4>
56
+ </header>
57
+ <p class="cn-grouped-cards__extra-text">
58
+ {{ extra.text }}
59
+ </p>
60
+ </section>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <script>
66
+ import { translate as t } from '@nextcloud/l10n'
67
+ import { NcLoadingIcon } from '@nextcloud/vue'
68
+ import { inject, ref } from 'vue'
69
+ import { useEndpointSource } from '../../composables/useEndpointSource.js'
70
+ import { fillTemplate } from '../../utils/fillTemplate.js'
71
+ import { unwrapAppConfig } from '../../utils/formatMetric.js'
72
+ import { languageMapText } from '../../utils/languageMapText.js'
73
+ import { readPath } from '../../utils/readPath.js'
74
+
75
+ let uid = 0
76
+
77
+ /**
78
+ * Unwrap a value that may be a ref.
79
+ *
80
+ * @param {unknown} value The value.
81
+ * @return {unknown} The plain value.
82
+ */
83
+ function unwrap(value) {
84
+ return value && typeof value === 'object' && 'value' in value ? value.value : value
85
+ }
86
+
87
+ /**
88
+ * A row as the templates read it: a language map (`{ nl, en }`) becomes its
89
+ * text in the user's language, and a list of plain values is joined with ", ".
90
+ * Nested objects stay as they are, for a template that reads a deeper path.
91
+ *
92
+ * @param {object} row The row.
93
+ * @return {object} A shallow copy with readable values.
94
+ */
95
+ function readable(row) {
96
+ if (!row || typeof row !== 'object') {
97
+ return {}
98
+ }
99
+ const out = {}
100
+ for (const [key, value] of Object.entries(row)) {
101
+ if (Array.isArray(value)) {
102
+ const plain = value.filter((entry) => typeof entry === 'string' || typeof entry === 'number')
103
+ out[key] = plain.length === value.length ? plain.join(', ') : value
104
+ } else if (value && typeof value === 'object' && Object.values(value).every((entry) => typeof entry === 'string')) {
105
+ out[key] = languageMapText(value)
106
+ } else {
107
+ out[key] = value
108
+ }
109
+ }
110
+ return out
111
+ }
112
+
113
+ /**
114
+ * CnGroupedCardsWidget — groups of cards side by side, read from an app
115
+ * endpoint: a case type's phases, each with the tasks it runs, and a dashed
116
+ * box of what is available in every phase (board DqZaaktype, Fasen en taken).
117
+ *
118
+ * Content-only on a card surface: the detail host or the dashboard draws the
119
+ * titled card around it.
120
+ */
121
+ export default {
122
+ name: 'CnGroupedCardsWidget',
123
+
124
+ components: { NcLoadingIcon },
125
+
126
+ inject: {
127
+ /** The host app's translate function. */
128
+ cnTranslate: { default: () => (key) => key },
129
+ /** The page's app config map, for money tokens in a template. */
130
+ cnAppConfig: { default: () => ({}) },
131
+ },
132
+
133
+ props: {
134
+ /**
135
+ * The widget config: `{ endpointSource, groupsPath?, itemsKey?,
136
+ * groupTitle?, groupCount?, itemTitle?, itemMeta?, columns?,
137
+ * extraGroup?, emptyText?, groupEmptyText?, errorText? }`.
138
+ *
139
+ * - `endpointSource` (`{ url, params?, responsePath? }`) reads the payload; tokens
140
+ * such as `@objectId` resolve as on every widget.
141
+ * - `groupsPath` (default `groups`) is the list of groups in the payload, and
142
+ * `itemsKey` (default `items`) the list of cards in each group.
143
+ * - `groupTitle` and `itemTitle` are templates (`"Phase {order}: {name}"`,
144
+ * `"{title}"`); `itemMeta` is a template or a list of templates joined
145
+ * with " · ", a part whose token is empty being left out.
146
+ * - `groupCount` (`{ one, other }`, `{count}` filled in) draws a count pill.
147
+ * - `extraGroup` (`{ title, path, itemText, join? }`) closes the grid with a
148
+ * dashed box whose entries, read at `path` of the payload, are one line.
149
+ * - `columns` (default 2) is the number of groups side by side.
150
+ *
151
+ * A language map (`{ nl, en }`) reads in the user's language, and a list
152
+ * of plain values reads joined with ", ". Texts go through the app's
153
+ * translate.
154
+ *
155
+ * @type {object}
156
+ */
157
+ content: {
158
+ type: Object,
159
+ default: () => ({}),
160
+ },
161
+
162
+ /** The current record's id, for the `@objectId` token. */
163
+ objectId: {
164
+ type: [String, Number],
165
+ default: null,
166
+ },
167
+
168
+ /** The current record, for `@object.<field>` tokens. */
169
+ objectData: {
170
+ type: Object,
171
+ default: null,
172
+ },
173
+ },
174
+
175
+ setup(props) {
176
+ const objectCtxRaw = inject('cnObjectContext', null)
177
+ const workspaceRaw = inject('cnWorkspaceContext', null)
178
+ const appConfigRaw = inject('cnAppConfig', ref({}))
179
+ const { data, loading, error } = useEndpointSource(
180
+ () => props.content?.endpointSource || null,
181
+ {
182
+ ctx: () => {
183
+ const injected = unwrap(objectCtxRaw) || {}
184
+ return {
185
+ ...injected,
186
+ objectId: props.objectId ?? injected.objectId ?? props.objectData?.id ?? null,
187
+ object: props.objectData || injected.object || null,
188
+ workspace: unwrap(workspaceRaw) || {},
189
+ config: unwrapAppConfig(appConfigRaw) || {},
190
+ }
191
+ },
192
+ },
193
+ )
194
+ uid += 1
195
+ return { payload: data, loading, error, idPrefix: `cn-grouped-cards-${uid}` }
196
+ },
197
+
198
+ computed: {
199
+ /** @return {number} The groups side by side. */
200
+ columns() {
201
+ const value = Number(this.content?.columns)
202
+ return Number.isFinite(value) && value >= 1 ? Math.min(4, Math.floor(value)) : 2
203
+ },
204
+
205
+ /**
206
+ * The groups ready to draw.
207
+ *
208
+ * @return {Array<{key: string, headingId: string, title: string, count: string, items: Array<object>}>} The groups.
209
+ * @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint
210
+ */
211
+ groups() {
212
+ const c = this.content || {}
213
+ const list = readPath(this.payload, c.groupsPath || 'groups')
214
+ if (!Array.isArray(list)) {
215
+ return []
216
+ }
217
+ return list.filter((group) => group && typeof group === 'object').map((raw, index) => {
218
+ const group = readable(raw)
219
+ const itemList = Array.isArray(raw[c.itemsKey || 'items']) ? raw[c.itemsKey || 'items'] : []
220
+ const items = itemList.filter((item) => item && typeof item === 'object').map((rawItem, itemIndex) => {
221
+ const item = readable(rawItem)
222
+ return {
223
+ key: String(rawItem.id ?? `item-${itemIndex}`),
224
+ title: this.fill(c.itemTitle || '{title}', item),
225
+ meta: this.metaOf(item),
226
+ }
227
+ })
228
+ return {
229
+ key: String(raw.id ?? `group-${index}`),
230
+ headingId: `${this.idPrefix}-group-${index}`,
231
+ title: this.fill(c.groupTitle || '{name}', group),
232
+ count: this.countOf(items.length),
233
+ items,
234
+ }
235
+ })
236
+ },
237
+
238
+ /**
239
+ * The closing box of entries available everywhere, or null.
240
+ *
241
+ * @return {{headingId: string, title: string, text: string}|null} The box.
242
+ * @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint
243
+ */
244
+ extra() {
245
+ const spec = this.content?.extraGroup
246
+ if (!spec || typeof spec !== 'object' || !spec.path) {
247
+ return null
248
+ }
249
+ const list = readPath(this.payload, spec.path)
250
+ if (!Array.isArray(list) || list.length === 0) {
251
+ return null
252
+ }
253
+ const parts = list
254
+ .map((entry) => (entry && typeof entry === 'object' ? this.fill(spec.itemText || '{label}', readable(entry)) : languageMapText(entry)))
255
+ .filter((text) => text !== '')
256
+ if (parts.length === 0) {
257
+ return null
258
+ }
259
+ return {
260
+ headingId: `${this.idPrefix}-extra`,
261
+ title: this.translate(spec.title || 'Always available'),
262
+ text: parts.join(typeof spec.join === 'string' ? spec.join : ' · '),
263
+ }
264
+ },
265
+
266
+ /** @return {string} The empty text. */
267
+ emptyLabel() {
268
+ return this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing to show yet')
269
+ },
270
+
271
+ /** @return {string} The text of a group without cards. */
272
+ groupEmptyLabel() {
273
+ return this.content?.groupEmptyText ? this.translate(this.content.groupEmptyText) : t('nextcloud-vue', 'Nothing here yet')
274
+ },
275
+
276
+ /** @return {string} The error text. */
277
+ errorLabel() {
278
+ return this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')
279
+ },
280
+
281
+ /** @return {string} The loading label. */
282
+ loadingLabel() {
283
+ return t('nextcloud-vue', 'Loading')
284
+ },
285
+ },
286
+
287
+ methods: {
288
+ /**
289
+ * Translate a text through the host app's catalogue.
290
+ *
291
+ * @param {string} text The text.
292
+ * @return {string} The translated text.
293
+ */
294
+ translate(text) {
295
+ const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
296
+ return fn(text)
297
+ },
298
+
299
+ /**
300
+ * Fill a template from a row, after translating it.
301
+ *
302
+ * @param {string} template The template.
303
+ * @param {object} row The row.
304
+ * @return {string} The text, '' when a token is empty.
305
+ */
306
+ fill(template, row) {
307
+ if (typeof template !== 'string' || template === '') {
308
+ return ''
309
+ }
310
+ return fillTemplate(this.translate(template), row, new Date(), { config: unwrapAppConfig(this.cnAppConfig) })
311
+ },
312
+
313
+ /**
314
+ * A card's meta line: one template, or several joined with " · ", a
315
+ * part whose token is empty being left out with its separator.
316
+ *
317
+ * @param {object} item The card's row.
318
+ * @return {string} The line.
319
+ */
320
+ metaOf(item) {
321
+ const spec = this.content?.itemMeta
322
+ const parts = Array.isArray(spec) ? spec : (typeof spec === 'string' ? [spec] : [])
323
+ return parts.map((part) => this.fill(part, item)).filter((text) => text !== '').join(' · ')
324
+ },
325
+
326
+ /**
327
+ * The count pill of a group (`groupCount: { one, other }`).
328
+ *
329
+ * @param {number} count The number of cards.
330
+ * @return {string} The text, '' without `groupCount`.
331
+ */
332
+ countOf(count) {
333
+ const spec = this.content?.groupCount
334
+ if (!spec || typeof spec !== 'object') {
335
+ return ''
336
+ }
337
+ const template = count === 1 ? spec.one : spec.other
338
+ return typeof template === 'string' && template !== '' ? this.translate(template).replace(/\{count\}/g, String(count)) : ''
339
+ },
340
+ },
341
+ }
342
+ </script>
343
+
344
+ <style scoped>
345
+ .cn-grouped-cards__grid {
346
+ display: grid;
347
+ grid-template-columns: repeat(var(--cn-grouped-cards-columns, 2), minmax(0, 1fr));
348
+ gap: 16px;
349
+ }
350
+
351
+ @media (max-width: 700px) {
352
+ .cn-grouped-cards__grid {
353
+ grid-template-columns: minmax(0, 1fr);
354
+ }
355
+ }
356
+
357
+ .cn-grouped-cards__group {
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 8px;
361
+ padding: 12px 14px 14px;
362
+ border-radius: var(--border-radius-large, 10px);
363
+ background: var(--color-background-hover);
364
+ }
365
+
366
+ .cn-grouped-cards__group--extra {
367
+ background: transparent;
368
+ border: 1px dashed var(--color-border-dark);
369
+ }
370
+
371
+ .cn-grouped-cards__group-header {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: space-between;
375
+ gap: 8px;
376
+ }
377
+
378
+ .cn-grouped-cards__group-title {
379
+ margin: 0;
380
+ font-size: 15px;
381
+ font-weight: 600;
382
+ color: var(--color-main-text);
383
+ }
384
+
385
+ .cn-grouped-cards__count {
386
+ padding: 2px 8px;
387
+ border-radius: var(--border-radius-pill, 12px);
388
+ background: var(--color-main-background);
389
+ color: var(--color-text-maxcontrast);
390
+ font-size: 12px;
391
+ font-weight: 600;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .cn-grouped-cards__items {
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 8px;
399
+ margin: 0;
400
+ padding: 0;
401
+ list-style: none;
402
+ }
403
+
404
+ .cn-grouped-cards__item {
405
+ display: flex;
406
+ flex-direction: column;
407
+ gap: 2px;
408
+ padding: 10px 12px;
409
+ border: 1px solid var(--color-border);
410
+ border-radius: var(--border-radius-large, 8px);
411
+ background: var(--color-main-background);
412
+ }
413
+
414
+ .cn-grouped-cards__item-title {
415
+ font-weight: 600;
416
+ color: var(--color-main-text);
417
+ }
418
+
419
+ .cn-grouped-cards__item-meta,
420
+ .cn-grouped-cards__extra-text,
421
+ .cn-grouped-cards__group-empty,
422
+ .cn-grouped-cards__state {
423
+ margin: 0;
424
+ color: var(--color-text-maxcontrast);
425
+ font-size: 13px;
426
+ }
427
+
428
+ .cn-grouped-cards__loading {
429
+ display: flex;
430
+ justify-content: center;
431
+ padding: 8px 0;
432
+ }
433
+ </style>
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnGroupedCardsWidget — self-registers the `grouped-cards` widget type
6
+ * (groups of cards side by side, read from an app endpoint) into the shared
7
+ * dashboardWidgetRegistry at module load.
8
+ */
9
+
10
+ import CnGroupedCardsWidget from './CnGroupedCardsWidget.vue'
11
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
12
+
13
+ registerDashboardWidget('grouped-cards', {
14
+ renderer: CnGroupedCardsWidget,
15
+ form: null,
16
+ defaultContent: {
17
+ endpointSource: { url: '' },
18
+ groupsPath: 'groups',
19
+ itemsKey: 'items',
20
+ columns: 2,
21
+ },
22
+ displayName: 'Grouped cards',
23
+ icon: 'ViewGridOutline',
24
+ surfaces: ['detail-page', 'app-dashboard'],
25
+ })
26
+
27
+ export { CnGroupedCardsWidget }
28
+ export default CnGroupedCardsWidget
@@ -362,7 +362,7 @@ export default {
362
362
  | `lensReasonTexts` | Object \| null | `null` | App wording for an unavailable lens, keyed `<lens>.<reason>` or `<reason>`. |
363
363
  | `loadingText` | String | `'Loading…'` | Accessible label for the loading spinner (NcLoadingIcon aria-label) |
364
364
  | `rowIcon` | String \| Function | `null` | Optional leading icon for every table row — a static MDI name or `(row) => iconName`. Forwarded to CnDataTable; fed from the manifest as `pages[].config.rowIcon`. Unset = no icon column. |
365
- | `rowClass` | Function | `null` | Callback returning CSS class(es) for a row |
365
+ | `rowClass` | Function \| Array | `null` | Callback returning CSS class(es) for a row, or (manifest `config.rowClass`) rules `[{ when: { field, op?, value }, class }]` compiled into one: each adds its class to a row whose field compares to the value (`visibleWhen` operators; two dates compare as moments, `@today` included). |
366
366
  | `rowIcon` | String \| Function | `null` | Optional leading row icon forwarded to CnDataTable — a static MDI icon name or `(row) => iconName`. Fed from the manifest as `pages[].config.rowIcon`; unset = no icon column |
367
367
  | `inlineActionCount` | Number | `2` | How many row actions to show inline (rest go in overflow menu) |
368
368
  | `showMassImport` | Boolean | `true` | Whether to show the mass Import action |
@@ -414,6 +414,7 @@ export default {
414
414
  | `quickFilterMode` | String | `'chips'` | How the quick filters render: `'chips'` (pill strip) or `'dropdown'` (a single `NcSelect`). Fed from `pages[].config.quickFilterMode`. |
415
415
  | `quickFilterMultiple` | Boolean | `false` | Allow several quick filters active at once; selected tabs' filters are OR-ed into the fetch (same field → `field[]=` array). Fed from `pages[].config.quickFilterMultiple`. |
416
416
  | `quickFilterMaxVisible` | Number | `0` | Chips mode only — how many quick-filter pills render inline before the rest move behind one more chip — a `⋯` pill that opens a panel of the hidden lenses. `0` renders every tab, which wraps the actions bar onto a second line once a page declares more than a handful. The visible set is the first *n* entries of `quickFilters`. Fed from `pages[].config.quickFilterMaxVisible`. |
417
+ | `quickFilterPlacement` | String | `''` | Where the quick-filter chips sit. `'toolbar'` keeps them on one row with the Filter button and the view switch, in whatever width that row has beside a side panel (DqTeamwachtrij): the chips take the room the buttons leave, and those that do not fit move behind the overflow chip. Fed from `pages[].config.quickFilterPlacement`. |
417
418
  | `cardComponent` | String | `''` | Optional name of a consumer-provided card component (registered in the v2 `registry` — any kind carrying a `component` — or in the legacy `customComponents` map on `CnAppRoot`) to render in place of the default `CnObjectCard` when the page is in card-grid view mode. Resolution priority: `#card` scoped slot → `cardComponent` registry entry → default `CnObjectCard`. Unknown names log `console.warn` once and fall back to the default. |
418
419
  | `customComponents` | Object | `null` | Optional explicit `customComponents` registry. Overrides the registry injected from `CnAppRoot` via `cnCustomComponents`. Mostly used by unit tests; production consumers register components on `CnAppRoot` instead. |
419
420
  | `subscribe` | Boolean | `true` | Self-fetch mode only. When `register` + `schema` are set (and no `objects` prop), the page subscribes to the collection's `or-collection-{register}-{schema}` live-update scope on mount and refetches on an update event; released on unmount. Set `false` (manifest: `config.subscribe: false`) for static / read-once views. No-op in consumer-managed mode or on stores without live-updates support. |
@@ -1136,3 +1137,6 @@ columns are right.
1136
1137
  ## Board look
1137
1138
 
1138
1139
  With `look: "board"` (the page's `look` prop, `config.look`, or the app's) the index page takes the DqZaken setup: a header with the count line from `countText` (`{shown}`, `{total}`), the buttons Download, Actions, the buildiq square and the primary one in that order, a two-row toolbar (saved-view chips, Filter with a count and the view switch; then search and active filter chips), a bulk band between toolbar and table (`bulkHint`), the table as a white card with one menu button per row, and a footer inside the card (`footerNote`). `cardFields` names the facts of each card in the cards view. Without the look nothing changes.
1140
+
1141
+ `bulkBandAlways` (`config.bulkBandAlways`) draws the board bulk band before a selection too, its buttons
1142
+ disabled until rows are selected (DqZaken, DqWerkbord).