@conduction/nextcloud-vue 2.115.0 → 2.116.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 (117) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +92 -0
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +2 -2
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js +9 -0
  7. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js.map +1 -0
  8. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js +145 -0
  9. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js.map +1 -0
  10. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js +58 -0
  11. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js.map +1 -0
  12. package/dist/esm/components/CnChecklistWidget/index.js +28 -0
  13. package/dist/esm/components/CnChecklistWidget/index.js.map +1 -0
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -0
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +376 -51
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +364 -285
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -0
  25. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +6 -4
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  28. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +1 -1
  29. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +48 -1
  30. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +11 -2
  32. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +1 -1
  34. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +11 -3
  35. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +8 -3
  37. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +55 -2
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +94 -4
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +248 -239
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +9 -0
  48. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js.map +1 -0
  49. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +282 -0
  50. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -0
  51. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +202 -0
  52. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -0
  53. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  54. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
  55. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  58. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  59. package/dist/esm/composables/indexSources.js +64 -0
  60. package/dist/esm/composables/indexSources.js.map +1 -1
  61. package/dist/esm/composables/useTaskInboxStore.js +85 -0
  62. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  63. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +9 -0
  64. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js.map +1 -0
  65. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +107 -0
  66. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -0
  67. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +110 -0
  68. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -0
  69. package/dist/esm/index.js +1 -0
  70. package/dist/esm/index.js.map +1 -1
  71. package/dist/esm/l10n/en.json.js +19 -1
  72. package/dist/esm/l10n/en.json.js.map +1 -1
  73. package/dist/esm/l10n/nl.json.js +23 -1
  74. package/dist/esm/l10n/nl.json.js.map +1 -1
  75. package/dist/esm/library-style.js +1 -1
  76. package/dist/esm/nextcloud-vue.css +1054 -911
  77. package/dist/esm/utils/actionsDispatcher.js +10 -5
  78. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  79. package/dist/esm/utils/detailActionModel.js +6 -2
  80. package/dist/esm/utils/detailActionModel.js.map +1 -1
  81. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  82. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  83. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  84. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  85. package/dist/esm/utils/widgetDispatch.js +1 -1
  86. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  87. package/dist/nextcloud-vue.cjs.js +17078 -15224
  88. package/dist/nextcloud-vue.cjs.js.map +2 -2
  89. package/dist/nextcloud-vue.css +1054 -911
  90. package/l10n/en.json +19 -1
  91. package/l10n/nl.json +23 -1
  92. package/package.json +1 -1
  93. package/src/components/CnActionButtons/CnActionButtons.vue +93 -1
  94. package/src/components/CnChecklistWidget/CnChecklistWidget.vue +206 -0
  95. package/src/components/CnChecklistWidget/index.js +27 -0
  96. package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
  97. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  98. package/src/components/CnDetailPage/CnDetailPage.vue +377 -10
  99. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +15 -2
  100. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +59 -1
  101. package/src/components/CnMacrosWidget/CnMacrosWidget.vue +16 -6
  102. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +55 -2
  103. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +101 -3
  104. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +508 -0
  105. package/src/components/CnTabsWidget/CnTabsWidget.vue +10 -5
  106. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  107. package/src/components/index.js +1 -0
  108. package/src/composables/indexSources.js +64 -0
  109. package/src/composables/useTaskInboxStore.js +85 -0
  110. package/src/dialogs/CnAccessPurposeDialog.vue +183 -0
  111. package/src/index.js +1 -0
  112. package/src/schemas/app-manifest-v2.schema.json +49 -3
  113. package/src/utils/actionsDispatcher.js +10 -5
  114. package/src/utils/detailActionModel.js +6 -2
  115. package/src/utils/libraryWidgetKeys.js +1 -0
  116. package/src/utils/validateManifestV2.compiled.js +3 -3
  117. package/src/utils/widgetDispatch.js +1 -1
@@ -223,6 +223,91 @@ export const useTaskInboxStore = defineStore('cnTaskInbox', {
223
223
  }
224
224
  },
