@conduction/nextcloud-vue 2.114.0 → 2.115.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 (118) hide show
  1. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue.js +1 -1
  2. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js +1 -1
  3. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +6 -2
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  10. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +2 -1
  11. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  12. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +19 -2
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -3
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +2 -2
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  22. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +37 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/rowsToCsv.js +90 -0
  26. package/dist/esm/components/CnIndexPage/rowsToCsv.js.map +1 -0
  27. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +9 -0
  28. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js.map +1 -0
  29. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +161 -0
  30. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -0
  31. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +97 -0
  32. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -0
  33. package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue.js +9 -0
  34. package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue.js.map +1 -0
  35. package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue2.js +63 -0
  36. package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue2.js.map +1 -0
  37. package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue3.js +54 -0
  38. package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue3.js.map +1 -0
  39. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  40. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +15 -9
  41. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +55 -63
  43. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +1 -1
  45. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +2 -1
  46. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  47. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  48. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  49. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +13 -4
  50. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  51. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +2 -1
  52. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  53. package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
  54. package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
  55. package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
  56. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  57. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
  58. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +1 -1
  60. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
  62. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +2 -2
  64. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +32 -20
  66. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  67. package/dist/esm/composables/indexSources.js +43 -11
  68. package/dist/esm/composables/indexSources.js.map +1 -1
  69. package/dist/esm/composables/useTaskInboxStore.js +41 -0
  70. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  71. package/dist/esm/index.js +1 -0
  72. package/dist/esm/index.js.map +1 -1
  73. package/dist/esm/l10n/en.json.js +4 -1
  74. package/dist/esm/l10n/en.json.js.map +1 -1
  75. package/dist/esm/l10n/nl.json.js +9 -1
  76. package/dist/esm/l10n/nl.json.js.map +1 -1
  77. package/dist/esm/library-style.js +1 -1
  78. package/dist/esm/nextcloud-vue.css +482 -330
  79. package/dist/esm/utils/contentHeaderLink.js +37 -1
  80. package/dist/esm/utils/contentHeaderLink.js.map +1 -1
  81. package/dist/esm/utils/declaredMacros.js +31 -0
  82. package/dist/esm/utils/declaredMacros.js.map +1 -0
  83. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  84. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  85. package/dist/nextcloud-vue.cjs.js +2673 -1977
  86. package/dist/nextcloud-vue.cjs.js.map +2 -2
  87. package/dist/nextcloud-vue.css +482 -330
  88. package/l10n/en.json +4 -1
  89. package/l10n/nl.json +9 -1
  90. package/package.json +1 -1
  91. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  92. package/src/components/CnCellRenderer/CnCellRenderer.vue +6 -2
  93. package/src/components/CnChoiceCards/CnChoiceCards.vue +2 -1
  94. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -3
  95. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  96. package/src/components/CnIndexPage/CnIndexPage.vue +37 -1
  97. package/src/components/CnIndexPage/rowsToCsv.js +87 -0
  98. package/src/components/CnMacrosWidget/CnMacrosWidget.md +30 -0
  99. package/src/components/CnMacrosWidget/CnMacrosWidget.vue +266 -0
  100. package/src/components/CnMacrosWidget/index.js +6 -0
  101. package/src/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue +104 -0
  102. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +53 -38
  103. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +2 -1
  104. package/src/components/CnSetupWizard/CnSetupWizard.vue +28 -4
  105. package/src/components/CnStepper/CnStepper.vue +2 -1
  106. package/src/components/CnTabsWidget/CnTabsWidget.vue +11 -6
  107. package/src/components/CnTasksWidget/CnTasksWidget.vue +2 -2
  108. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  109. package/src/components/index.js +1 -0
  110. package/src/composables/indexSources.js +43 -11
  111. package/src/composables/useTaskInboxStore.js +41 -0
  112. package/src/css/look-board-detail.css +4 -3
  113. package/src/css/look-board-index.css +62 -0
  114. package/src/css/look-board.css +20 -0
  115. package/src/index.js +1 -0
  116. package/src/utils/contentHeaderLink.js +35 -0
  117. package/src/utils/declaredMacros.js +28 -0
  118. package/src/utils/libraryWidgetKeys.js +1 -0
