@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
package/l10n/en.json CHANGED
@@ -1932,7 +1932,25 @@
1932
1932
  "no": "no",
1933
1933
  "Recompute nomination": "Recompute nomination",
1934
1934
  "Recompute": "Recompute",
1935
- "Task deadline": "Deadline"
1935
+ "Task deadline": "Deadline",
1936
+ "The note could not be recorded. It is kept here; try again.": "The note could not be recorded. It is kept here; try again.",
1937
+ "This field is not available here.": "This field is not available here.",
1938
+ "filled in, not recorded yet": "filled in, not recorded yet",
1939
+ "Why are you opening this?": "Why are you opening this?",
1940
+ "This record holds special categories of personal data. Your purpose is recorded with your name and the time.": "This record holds special categories of personal data. Your purpose is recorded with your name and the time.",
1941
+ "Purpose": "Purpose",
1942
+ "Describe your purpose": "Describe your purpose",
1943
+ "State a purpose to open this": "State a purpose to open this",
1944
+ "This record holds special categories of personal data. It opens once your purpose is recorded.": "This record holds special categories of personal data. It opens once your purpose is recorded.",
1945
+ "State a purpose": "State a purpose",
1946
+ "This page cannot record a purpose, so the record stays closed.": "This page cannot record a purpose, so the record stays closed.",
1947
+ "Your purpose could not be recorded, so the record stays closed.": "Your purpose could not be recorded, so the record stays closed.",
1948
+ "Hand back": "Hand back",
1949
+ "Pick up": "Pick up",
1950
+ "Task completed": "Task completed",
1951
+ "This could not be done. Try again.": "This could not be done. Try again.",
1952
+ "{done} of {total} done": "{done} of {total} done",
1953
+ "Nothing to check off": "Nothing to check off"
1936
1954
  },
1937
1955
  "plurals": {
1938
1956
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2149,7 +2149,25 @@
2149
2149
  "Last run: {name}, {date}": "Laatst uitgevoerd: {name}, {date}",
2150
2150
  "This record has no macros": "Er zijn geen macro's voor dit item",
2151
2151
  "Running …": "Bezig …",
2152
- "Execute": "Uitvoeren"
2152
+ "Execute": "Uitvoeren",
2153
+ "The note could not be recorded. It is kept here; try again.": "De notitie kon niet worden vastgelegd. Ze blijft hier staan; probeer het opnieuw.",
2154
+ "This field is not available here.": "Dit veld is hier niet beschikbaar.",
2155
+ "filled in, not recorded yet": "ingevuld, nog niet vastgelegd",
2156
+ "Why are you opening this?": "Waarom open je dit?",
2157
+ "This record holds special categories of personal data. Your purpose is recorded with your name and the time.": "Hierin staan bijzondere persoonsgegevens. Je doel wordt vastgelegd met je naam en het tijdstip.",
2158
+ "Purpose": "Doel",
2159
+ "Describe your purpose": "Beschrijf je doel",
2160
+ "State a purpose to open this": "Geef een doel op om dit te openen",
2161
+ "This record holds special categories of personal data. It opens once your purpose is recorded.": "Hierin staan bijzondere persoonsgegevens. Het opent zodra je doel is vastgelegd.",
2162
+ "State a purpose": "Doel opgeven",
2163
+ "This page cannot record a purpose, so the record stays closed.": "Deze pagina kan geen doel vastleggen, dus het blijft gesloten.",
2164
+ "Your purpose could not be recorded, so the record stays closed.": "Je doel kon niet worden vastgelegd, dus het blijft gesloten.",
2165
+ "Hand back": "Teruggeven",
2166
+ "Pick up": "Oppakken",
2167
+ "Task completed": "Taak afgerond",
2168
+ "This could not be done. Try again.": "Dit lukte niet. Probeer het opnieuw.",
2169
+ "{done} of {total} done": "{done} van {total} klaar",
2170
+ "Nothing to check off": "Niets af te vinken"
2153
2171
  },