225
225
 
226
+ /**
227
+ * Run one lifecycle verb on a task read for a detail page, and keep
228
+ * that record in step with the engine's answer.
229
+ *
230
+ * `url` replaces the engine's own verb route for an app that puts a
231
+ * step of its own in front of the engine (dossiq completes a task
232
+ * through its own endpoint, which checks the case type's form first).
233
+ * `{id}` in it is the task uuid. A refusal is returned with the
234
+ * server's message as-is and leaves the record as it was.
235
+ *
236
+ * @param {string} uuid The task uuid.
237
+ * @param {string} verb The verb.
238
+ * @param {{url?: string}} [options] An app route to post the verb to instead.
239
+ * @return {Promise<{ok: boolean, status: (number|null), record: (object|null), message: string}>} The outcome.
240
+ * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs
241
+ */
242
+ async runRecordVerb(uuid, verb, options = {}) {
243
+ const key = String(uuid || '').trim()
244
+ const name = String(verb || '').trim()
245
+ if (key === '' || name === '') {
246
+ return { ok: false, status: null, record: null, message: '' }
247
+ }
248
+ const route = typeof options.url === 'string' && options.url !== ''
249
+ ? options.url.replace('{id}', encodeURIComponent(key))
250
+ : `${FLOW_TASKS_URL}/${encodeURIComponent(key)}/${encodeURIComponent(name)}`
251
+ try {
252
+ const response = await axios.post(generateUrl(route), {})
253
+ return { ok: true, status: response.status || 200, record: await this.keepRecord(key, response.data), message: '' }
254
+ } catch (error) {
255
+ return { ok: false, status: error?.response?.status ?? null, record: null, message: serverMessage(error) }
256
+ }
257
+ },
258
+
259
+ /**
260
+ * Tick or untick one checklist item on a task read for a detail page
261
+ * (`PATCH /flow-tasks/{uuid}/checklist/{itemId}?checked=`). The engine
262
+ * answers with the whole task, which replaces the record, so a refused
263
+ * tick leaves the item as it was on screen.
264
+ *
265
+ * @param {string} uuid The task uuid.
266
+ * @param {string} itemId The checklist item id.
267
+ * @param {boolean} checked The new state.
268
+ * @return {Promise<{ok: boolean, status: (number|null), record: (object|null), message: string}>} The outcome.
269
+ * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source
270
+ */
271
+ async checkRecordItem(uuid, itemId, checked) {
272
+ const key = String(uuid || '').trim()
273
+ const item = String(itemId || '').trim()
274
+ if (key === '' || item === '') {
275
+ return { ok: false, status: null, record: null, message: '' }
276
+ }
277
+ try {
278
+ const response = await axios.patch(
279
+ generateUrl(`${FLOW_TASKS_URL}/${encodeURIComponent(key)}/checklist/${encodeURIComponent(item)}`),
280
+ null,
281
+ { params: { checked: checked === true ? 'true' : 'false' } },
282
+ )
283
+ return { ok: true, status: response.status || 200, record: await this.keepRecord(key, response.data), message: '' }
284
+ } catch (error) {
285
+ return { ok: false, status: error?.response?.status ?? null, record: null, message: serverMessage(error) }
286
+ }
287
+ },
288
+
289
+ /**
290
+ * Store the task a write answered with as the detail record. An answer
291
+ * that carries no task (an app route that returns only a status) is
292
+ * followed by a fresh read, so the page never shows the state from
293
+ * before the write.
294
+ *
295
+ * @param {string} key The task uuid.
296
+ * @param {unknown} data The response body.
297
+ * @return {Promise<object|null>} The task now held.
298
+ */
299
+ async keepRecord(key, data) {
300
+ const body = data && typeof data === 'object' ? data : null
301
+ const task = body && (body.results || body.task) && typeof (body.results || body.task) === 'object' && !Array.isArray(body.results || body.task)
302
+ ? (body.results || body.task)
303
+ : body
304
+ if (task && typeof task === 'object' && !Array.isArray(task) && String(task.uuid || '') === key) {
305
+ this.records = { ...this.records, [key]: task }
306
+ return task
307
+ }
308
+ return this.fetchRecord(key)
309
+ },
310
+
226
311
  /**
227
312
  * Load one inbox page.
228
313
  *
@@ -0,0 +1,183 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <CnDialog
7
+ :name="title || t('nextcloud-vue', 'Why are you opening this?')"
8
+ defaultWidth="confirm"
9
+ size="small"
10
+ :noClose="loading"
11
+ data-testid="cn-access-purpose-dialog"
12
+ @closing="$emit('cancel')">
13
+ <p class="cn-access-purpose__sentence">
14
+ {{ description || t('nextcloud-vue', 'This record holds special categories of personal data. Your purpose is recorded with your name and the time.') }}
15
+ </p>
16
+ <fieldset class="cn-access-purpose__choices">
17
+ <legend class="cn-access-purpose__legend">
18
+ {{ t('nextcloud-vue', 'Purpose') }}
19
+ </legend>
20
+ <NcCheckboxRadioSwitch
21
+ v-for="option in choices"
22
+ :key="option.value"
23
+ v-model="purpose"
24
+ type="radio"
25
+ name="cn-access-purpose"
26
+ :value="option.value"
27
+ :disabled="loading"
28
+ :data-testid="`cn-access-purpose-option-${option.value}`">
29
+ {{ option.label }}
30
+ </NcCheckboxRadioSwitch>
31
+ </fieldset>
32
+ <NcTextArea
33
+ v-if="purpose === OTHER"
34
+ v-model="note"
35
+ :label="t('nextcloud-vue', 'Describe your purpose')"
36
+ :maxlength="500"
37
+ :disabled="loading"
38
+ data-testid="cn-access-purpose-note" />
39
+ <NcNoteCard v-if="error" type="error" data-testid="cn-access-purpose-error">
40
+ {{ error }}
41
+ </NcNoteCard>
42
+
43
+ <template #actions>
44
+ <NcButton :disabled="loading" data-testid="cn-access-purpose-cancel" @click="$emit('cancel')">
45
+ {{ t('nextcloud-vue', 'Cancel') }}
46
+ </NcButton>
47
+ <NcButton
48
+ variant="primary"
49
+ :disabled="!complete || loading"
50
+ data-testid="cn-access-purpose-submit"
51
+ @click="submit">
52
+ <template v-if="loading" #icon>
53
+ <NcLoadingIcon :size="20" :name="t('nextcloud-vue', 'Loading …')" />
54
+ </template>
55
+ {{ t('nextcloud-vue', 'Open') }}
56
+ </NcButton>
57
+ </template>
58
+ </CnDialog>
59
+ </template>
60
+
61
+ <script>
62
+ import { translate as t } from '@nextcloud/l10n'
63
+ import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcTextArea } from '@nextcloud/vue'
64
+ import CnDialog from '../components/CnDialog/CnDialog.vue'
65
+
66
+ /** The value of the "Other" choice, which asks for a written purpose. */
67
+ const OTHER = 'other'
68
+
69
+ /**
70
+ * CnAccessPurposeDialog — asks why the reader opens a record before the page
71
+ * shows it (detail page `accessPurpose`). A short list of purposes plus
72
+ * "Other", which needs a written purpose. Emits the choice; the page records
73
+ * it and decides whether to open.
74
+ *
75
+ * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
76
+ */
77
+ export default {
78
+ name: 'CnAccessPurposeDialog',
79
+
80
+ components: { CnDialog, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcTextArea },
81
+
82
+ props: {
83
+ /** The dialog's title; a default question when empty. */
84
+ title: {
85
+ type: String,
86
+ default: '',
87
+ },
88
+
89
+ /** The sentence under the title; a default when empty. */
90
+ description: {
91
+ type: String,
92
+ default: '',
93
+ },
94
+
95
+ /**
96
+ * The purposes offered, already translated. "Other" is added at the end.
97
+ *
98
+ * @type {Array<{value: string, label: string}>}
99
+ */
100
+ options: {
101
+ type: Array,
102
+ default: () => [],
103
+ },
104
+
105
+ /** Whether the purpose is being recorded. */
106
+ loading: {
107
+ type: Boolean,
108
+ default: false,
109
+ },
110
+
111
+ /** Why the last attempt was refused, shown in the dialog. */
112
+ error: {
113
+ type: String,
114
+ default: '',
115
+ },
116
+ },
117
+
118
+ emits: [
119
+ /**
120
+ * The reader chose a purpose.
121
+ *
122
+ * @type {{purpose: string, note: string}}
123
+ */
124
+ 'submit',
125
+ /** The reader closed the dialog without a purpose. */
126
+ 'cancel',
127
+ ],
128
+
129
+ data() {
130
+ return { purpose: '', note: '', OTHER }
131
+ },
132
+
133
+ computed: {
134
+ /** @return {Array<{value: string, label: string}>} The offered purposes and "Other". */
135
+ choices() {
136
+ const own = (Array.isArray(this.options) ? this.options : [])
137
+ .filter((option) => option && typeof option.value === 'string' && option.value !== '' && option.value !== OTHER)
138
+ return [...own, { value: OTHER, label: t('nextcloud-vue', 'Other') }]
139
+ },
140
+
141
+ /** @return {boolean} Whether a purpose is chosen and, for "Other", written. */
142
+ complete() {
143
+ if (this.purpose === '') {
144
+ return false
145
+ }
146
+ return this.purpose !== OTHER || this.note.trim() !== ''
147
+ },
148
+ },
149
+
150
+ methods: {
151
+ t,
152
+
153
+ /**
154
+ * Emit the chosen purpose; the written note only goes with "Other".
155
+ *
156
+ * @return {void}
157
+ */
158
+ submit() {
159
+ if (!this.complete) {
160
+ return
161
+ }
162
+ this.$emit('submit', { purpose: this.purpose, note: this.purpose === OTHER ? this.note.trim() : '' })
163
+ },
164
+ },
165
+ }
166
+ </script>
167
+
168
+ <style scoped>
169
+ .cn-access-purpose__sentence {
170
+ margin: 0 0 12px;
171
+ }
172
+
173
+ .cn-access-purpose__choices {
174
+ margin: 0 0 12px;
175
+ padding: 0;
176
+ border: 0;
177
+ }
178
+
179
+ .cn-access-purpose__legend {
180
+ margin-bottom: 4px;
181
+ font-weight: 600;
182
+ }
183
+ </style>
package/src/index.js CHANGED
@@ -63,6 +63,7 @@ export {
63
63
  CnChartWidgetForm,
64
64
  CnChatPage,
65
65
  CnChecklistRunWidget,
66
+ CnChecklistWidget,
66
67
  CnChildRecordsField,
67
68
  CnChipsFieldWidget,
68
69
  CnChoiceCards,
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.136.0",
6
+ "version": "2.137.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -1363,6 +1363,11 @@
1363
1363
  "minLength": 1,
1364
1364
  "description": "Dot-path to the value on the record."
1365
1365
  },