@@ -0,0 +1,104 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <CnDialog
7
+ :name="tr('Recompute the nomination')"
8
+ width="confirm"
9
+ :noClose="busy"
10
+ data-testid="cn-retention-recompute-dialog"
11
+ @closing="$emit('close')">
12
+ <NcTextField
13
+ :modelValue="reason"
14
+ :label="tr('Why are you recomputing this?')"
15
+ data-testid="cn-retention-reason"
16
+ @update:modelValue="(value) => (reason = String(value ?? ''))" />
17
+ <p class="cn-retention-recompute__muted">
18
+ {{ recordedAgainst }}
19
+ </p>
20
+ <template #actions>
21
+ <NcButton :disabled="busy" data-testid="cn-retention-recompute-cancel" @click="$emit('close')">
22
+ {{ tr('Cancel') }}
23
+ </NcButton>
24
+ <NcButton
25
+ variant="primary"
26
+ :disabled="busy || reason.trim() === ''"
27
+ data-testid="cn-retention-recompute"
28
+ @click="$emit('confirm', reason.trim())">
29
+ {{ tr('Recompute') }}
30
+ </NcButton>
31
+ </template>
32
+ </CnDialog>
33
+ </template>
34
+
35
+ <script>
36
+ import { translate as t } from '@nextcloud/l10n'
37
+ import { NcButton, NcTextField } from '@nextcloud/vue'
38
+ import { CnDialog } from '../CnDialog/index.js'
39
+
40
+ /**
41
+ * CnRetentionRecomputeDialog — asks why a record's archival nomination is
42
+ * recomputed, the reason OpenRegister records with it. The retention widget
43
+ * opens it from its card header (board DqZaakArchivering, "Nominatie
44
+ * herberekenen") and posts the recompute itself.
45
+ *
46
+ * @event close The dialog is closed without recomputing.
47
+ * @event confirm The person confirmed, with the reason (trimmed, never empty).
48
+ * @spec openspec/changes/retention-recompute-header/specs/retention-recompute-header/spec.md#requirement-recomputing-the-nomination-starts-in-the-card-header-and-asks-for-a-reason
49
+ */
50
+ export default {
51
+ name: 'CnRetentionRecomputeDialog',
52
+
53
+ components: { CnDialog, NcButton, NcTextField },
54
+
55
+ inject: {
56
+ /** The host app's translate (CnAppRoot `cnTranslate`). */
57
+ cnTranslate: { default: () => (key) => key },
58
+ },
59
+
60
+ props: {
61
+ /** The user the recompute is recorded against. */
62
+ user: { type: String, default: '' },
63
+ /** Whether the recompute is being posted. */
64
+ busy: { type: Boolean, default: false },
65
+ },
66
+
67
+ emits: ['close', 'confirm'],
68
+
69
+ data() {
70
+ return { reason: '' }
71
+ },
72
+
73
+ computed: {
74
+ /**
75
+ * The line naming who the recompute is recorded against.
76
+ *
77
+ * @return {string} The line.
78
+ */
79
+ recordedAgainst() {
80
+ return t('nextcloud-vue', 'This is recorded against {user}.', { user: this.user })
81
+ },
82
+ },
83
+
84
+ methods: {
85
+ /**
86
+ * A library string in the reader's language.
87
+ *
88
+ * @param {string} key The English text.
89
+ * @return {string} The text.
90
+ */
91
+ tr(key) {
92
+ return t('nextcloud-vue', key)
93
+ },
94
+ },
95
+ }
96
+ </script>
97
+
98
+ <style scoped>
99
+ .cn-retention-recompute__muted {
100
+ margin: 0;
101
+ color: var(--color-text-maxcontrast);
102
+ font-size: 14px;
103
+ }
104
+ </style>
@@ -5,13 +5,35 @@
5
5
  <template>
6
6
  <div class="cn-retention" data-testid="cn-retention">
7
7
  <header class="cn-retention__head">