2154
2172
  "plurals": {
2155
2173
  "{count} member": [
@@ -2279,6 +2297,10 @@
2279
2297
  "%n passage found. Review each one.": [
2280
2298
  "%n passage gevonden. Beoordeel elke passage.",
2281
2299
  "%n passages gevonden. Beoordeel elke passage."
2300
+ ],
2301
+ "{count} change: {list}": [
2302
+ "{count} wijziging: {list}",
2303
+ "{count} wijzigingen: {list}"
2282
2304
  ]
2283
2305
  }
2284
2306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.115.0",
3
+ "version": "2.116.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": {
@@ -171,7 +171,7 @@
171
171
  v-if="formEntry && formSchema && !formEntry.advanced"
172
172
  ref="formDialog"
173
173
  :schema="formSchema"
174
- :item="null"
174
+ :item="formItem"
175
175
  :register="formRegister"
176
176
  :initialData="formInitialValues || {}"
177
177
  :dialogTitle="tr(formEntry.formTitle) || ''"
@@ -247,6 +247,11 @@ import { CnIcon } from '../CnIcon/index.js'
247
247
  * `excludeFields` / `fieldOverrides` narrow what the button asks for, which
248
248
  * is how one schema serves both a full editor and a quick-create button.
249
249
  * `advanced: true` swaps in the properties/JSON table for a power user.
250
+ * With `endpoint: { url, method?, loadUrl?, responsePath? }` and
251
+ * `formProperties` (JSON-schema properties) it edits a record that is not a
252
+ * register object instead: it reads the record from the endpoint, shows the
253
+ * declared fields filled in, and sends them back with PATCH (or PUT). With
254
+ * `method: "POST"` it creates one: an empty form, posted to the endpoint.
250
255
  * - **`toggle`** — a two-way state button: `GET`s `stateSource` on mount,
251
256
  * renders `labelOn` / `labelOff`, and on click `writes` the flipped
252
257
  * value OPTIMISTICALLY, reverting on failure.
@@ -428,6 +433,8 @@ export default {
428
433
  formEntry: null,
429
434
  /** The fetched schema object for the open-form dialog (null until loaded). */
430
435
  formSchema: null,
436
+ /** The record an endpoint form edits (null for a create form). */
437
+ formItem: null,
431
438
  /** The run-node action currently in flight / showing its dialog (null = closed). */
432
439
  runNodeEntry: null,
433
440
  /** The target node's declared config-form fields, from the describe GET. */
@@ -993,6 +1000,11 @@ export default {
993
1000
  async openForm(entry) {
994
1001
  this.formEntry = entry
995
1002
  this.formSchema = null
1003
+ this.formItem = null
1004
+ if (entry.endpoint && typeof entry.endpoint.url === 'string' && entry.endpoint.url !== '') {
1005
+ await this.openEndpointForm(entry)
1006
+ return
1007
+ }
996
1008
  const register = entry.register || this.objectCtx.register || ''
997
1009
  const schema = entry.schema || ''
998
1010
  if (!schema) {
@@ -1015,6 +1027,81 @@ export default {
1015
1027
  }
1016
1028
  },
1017
1029
 
1030
+ /**
1031
+ * Open an `open-form` action with an `endpoint` as an edit form over a
1032
+ * record that is not a register object (an OpenRegister organisation):
1033
+ * the fields are the action's `formProperties`, the values are read from
1034
+ * `endpoint.loadUrl` (else `endpoint.url`) at `endpoint.responsePath`,
1035
+ * and the save sends the changed fields to `endpoint.url`. A failed
1036
+ * read toasts and opens nothing.
1037
+ *
1038
+ * @param {object} entry The open-form action with `endpoint` and `formProperties`.
1039
+ * @return {Promise<void>}
1040
+ * @spec openspec/changes/open-form-endpoint/specs/open-form-endpoint/spec.md#requirement-an-open-form-action-can-edit-a-record-through-an-endpoint
1041
+ */
1042
+ async openEndpointForm(entry) {
1043
+ const properties = entry.formProperties && typeof entry.formProperties === 'object' && !Array.isArray(entry.formProperties) ? entry.formProperties : {}
1044
+ if (String(entry.endpoint.method || '').toUpperCase() === 'POST') {
1045
+ this.formSchema = { type: 'object', properties }
1046
+ return
1047
+ }
1048
+ const loadUrl = entry.endpoint.loadUrl || entry.endpoint.url
1049
+ const record = await fetchEndpointSource({ url: loadUrl, responsePath: entry.endpoint.responsePath || '' }, this.tokenCtx, { force: true })
1050
+ .catch(() => null)
1051
+ if (!record || typeof record !== 'object') {
1052
+ const { showError } = await import('@nextcloud/dialogs')
1053
+ if (typeof showError === 'function') {
1054
+ showError(t('nextcloud-vue', 'Could not open the form.'))
1055
+ }
1056
+ this.formEntry = null
1057
+ return
1058
+ }
1059
+ this.formItem = record
1060
+ this.formSchema = { type: 'object', properties }
1061
+ },
1062
+
1063
+ /**
1064
+ * Save an endpoint form: send the declared fields to `endpoint.url`
1065
+ * with `endpoint.method` (`PATCH` by default, or `PUT`), then toast and
1066
+ * refresh the page as an object save does.
1067
+ *
1068
+ * @param {object} entry The open-form action.
1069
+ * @param {object} formData The form's values.
1070
+ * @return {Promise<void>}
1071
+ * @spec openspec/changes/open-form-endpoint/specs/open-form-endpoint/spec.md#requirement-an-open-form-action-can-edit-a-record-through-an-endpoint
1072
+ */
1073
+ async saveEndpointForm(entry, formData) {
1074
+ const dialog = this.$refs.formDialog
1075
+ const keys = Object.keys(this.formSchema?.properties || {})
1076
+ const body = Object.fromEntries(keys.filter((key) => key in formData).map((key) => [key, formData[key]]))
1077
+ const verb = String(entry.endpoint.method || 'PATCH').toUpperCase()
1078
+ const method = verb === 'PUT' ? 'put' : (verb === 'POST' ? 'post' : 'patch')
1079
+ try {
1080
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
1081
+ import('@nextcloud/axios'),
1082
+ import('@nextcloud/router'),
1083
+ ])
1084
+ const url = interpolateActionTarget(entry.endpoint.url, this.tokenCtx)
1085
+ const response = await axios[method](/^https?:\/\//i.test(url) ? url : generateUrl(url), body)
1086
+ if (dialog && typeof dialog.setResult === 'function') {
1087
+ dialog.setResult({ success: true })
1088
+ }
1089
+ const { showSuccess } = await import('@nextcloud/dialogs')
1090
+ if (typeof showSuccess === 'function') {
1091
+ showSuccess(this.tr(entry.successMessage) || t('nextcloud-vue', 'Saved.'))
1092
+ }
1093
+ const { emit } = await import('@nextcloud/event-bus')
1094
+ emit('cn:page:refresh', {})
1095
+ this.$emit('created', response && response.data)
1096
+ } catch (error) {
1097
+ const serverMessage = error && error.response && error.response.data
1098
+ && (error.response.data.error || error.response.data.message)
1099
+ if (dialog && typeof dialog.setResult === 'function') {
1100
+ dialog.setResult({ error: serverMessage || this.tr(entry.errorMessage) || t('nextcloud-vue', 'Save failed.') })
1101
+ }
1102
+ }
1103
+ },
1104
+
1018
1105
  /**
1019
1106
  * Open (or skip) the `run-node` config dialog: GET the target node's
1020
1107
  * describe endpoint (`or-flow-run-node`'s `GET .../{flowId}/{nodeId}/run`),
@@ -1125,6 +1212,10 @@ export default {
1125
1212
  */
1126
1213
  async onFormConfirm(formData, dynamic = null) {
1127
1214
  const entry = this.formEntry
1215
+ if (entry && entry.endpoint) {
1216
+ await this.saveEndpointForm(entry, formData)
1217
+ return
1218
+ }
1128
1219
  const dialog = this.$refs.formDialog
1129
1220
  const register = this.formRegister
1130
1221
  const schema = (entry && entry.schema) || ''
@@ -1267,6 +1358,7 @@ export default {
1267
1358
  closeForm() {
1268
1359
  this.formEntry = null
1269
1360
  this.formSchema = null
1361
+ this.formItem = null
1270
1362
  },
1271
1363
  },
1272
1364
  }
@@ -0,0 +1,206 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-checklist-widget" data-testid="cn-checklist-widget">
8
+ <p v-if="items.length === 0" class="cn-checklist-widget__muted" data-testid="cn-checklist-empty">
9
+ {{ emptyLabel }}
10
+ </p>
11
+ <template v-else>
12
+ <p class="cn-checklist-widget__summary" data-testid="cn-checklist-summary">
13
+ {{ summary }}
14
+ </p>
15
+ <ul class="cn-checklist-widget__items">
16
+ <li v-for="item in items" :key="item.id" class="cn-checklist-widget__item">
17
+ <NcCheckboxRadioSwitch
18
+ :modelValue="item.checked"
19
+ :disabled="!writable || busy"
20
+ :data-testid="`cn-checklist-item-${item.id}`"
21
+ @update:modelValue="toggle(item, $event)">
22
+ {{ item.label }}
23
+ </NcCheckboxRadioSwitch>
24
+ <p v-if="item.description" class="cn-checklist-widget__description">
25
+ {{ item.description }}
26
+ </p>
27
+ </li>
28
+ </ul>
29
+ </template>
30
+ </div>
31
+ </template>
32
+
33
+ <script>
34
+ import { translate as t } from '@nextcloud/l10n'
35
+ import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
36
+ import { readPath } from '../../utils/readPath.js'
37
+
38
+ /**
39
+ * CnChecklistWidget — the checklist a record carries, ticked in place.
40
+ *
41
+ * The items are read off the record in view (`content.path`, `checklist` by
42
+ * default), each `{ id, label, description, checked }` unless `content`
43
+ * names other fields. A tick is written through the detail page's record
44
+ * source (`cnRecordChecklist`, provided by CnDetailPage when its source can
45
+ * check items, as the `tasks` source does through the task engine). The box
46
+ * follows the record the write answers with, so a refused tick leaves the
47
+ * item as it was. Without a source that can write, or on a record the source
48
+ * says can no longer be ticked, the list is read-only.
49
+ *
50
+ * An item without an id is left out: the write addresses an item by its id,
51
+ * and a box that cannot be saved would look saved.
52
+ */
53
+ export default {
54
+ name: 'CnChecklistWidget',
55
+
56
+ components: { NcCheckboxRadioSwitch },
57
+
58
+ inject: {
59
+ cnTranslate: { default: () => (key) => key },
60
+ cnObjectContext: { default: null },
61
+ cnRecordChecklist: { default: null },
62
+ },
63
+
64
+ props: {
65
+ /**
66
+ * The widget configuration: `path` (where the items sit on the record,
67
+ * default `checklist`), `idField` / `labelField` / `descriptionField` /
68
+ * `checkedField` (default `id`, `label`, `description`, `checked`),
69
+ * `emptyText` (shown when the record carries no items).
70
+ */
71
+ content: {
72
+ type: Object,
73
+ default: () => ({}),
74
+ },
75
+
76
+ /** The record in view, as the detail page hands it to a widget. */
77
+ objectData: {
78
+ type: Object,
79
+ default: null,
80
+ },
81
+ },
82
+
83
+ data() {
84
+ return {
85
+ /** Whether a tick is being written, so two cannot cross. */
86
+ busy: false,
87
+ }
88
+ },
89
+
90
+ computed: {
91
+ /**
92
+ * The record: the prop, else the page's injected context.
93
+ *
94
+ * @return {object|null}
95
+ */
96
+ record() {
97
+ if (this.objectData) {
98
+ return this.objectData
99
+ }
100
+ const context = this.cnObjectContext
101
+ const value = context && typeof context === 'object' && 'value' in context ? context.value : context
102
+ return value?.object || null
103
+ },
104
+
105
+ /**
106
+ * The items, normalised, without the ones that have no id.
107
+ *
108
+ * @return {Array<{id: string, label: string, description: string, checked: boolean}>}
109
+ */
110
+ items() {
111
+ const content = this.content || {}
112
+ const list = this.record ? readPath(this.record, content.path || 'checklist') : null
113
+ if (!Array.isArray(list)) {
114
+ return []
115
+ }
116
+ return list
117
+ .filter((item) => item && typeof item === 'object' && String(item[content.idField || 'id'] ?? '').trim() !== '')
118
+ .map((item) => {
119
+ const id = String(item[content.idField || 'id']).trim()
120
+ return {
121
+ id,
122
+ label: String(item[content.labelField || 'label'] ?? '').trim() || id,
123
+ description: String(item[content.descriptionField || 'description'] ?? '').trim(),
124
+ checked: item[content.checkedField || 'checked'] === true,
125
+ }
126
+ })
127
+ },
128
+
129
+ /**
130
+ * "2 of 4 done".
131
+ *
132
+ * @return {string}
133
+ */
134
+ summary() {
135
+ const done = this.items.filter((item) => item.checked).length
136
+ return t('nextcloud-vue', '{done} of {total} done', { done, total: this.items.length })
137
+ },
138
+
139
+ /**
140
+ * Whether a tick can be written for this record.
141
+ *
142
+ * @return {boolean}
143
+ */
144
+ writable() {
145
+ return Boolean(this.cnRecordChecklist && this.cnRecordChecklist.canCheck === true)
146
+ },
147
+
148
+ /** @return {string} The empty text, through the app's catalogue. */
149
+ emptyLabel() {
150
+ const text = this.content?.emptyText
151
+ return typeof text === 'string' && text !== '' ? this.cnTranslate(text) : t('nextcloud-vue', 'Nothing to check off')
152
+ },
153
+ },
154
+
155
+ methods: {
156
+ /**
157
+ * Write one tick through the page's record source.
158
+ *
159
+ * @param {{id: string}} item The item.
160
+ * @param {boolean} checked The new state.
161
+ * @return {Promise<void>}
162
+ */
163
+ async toggle(item, checked) {
164
+ if (!this.writable || this.busy) {
165
+ return
166
+ }
167
+ this.busy = true
168
+ try {
169
+ await this.cnRecordChecklist.check(item.id, checked === true)
170
+ } finally {
171
+ this.busy = false
172
+ }
173
+ },
174
+ },
175
+ }
176
+ </script>
177
+
178
+ <style scoped>
179
+ .cn-checklist-widget {
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 8px;
183
+ min-width: 0;
184
+ }
185
+
186
+ .cn-checklist-widget__summary,
187
+ .cn-checklist-widget__muted {
188
+ margin: 0;
189
+ color: var(--color-text-maxcontrast);
190
+ }
191
+
192
+ .cn-checklist-widget__items {
193
+ margin: 0;
194
+ padding: 0;
195
+ list-style: none;
196
+ }
197
+
198
+ .cn-checklist-widget__item {
199
+ min-width: 0;
200
+ }
201
+
202
+ .cn-checklist-widget__description {
203
+ margin: 0 0 4px 36px;
204
+ color: var(--color-text-maxcontrast);
205
+ }
206
+ </style>
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnChecklistWidget — self-registers the `checklist` widget type (the
6
+ * checklist a record carries, ticked in place through the page's record
7
+ * source) into the shared dashboardWidgetRegistry at module load. Renderer
8
+ * only: the items come from the record, so the Add-widget picker does not
9
+ * offer it.
10
+ */
11
+
12
+ import CnChecklistWidget from './CnChecklistWidget.vue'
13
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
14
+
15
+ registerDashboardWidget('checklist', {
16
+ renderer: CnChecklistWidget,
17
+ form: null,
18
+ defaultContent: {
19
+ path: 'checklist',
20
+ },
21
+ displayName: 'Checklist',
22
+ icon: 'CheckboxMarkedOutline',
23
+ surfaces: ['detail-page'],
24
+ })
25
+
26
+ export { CnChecklistWidget }
27
+ export default CnChecklistWidget
@@ -37,7 +37,7 @@
37
37
  <CnHeaderTrail
38
38
  :breadcrumb="breadcrumb"
39
39
  :current="title ? resolvedTitle : ''"
40
- :meta="headerMeta ? effectiveTranslate(headerMeta) : ''" />
40
+ :meta="resolvedHeaderMeta" />
41
41
  <!-- `dateRange.placement: "description"`: the segmented period
42
42
  group ends the description line ("Week 41 · alle teams ·
43
43
  [Deze week | Dit kwartaal | Dit jaar]"). -->
@@ -2178,6 +2178,31 @@ export default {
2178
2178
  * @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
2179
2179
  * @return {string}
2180
2180
  */
2181
+ /**
2182
+ * `headerMeta` through the host translate, with `{today}` written out as
2183
+ * today's date (DqDagAfsluiten "Dag afsluiten, woensdag 7 oktober").
2184
+ *
2185
+ * @spec openspec/changes/record-list-notes/specs/record-list-notes/spec.md#requirement-a-dashboard-header-meta-can-name-today
2186
+ * @return {string}
2187
+ */
2188
+ resolvedHeaderMeta() {
2189
+ if (!this.headerMeta) {
2190
+ return ''
2191
+ }
2192
+ const meta = this.effectiveTranslate(this.headerMeta)
2193
+ if (!meta.includes('{today}')) {
2194
+ return meta
2195
+ }
2196
+ const options = { weekday: 'long', day: 'numeric', month: 'long' }
2197
+ let today
2198
+ try {
2199
+ today = new Intl.DateTimeFormat(getCanonicalLocale() || undefined, options).format(new Date())
2200
+ } catch {
2201
+ today = new Intl.DateTimeFormat(undefined, options).format(new Date())
2202
+ }
2203
+ return meta.split('{today}').join(today)
2204
+ },
2205
+
2181
2206
  subtitleText() {
2182
2207
  const description = this.description ? this.resolvedDescription : ''
2183
2208
  const dateOnly = this.greeting === 'date'
@@ -428,6 +428,8 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
428
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
+ | `verbEndpoints` | Object | `null` | App routes that replace a record source's own route for a verb, by verb id (manifest `config.verbEndpoints`), such as `{ "complete": "/apps/dossiq/api/case-tasks/{id}/complete" }` for an app that checks something of its own before the engine completes the task. `{id}` is the record id. Only read with `entitySource`. |
432
+ | `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label }`; "Other" with a written purpose is always added); `url` is the app endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}` filled in). Nothing of the record shows until the endpoint accepts, and the same record is not asked again in the same browser session. A refusal keeps the record closed. `null` asks nothing. |
431
433
  | `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
432
434
 
433
435
  ## The record as a place