1366
+ "values": {
1367
+ "type": "array",
1368
+ "items": { "type": "string" },
1369
+ "description": "With `field`: draw the pill only when the raw value is one of these (a priority pill for high and urgent only)."
1370
+ },
1366
1371
  "text": {
1367
1372
  "type": "string",
1368
1373
  "minLength": 1,
@@ -3202,6 +3207,23 @@
3202
3207
  "default": 1,
3203
3208
  "description": "open-form: how many columns the fields flow into. 2 pairs them up, which halves the scrolling on a longer form. Textareas and JSON editors still span the full width, and the layout collapses to one column below 700px. Widen the dialog with size: 'large' as well, or two columns are merely two narrow ones."
3204
3209
  },
3210
+ "endpoint": {
3211
+ "type": "object",
3212
+ "description": "open-form only: edit a record that is not a register object through an endpoint (an OpenRegister organisation). `url` is where the save goes (token-interpolated, app-relative or absolute), `method` is PATCH (default) or PUT, or POST to create a record with an empty form, `loadUrl` reads the record when it differs from `url`, `responsePath` plucks it from the answer. The fields are `formProperties`. Without `endpoint` the action is the schema-driven create form.",
3213
+ "properties": {
3214
+ "url": { "type": "string" },
3215
+ "method": { "type": "string", "enum": ["PATCH", "PUT", "POST"] },
3216
+ "loadUrl": { "type": "string" },
3217
+ "responsePath": { "type": "string" }
3218
+ },
3219
+ "required": ["url"],
3220
+ "additionalProperties": false
3221
+ },
3222
+ "formProperties": {
3223
+ "type": "object",
3224
+ "description": "open-form with `endpoint` only: the fields the form shows and sends, as JSON-schema properties (`{ \"name\": { \"type\": \"string\", \"title\": \"Name\" } }`).",
3225
+ "additionalProperties": { "type": "object" }
3226
+ },
3205
3227
  "handler": {
3206
3228
  "type": "string",
3207
3229
  "description": "Registry key for type: 'handler'. Resolved across both registries passed to CnAppRoot: a v2 `registry` entry of `kind: 'handler'` exposing the function as `.handler` (or `.fn`), a directly function-valued registry entry, or a function-valued entry in the deprecated `customComponents` map. The same resolution serves actions[], bulkActions[] and headerActions[]. Also the built-in dispatch verb when paired with `route` (handler: 'navigate' → $router.push to `route`)."
@@ -3255,8 +3277,8 @@
3255
3277
  },