8
- <h3 class="cn-retention__title">
9
- {{ text(content.title) || t('nextcloud-vue', 'Archiving') }}
10
- </h3>
11
- <p class="cn-retention__muted">
12
- {{ text(content.subtitle) || t('nextcloud-vue', 'What happens to this record once it is closed') }}
13
- </p>
8
+ <div class="cn-retention__head-text">
9
+ <h3 class="cn-retention__title">
10
+ {{ text(content.title) || t('nextcloud-vue', 'Archiving') }}
11
+ </h3>
12
+ <p class="cn-retention__muted">
13
+ {{ text(content.subtitle) || t('nextcloud-vue', 'What happens to this record once it is closed') }}
14
+ </p>
15
+ </div>
16
+ <!-- Recompute starts in the header (board DqZaakArchivering); the
17
+ dialog asks for the reason OpenRegister records with it. -->
18
+ <NcButton
19
+ v-if="content.recompute !== false && !loading && !error"
20
+ :disabled="!mayRecompute || busy"
21
+ :title="mayRecompute ? undefined : t('nextcloud-vue', 'Recomputing needs the archivist role')"
22
+ :aria-description="mayRecompute ? undefined : t('nextcloud-vue', 'Recomputing needs the archivist role')"
23
+ :data-testid="mayRecompute ? 'cn-retention-recompute-open' : 'cn-retention-recompute-denied'"
24
+ @click="recomputeOpen = true">
25
+ <template #icon>
26
+ <Refresh :size="20" />
27
+ </template>
28
+ {{ t('nextcloud-vue', 'Recompute nomination') }}
29
+ </NcButton>
14
30
  </header>
31
+ <CnRetentionRecomputeDialog
32
+ v-if="recomputeOpen"
33
+ :user="currentUid"
34
+ :busy="busy"
35
+ @close="recomputeOpen = false"
36
+ @confirm="recompute" />
15
37
 
16
38
  <NcLoadingIcon v-if="loading" :size="24" />
17
39
  <NcEmptyContent v-else-if="error"
@@ -65,23 +87,6 @@
65
87
  </p>
66
88
  </div>
67
89
 
68
- <div v-if="content.recompute !== false" class="cn-retention__recompute">
69
- <NcButton v-if="!mayRecompute" disabled data-testid="cn-retention-recompute-denied">
70
- {{ t('nextcloud-vue', 'Recomputing needs the archivist role') }}
71
- </NcButton>
72
- <template v-else>
73
- <NcTextField :modelValue="reason"
74
- :label="t('nextcloud-vue', 'Why are you recomputing this?')"
75
- data-testid="cn-retention-reason"
76
- @update:modelValue="(v) => (reason = String(v ?? ''))" />
77
- <p class="cn-retention__muted">
78
- {{ t('nextcloud-vue', 'This is recorded against {user}.', { user: currentUid }) }}
79
- </p>
80
- <NcButton :disabled="busy || reason.trim() === ''" data-testid="cn-retention-recompute" @click="recompute">
81
- {{ t('nextcloud-vue', 'Recompute the nomination') }}
82
- </NcButton>
83
- </template>
84
- </div>
85
90
  </template>
86
91
  </div>
87
92
  </template>
@@ -91,7 +96,9 @@ import { getCurrentUser } from '@nextcloud/auth'
91
96
  import axios from '@nextcloud/axios'
92
97
  import { translate as t } from '@nextcloud/l10n'
93
98
  import { generateUrl } from '@nextcloud/router'
94
- import { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField } from '@nextcloud/vue'
99
+ import { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
100
+ import Refresh from 'vue-material-design-icons/Refresh.vue'
101
+ import CnRetentionRecomputeDialog from './CnRetentionRecomputeDialog.vue'
95
102
  import { readPath } from '../../utils/readPath.js'
96
103
 
97
104
  /**
@@ -120,7 +127,7 @@ import { readPath } from '../../utils/readPath.js'
120
127
  export default {
121
128
  name: 'CnRetentionWidget',
122
129
 
123
- components: { NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcTextField },
130
+ components: { CnRetentionRecomputeDialog, NcButton, NcEmptyContent, NcLoadingIcon, NcNoteCard, Refresh },
124
131
 
125
132
  inject: {
126
133
  /** The host app's translate (CnAppRoot `cnTranslate`). */
@@ -176,7 +183,8 @@ export default {
176
183
  loading: true,
177
184
  error: '',
178
185
  record: null,
179
- reason: '',
186
+ /** Whether the recompute dialog is open. */
187
+ recomputeOpen: false,
180
188
  busy: false,
181
189
  }
182
190
  },
@@ -338,19 +346,22 @@ export default {
338
346
  },
339
347
 
340
348
  /**
341
- * Ask OpenRegister to derive the nomination again, then read it back.
349
+ * Ask OpenRegister to derive the nomination again with the reason the
350
+ * dialog asked for, then read it back.
342
351
  *
343
- * @spec openspec/changes/retention-widget/specs/retention-widget/spec.md#requirement-a-record-shows-its-stored-retention
352
+ * @spec openspec/changes/retention-recompute-header/specs/retention-recompute-header/spec.md#requirement-recomputing-the-nomination-starts-in-the-card-header-and-asks-for-a-reason
353
+ * @param {string} reason Why it is recomputed.
344
354
  * @return {Promise<void>}
345
355
  */
346
- async recompute() {
347
- if (this.busy || this.reason.trim() === '') {
356
+ async recompute(reason) {
357
+ const why = String(reason ?? '').trim()
358
+ if (this.busy || why === '') {
348
359
  return
349
360
  }
350
361
  this.busy = true
351
362
  try {
352
- await axios.post(generateUrl(`/apps/openregister/api/archival/objects/${encodeURIComponent(this.id)}/nomination/recompute`), { reason: this.reason.trim() })
353
- this.reason = ''
363
+ await axios.post(generateUrl(`/apps/openregister/api/archival/objects/${encodeURIComponent(this.id)}/nomination/recompute`), { reason: why })
364
+ this.recomputeOpen = false
354
365
  await this.load()
355
366
  } catch (failure) {
356
367
  const body = failure && failure.response && failure.response.data
@@ -372,8 +383,18 @@ export default {
372
383
 
373
384
  .cn-retention__head {
374
385
  display: flex;
386
+ flex-wrap: wrap;
387
+ align-items: flex-start;
388
+ justify-content: space-between;
389
+ gap: 10px 16px;
390
+ }
391
+
392
+ .cn-retention__head-text {
393
+ display: flex;
394
+ flex: 1 1 260px;
375
395
  flex-direction: column;
376
396
  gap: 2px;
397
+ min-width: 0;
377
398
  }
378
399
 
379
400
  .cn-retention__title {
@@ -404,10 +425,4 @@ export default {
404
425
  font-weight: 600;
405
426
  }
406
427
 
407
- .cn-retention__recompute {
408
- display: flex;
409
- flex-direction: column;
410
- gap: 8px;
411
- max-width: 480px;
412
- }
413
428
  </style>
@@ -81,7 +81,8 @@ export default {
81
81
  * @type {string|number|boolean|null}
82
82
  */
83
83
  modelValue: {
84
- type: [Boolean, String, Number],
84
+ // eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an option value of '' must stay a string
85
+ type: [String, Number, Boolean],
85
86
  default: null,
86
87
  },
87
88
 
@@ -78,8 +78,11 @@
78
78
  <NcNoteCard v-else-if="step.body" type="info">
79
79
  {{ stepBody(step) }}
80
80
  </NcNoteCard>
81
+ <!-- In the board look the step's title is the dialog's heading,
82
+ so the cards' legend is kept for assistive tech only. -->
81
83
  <CnChoiceCards
82
84
  v-if="isCardChoice(step)"
85
+ :class="{ 'cn-setup-step__choices--titled': isBoard }"
83
86
  :layout="step.display === 'rows' ? 'rows' : 'cards'"
84
87
  :label="stepTitle(step)"
85
88
  :options="optionsFor(step)"
@@ -1134,7 +1137,9 @@ export default {
1134
1137
  /**
1135
1138
  * CnWizardDialog `validate` hook — intercepts Next/Submit to persist
1136
1139
  * the active step before advancing. `choice`/`config-fields` steps
1137
- * POST their value via the setup contract; required `choice` steps
1140
+ * POST their value via the setup contract (a `config-fields` step only
1141
+ * the fields the person filled in, and nothing when they filled in
1142
+ * none); required `choice` steps
1138
1143
  * block advance until a value is picked; required `run-action` steps
1139
1144
  * block until the action has succeeded. Returns `true` to advance or
1140
1145
  * an error string to surface above the step and block navigation.
@@ -1168,11 +1173,18 @@ export default {
1168
1173
  }
1169
1174
  }
1170
1175
  if (step.type === 'config-fields') {
1171
- try {
1172
- const patch = {}
1173
- for (const field of this.fieldsFor(step)) {
1176
+ // Only what the person filled in is written: a field left alone
1177
+ // keeps what is stored, and a step left alone writes nothing.
1178
+ const patch = {}
1179
+ for (const field of this.fieldsFor(step)) {
1180
+ if (Object.hasOwn(this.configModel, field.key)) {
1174
1181
  patch[field.key] = this.configModel[field.key]
1175
1182
  }
1183
+ }
1184
+ if (Object.keys(patch).length === 0) {
1185
+ return true
1186
+ }
1187
+ try {
1176
1188
  await this.saveConfig(patch)
1177
1189
  this.localDone[step.id] = true
1178
1190
  return true
@@ -1457,6 +1469,18 @@ export default {
1457
1469
  line-height: 1.5;
1458
1470
  }
1459
1471
 
1472
+ .cn-setup-step__choices--titled :deep(.cn-choice-cards__legend) {
1473
+ position: absolute;
1474
+ width: 1px;
1475
+ height: 1px;
1476
+ margin: -1px;
1477
+ padding: 0;
1478
+ overflow: hidden;
1479
+ clip-path: inset(50%);
1480
+ white-space: nowrap;
1481
+ border: 0;
1482
+ }
1483
+
1460
1484
  .cn-setup-step__note {
1461
1485
  margin: 0;
1462
1486
  font-size: 13px;
@@ -230,13 +230,14 @@ button.cn-stepper__step {
230
230
  }
231
231
 
232
232
  /* The board look: one row, 28px circles, the label beside the circle. */
233
+ /* The board leaves 20px between the stepper and the step's body (DqInstellen). */
233
234
  .cn-stepper--board {
234
235
  flex-wrap: wrap;
235
236
  align-items: center;
236
237
  justify-content: flex-start;
237
238
  gap: 8px;
238
239
  padding: 0;
239
- margin: 0;
240
+ margin: 0 0 20px;
240
241
  }
241
242
 
242
243
  .cn-stepper--board .cn-stepper__item {
@@ -111,7 +111,7 @@
111
111
  <!-- A stack (`widgetIds`): every widget is a titled card; the
112
112
  first joins the open tab, the next ones stand below it. -->
113
113
  <div
114
- v-if="entry.widget && entry.stack.length"
114
+ v-if="entry.widget && entry.isStack"
115
115
  class="cn-tabs-widget__stack"
116
116
  :class="{ 'cn-tabs-widget__stack--grid': !!entry.grid }"
117
117
  data-testid="cn-tabs-widget-stack">
@@ -211,7 +211,7 @@ import CnIcon from '../CnIcon/CnIcon.vue'
211
211
  import CnTab from '../CnTabs/CnTab.vue'
212
212
  import CnTabs from '../CnTabs/CnTabs.vue'
213
213
  import { normalizeLook } from '../../composables/useLook.js'
214
- import { headerLinkTarget, widgetHeaderLink } from '../../utils/contentHeaderLink.js'
214
+ import { headerLinkTarget, resolveHeaderLink, widgetHeaderLink } from '../../utils/contentHeaderLink.js'
215
215
  import { resolveTabCount } from '../../utils/detailActionModel.js'
216
216
  import { detailWidgetVisible } from '../../utils/detailWidgetVisibility.js'
217
217
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
@@ -653,10 +653,10 @@ export default {
653
653
  // A stack (`widgetIds`) resolves every id; one that names no
654
654
  // widget is skipped. The first that resolves is the panel's.
655
655
  const ids = this.tabWidgetIds(tab)
656
- const widgets = ids.length > 1
657
- ? ids.map((id) => this.availableWidgets.find((w) => w && w.id === id) || null)
658
- .filter((w) => w && !this.hiddenByWorkspace(w) && !this.hiddenByRecord(w))
656
+ const named = ids.length > 1
657
+ ? ids.map((id) => this.availableWidgets.find((w) => w && w.id === id) || null).filter(Boolean)
659
658
  : []
659
+ const widgets = named.filter((w) => !this.hiddenByWorkspace(w) && !this.hiddenByRecord(w))
660
660
  const widget = ids.length > 1
661
661
  ? (widgets[0] || null)
662
662
  : (this.availableWidgets.find((w) => w && w.id === ids[0]) || null)
@@ -687,6 +687,11 @@ export default {
687
687
  icon: (tab && tab.icon) || widget?.icon || '',
688
688
  widget,
689
689
  stack: widgets.slice(1),
690
+ // A stack of two or more widgets stays a stack of titled cards
691
+ // when conditions leave one of them showing (DqSociaalDomein
692
+ // "De melding"): the record decides which cards show, not
693
+ // whether the panel is a card.
694
+ isStack: named.length > 1,
690
695
  grid: this.stackGrid(tab, widgets),
691
696
  count,
692
697
  overflow,
@@ -762,7 +767,7 @@ export default {
762
767
  * @return {object|null}
763
768
  */
764
769
  activeHeaderLink() {
765
- return widgetHeaderLink(this.activeTab?.widget)
770
+ return resolveHeaderLink(widgetHeaderLink(this.activeTab?.widget), { objectId: this.objectId, object: this.objectData })
766
771
  },
767
772
 
768
773
  /**
@@ -30,7 +30,7 @@
30
30
  <NcTextField
31
31
  :modelValue="newDue"
32
32
  type="date"
33
- :label="tr('Deadline')"
33
+ :label="tr('Task deadline')"
34
34
  data-testid="cn-tasks-widget-create-due"
35
35
  @update:modelValue="(value) => (newDue = String(value ?? ''))" />
36
36
  <NcButton
@@ -54,7 +54,7 @@
54
54
  {{ tr('Handler') }}
55
55
  </th>
56
56
  <th scope="col">
57
- {{ tr('Deadline') }}
57
+ {{ tr('Task deadline') }}
58
58
  </th>
59
59
  <th scope="col">
60
60
  {{ tr('Status') }}
@@ -22,6 +22,7 @@ import CnConversationWidget from '../CnConversationThread/CnConversationWidget.v
22
22
  import CnCountdownWidget from '../CnCountdownWidget/CnCountdownWidget.vue'
23
23
  import CnCountdownWidgetForm from '../CnCountdownWidgetForm/CnCountdownWidgetForm.vue'
24
24
  import CnDocumentReviewWidget from '../CnDocumentReviewList/CnDocumentReviewWidget.vue'
25
+ import CnMacrosWidget from '../CnMacrosWidget/CnMacrosWidget.vue'
25
26
  import CnMapWidget from '../CnMapWidget/CnMapWidget.vue'
26
27
  import CnMapWidgetForm from '../CnMapWidgetForm/CnMapWidgetForm.vue'
27
28
  import CnMarkdownWidget2 from '../CnMarkdownWidget/CnMarkdownWidget.vue'
@@ -345,6 +346,17 @@ registerDashboardWidget('ai-chat', {
345
346
  ownsTitle: true,
346
347
  })
347
348
 
349
+ // The macros the record's schema declares, each run by a button
350
+ // (DqZaakAssistent "Macro's").
351
+ registerDashboardWidget('macros', {
352
+ renderer: CnMacrosWidget,
353
+ form: null,
354
+ defaultContent: { description: '', emptyText: '' },
355
+ displayName: 'Macros',
356
+ icon: 'PlayBoxMultipleOutline',
357
+ surfaces: ['detail-page'],
358
+ })
359
+
348
360
  registerDashboardWidget('conversation', {
349
361
  renderer: CnConversationWidget,
350
362
  form: null,
@@ -256,6 +256,7 @@ export { CnChoicePickerWidget } from './CnChoicePickerWidget/index.js'
256
256
  export { CnRecordListWidget } from './CnRecordListWidget/index.js'
257
257
  export { CnBannerWidget } from './CnBannerWidget/index.js'
258
258
  export { default as CnConversationWidget } from './CnConversationThread/CnConversationWidget.vue'
259
+ export { CnMacrosWidget } from './CnMacrosWidget/index.js'
259
260
  export { default as CnDocumentReviewWidget } from './CnDocumentReviewList/CnDocumentReviewWidget.vue'
260
261
  export { default as CnNextStepWidget } from './CnNextStepCard/CnNextStepWidget.vue'
261
262
  export { default as CnAuditTrailWidget } from './CnAuditTrailWidget/CnAuditTrailWidget.vue'
@@ -497,6 +497,23 @@ function tasksSource() {
497
497
  [priorityLabels.urgent]: 'error',
498
498
  }
499
499
 
500
+ /**
501
+ * A task with the display fields a list row and a detail page read. Keyed
502
+ * on its uuid, the identity of the task contract.
503
+ *
504
+ * @param {object} task The task as the engine returned it.
505
+ * @return {object} The task with `id`, `title` and the labels.
506
+ */
507
+ const taskRow = (task) => ({
508
+ ...task,
509
+ id: task.uuid,
510
+ title: task.displayTitle || task.title || task.uuid,
511
+ subjectLabel: (task.subject && task.subject.title) || '',
512
+ stateLabel: stateLabels[task.state] || String(task.state || ''),
513
+ priorityLabel: priorityLabels[task.priority] || String(task.priority || ''),
514
+ dueLabel: taskDueLabel(task),
515
+ })
516
+
500
517
  return {
501
518
  store,
502
519
 
@@ -513,17 +530,32 @@ function tasksSource() {
513
530
  /**
514
531
  * @return {Array<object>} The rows, with the display fields resolved.
515
532
  */
516
- rows: () => (store.tasks || []).map((task) => ({
517
- ...task,
518
- // CnIndexPage keys rows on `id` by default; the row contract's
519
- // identity is the uuid.
520
- id: task.uuid,
521
- title: task.displayTitle || task.title || task.uuid,
522
- subjectLabel: (task.subject && task.subject.title) || '',
523
- stateLabel: stateLabels[task.state] || String(task.state || ''),
524
- priorityLabel: priorityLabels[task.priority] || String(task.priority || ''),
525
- dueLabel: taskDueLabel(task),
526
- })),
533
+ rows: () => (store.tasks || []).map(taskRow),
534
+
535
+ /**
536
+ * Read one task for a detail page (`type: "detail"` with
537
+ * `entitySource: "tasks"`).
538
+ *
539
+ * @param {string} id The task uuid.
540
+ * @return {Promise<object|null>} The task, or null when it could not be read.
541
+ * @spec openspec/changes/tasks-record-source/specs/tasks-record-source/spec.md#requirement-the-tasks-source-reads-one-task
542
+ */
543
+ loadRecord: (id) => store.fetchRecord(id),
544
+
545
+ /**
546
+ * @param {string} id The task uuid.
547
+ * @return {object|null} The task read for this id, with the row's display fields, or null.
548
+ */
549
+ record: (id) => {
550
+ const task = store.records?.[String(id)]
551
+ return task ? taskRow(task) : null
552
+ },
553
+
554
+ /**
555
+ * @param {string} id The task uuid.
556
+ * @return {number|null} The HTTP status of a failed read of this id, or null.
557
+ */
558
+ recordError: (id) => store.recordErrors?.[String(id)] ?? null,
527
559
 
528
560
  /**
529
561
  * @return {boolean} Whether a load is in flight.
@@ -118,9 +118,50 @@ export const useTaskInboxStore = defineStore('cnTaskInbox', {
118
118
  loading: false,
119
119
  /** @type {string|null} The last load failure, for the console trail. */
120
120
  error: null,
121
+ /** @type {Object<string, object>} Single tasks read for a detail page, keyed on their uuid. */
122
+ records: {},
123
+ /** @type {Object<string, number|null>} The HTTP status of a failed single read, keyed on the uuid. */
124
+ recordErrors: {},
121
125
  }),
122
126
 
123
127
  actions: {
128
+ /**
129
+ * Read one task into `records`, for a `type: "detail"` page on the
130
+ * `tasks` source. A refusal (403) or a missing task (404) leaves no
131
+ * record and keeps the status in `recordErrors`, so the page can tell
132
+ * "not found" from "still loading". The engine decides what the caller
133
+ * may read; nothing here widens that.
134
+ *
135
+ * @param {string} uuid The task uuid.
136
+ * @return {Promise<object|null>} The task, or null when it could not be read.
137
+ * @spec openspec/changes/tasks-record-source/specs/tasks-record-source/spec.md#requirement-the-tasks-source-reads-one-task
138
+ */
139
+ async fetchRecord(uuid) {
140
+ const key = String(uuid || '').trim()
141
+ if (key === '') {
142
+ return null
143
+ }
144
+ try {
145
+ const response = await axios.get(generateUrl(`${FLOW_TASKS_URL}/${encodeURIComponent(key)}`))
146
+ const data = response.data
147
+ const task = data && typeof data === 'object' && data.results && !Array.isArray(data.results) ? data.results : data
148
+ const { [key]: dropped, ...errors } = this.recordErrors
149
+ this.recordErrors = errors
150
+ if (!task || typeof task !== 'object' || Array.isArray(task)) {
151
+ return null
152
+ }
153
+ this.records = { ...this.records, [key]: task }
154
+ return task
155
+ } catch (error) {
156
+ const { [key]: dropped, ...records } = this.records
157
+ this.records = records
158
+ this.recordErrors = { ...this.recordErrors, [key]: error?.response?.status ?? null }
159
+ // eslint-disable-next-line no-console
160
+ console.error(`[useTaskInboxStore] Loading task "${key}" failed`, error)
161
+ return null
162
+ }
163
+ },
164
+
124
165
  /**
125
166
  * Read one inbox page WITHOUT touching the store's state, so a surface
126
167
  * that lists a different slice (the Tasks tab of one record) cannot
@@ -932,9 +932,10 @@
932
932
  /* ---- The archiving card (CnRetentionWidget, DqZaakArchivering) ----------- */
933
933
  /* The board titles it 17px at 1.2 with a 14px muted line 3px under it, and
934
934
  draws each fact as a 13px label over a 15px value at 500 above a hairline.
935
- The inset is the board card's own, from the panel around it.
935
+ "Recompute nomination" sits at the header's end. The inset is the board
936
+ card's own, from the panel around it.
936
937
  @spec openspec/changes/board-retention-card/specs/board-retention-card/spec.md#requirement-the-archiving-card-is-drawn-as-the-board-draws-it */
937
- .cn-look-board .cn-retention .cn-retention__head {
938
+ .cn-look-board .cn-retention .cn-retention__head-text {
938
939
  gap: 3px;
939
940
  }
940
941
 
@@ -943,7 +944,7 @@
943
944
  line-height: 1.2;
944
945
  }
945
946
 
946
- .cn-look-board .cn-retention .cn-retention__head .cn-retention__muted {
947
+ .cn-look-board .cn-retention .cn-retention__head-text .cn-retention__muted {
947
948
  font-size: 14px;
948
949
  }
949
950
 
@@ -43,6 +43,19 @@
43
43
  color: var(--color-text-maxcontrast);
44
44
  }
45
45
 
46
+ /* The breadcrumb row stays under the title, in the title's column, with a
47
+ count line below it. Unplaced, the trail took the next free cell: beside
48
+ the count line, under the header buttons (DqZaaktypen).
49
+ @spec openspec/changes/index-count-subtitle-keeps-crumb-row/specs/index-count-subtitle-keeps-crumb-row/spec.md#requirement-the-breadcrumb-row-stays-under-the-title */
50
+ .cn-look-board .cn-index-page .cn-page-header .cn-header-trail {
51
+ grid-column: 1;
52
+ grid-row: 2;
53
+ }
54
+
55
+ .cn-look-board .cn-index-page .cn-page-header .cn-header-trail ~ .cn-page-header__description {
56
+ grid-row: 3;
57
+ }
58
+
46
59
  .cn-look-board .cn-index-page .cn-page-header .cn-index-page__header-buttons {
47
60
  grid-column: 2;
48
61
  grid-row: 1;
@@ -1285,6 +1298,55 @@
1285
1298
  font-weight: 600;
1286
1299
  }
1287
1300
 
1301
+ /* The folder rows as the board draws them (DqAfdelingen), like a tree menu's
1302
+ folder rows: 8px 10px, an 8px corner, 14px at 400 without a folder icon,
1303
+ each level 18px further in inside the row so the highlight spans the pane,
1304
+ the open folder on the primary tint in the deep primary at 700. The weight
1305
+ goes through the theme's button token too: a row is a button, and the
1306
+ nldesign theme sets every button's weight with `!important`.
1307
+ @spec openspec/changes/board-folder-pane-rows/specs/board-folder-pane-rows/spec.md#requirement-a-board-folder-pane-draws-the-boards-folder-rows */
1308
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__children {
1309
+ padding-inline-start: 0;
1310
+ }
1311
+
1312
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__item,
1313
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-sidebar__all {
1314
+ --nldesign-component-button-font-weight: 400;
1315
+ min-height: 36px;
1316
+ margin: 0;
1317
+ padding: 8px 10px;
1318
+ border-radius: 8px;
1319
+ color: var(--color-main-text);
1320
+ font-size: 14px;
1321
+ font-weight: 400;
1322
+ }
1323
+
1324
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__children .cn-folder-tree__item {
1325
+ padding-inline-start: 28px;
1326
+ }
1327
+
1328
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__children .cn-folder-tree__children .cn-folder-tree__item {
1329
+ padding-inline-start: 46px;
1330
+ }
1331
+
1332
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__children .cn-folder-tree__children .cn-folder-tree__children .cn-folder-tree__item {
1333
+ padding-inline-start: 64px;
1334
+ }
1335
+
1336
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__item > :not(.cn-folder-tree__name):not(.cn-folder-tree__count),
1337
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-sidebar__all > :not(.cn-folder-sidebar__all-label) {
1338
+ display: none;
1339
+ }
1340
+
1341
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__item--active,
1342
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-sidebar__all--active {
1343
+ --nldesign-component-button-font-weight: 700;
1344
+ --nldesign-color-on-surface: var(--color-primary-element-light-text);
1345
+ background-color: var(--color-primary-element-light);
1346
+ color: var(--color-primary-element-light-text);
1347
+ font-weight: 700;
1348
+ }
1349
+
1288
1350
  /* The selected folder's heading above the list: a 20px title over a soft line. */
1289
1351
  .cn-look-board .cn-index-page__scope-heading {
1290
1352
  margin-bottom: 16px;