3256
3278
  "method": {
3257
3279
  "type": "string",
3258
- "enum": ["POST", "PUT"],
3259
- "description": "api-call: HTTP method (default POST). toggle writeUrl: default PUT."
3280
+ "enum": ["POST", "PUT", "GET"],
3281
+ "description": "api-call: HTTP method (default POST); GET sends the resolved params as the query string, and with download: true downloads the file the endpoint answers (DqDoorlooptijd). toggle writeUrl: default PUT."
3260
3282
  },
3261
3283
  "payload": {
3262
3284
  "type": "object",
@@ -5506,6 +5528,30 @@
5506
5528
  }
5507
5529
  }
5508
5530
  },
5531
+ "accessPurpose": {
5532
+ "type": "object",
5533
+ "required": ["when", "url"],
5534
+ "additionalProperties": false,
5535
+ "description": "type='detail': ask why the reader opens the record before the page shows it, and record the answer (detail-access-purpose). `when` is a condition in the visibleWhen grammar on the record and its visibilityContext records; `options` the purposes offered (\"Other\" with a written purpose is always added); `url` the app endpoint that records the purpose (POST { purpose, note }, {objectId} filled in). Nothing of the record shows until the endpoint accepts; the same record is not asked again in the same browser session. Maps to CnDetailPage `accessPurpose`.",
5536
+ "properties": {
5537
+ "when": { "type": "object", "description": "The condition under which the page asks, e.g. { \"field\": \"@context.caseType.specialDataCategories\", \"op\": \"notEmpty\" }." },
5538
+ "options": {
5539
+ "type": "array",
5540
+ "items": {
5541
+ "type": "object",
5542
+ "required": ["value", "label"],
5543
+ "additionalProperties": false,
5544
+ "properties": {
5545
+ "value": { "type": "string", "minLength": 1 },
5546
+ "label": { "type": "string", "minLength": 1, "description": "i18n key." }
5547
+ }
5548
+ }
5549
+ },
5550
+ "url": { "type": "string", "minLength": 1, "description": "App endpoint that records the purpose; {objectId} is filled in." },
5551
+ "title": { "type": "string", "description": "i18n key of the dialog's title." },
5552
+ "description": { "type": "string", "description": "i18n key of the sentence under the title." }
5553
+ }
5554
+ },
5509
5555
  "mapConfig": {
5510
5556
  "type": "object",
5511
5557
  "additionalProperties": true,
@@ -466,7 +466,8 @@ async function executeApiCall(action, context) {
466
466
  console.warn('[dispatchAction] api-call is missing its url or a required token is unresolved — skipping.', action)
467
467
  return { ok: false, error: new Error('api-call blocked') }
468
468
  }
469
- const method = String(action.method || 'POST').toUpperCase() === 'PUT' ? 'put' : 'post'
469
+ const verb = String(action.method || 'POST').toUpperCase()
470
+ const method = verb === 'PUT' ? 'put' : (verb === 'GET' ? 'get' : 'post')
470
471
  const isDownload = action.download === true
471
472
  const [{ default: axios }, { generateUrl }, dialogs] = await Promise.all([
472
473
  import('@nextcloud/axios'),
@@ -478,9 +479,13 @@ async function executeApiCall(action, context) {
478
479
  // Only pass a third axios config arg for a download call — an
479
480
  // explicit `undefined` third argument would otherwise change the
480
481
  // call shape for every existing (non-download) api-call consumer/test.
481
- const res = isDownload
482
- ? await axios[method](target, body, { responseType: 'blob' })
483
- : await axios[method](target, body)
482
+ // A GET sends the resolved params as the query string: it reads, as a
483
+ // download of what the page shows (DqDoorlooptijd "Downloaden").
484
+ const res = method === 'get'
485
+ ? await axios.get(target, isDownload ? { params: body, responseType: 'blob' } : { params: body })
486
+ : (isDownload
487
+ ? await axios[method](target, body, { responseType: 'blob' })
488
+ : await axios[method](target, body))
484
489
  if (isDownload) {
485
490
  const filename = parseDispositionFilename(
486
491
  res && res.headers && res.headers['content-disposition'],
@@ -731,7 +736,7 @@ export async function postRunNode(action, context, config = {}) {
731
736
  * through generateUrl) or absolute. May interpolate the shared URL tokens
732
737
  * (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@config.<key>`, and the
733
738
  * `{objectId}` brace form).
734
- * @param {string} [action.method] "api-call" only: "POST" (default) | "PUT".
739
+ * @param {string} [action.method] "api-call" only: "POST" (default) | "PUT" | "GET" (the resolved params as the query string).
735
740
  * @param {object} [action.payload] "api-call" only: the JSON body — preferred over `params`.
736
741
  * Values resolve the shared `@`-token grammar RECURSIVELY at any nesting depth (objects
737
742
  * and arrays of objects, e.g. `{ dataRefs: [{ id: '@objectId' }] }`); optional (`…?`)
@@ -225,9 +225,10 @@ export function resolveNextStep(config, object, stageField = 'status') {
225
225
  *
226
226
  * A pill with `field` reads it off the record; one without `field` and with
227
227
  * `text` is a constant. A count pill (`countFrom`) is resolved by
228
- * {@link resolveCountPill} once its count is known.
228
+ * {@link resolveCountPill} once its count is known. `values` limits a field
229
+ * pill to those raw values: a priority pill drawn only for high and urgent.
229
230
  *
230
- * @param {{field?: string, text?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.
231
+ * @param {{field?: string, text?: string, colorMap?: object, labels?: object, variant?: string, values?: Array<string>}|null} config The pill config.
231
232
  * @param {object|null} object The record.
232
233
  * @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null when the field is empty.
233
234
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills
@@ -252,6 +253,9 @@ export function resolvePill(config, object) {
252
253
  if (raw === '') {
253
254
  return null
254
255
  }
256
+ if (Array.isArray(config.values) && !config.values.map(String).includes(raw)) {
257
+ return null
258
+ }
255
259
  const mapped = stageEntry(config.labels, raw)
256
260
  return {
257
261
  label: typeof mapped === 'string' && mapped !== '' ? mapped : raw,
@@ -98,6 +98,7 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
98
98
  'banner',
99
99
  'calendar',
100
100
  'chart',
101
+ 'checklist',
101
102
  'checklist-run',
102
103
  'chips-field',
103
104
  'choice-picker',