@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
@@ -18,23 +18,24 @@ import Plus from 'vue-material-design-icons/Plus.vue';
18
18
  import Refresh from 'vue-material-design-icons/Refresh.vue';
19
19
  import Restore from 'vue-material-design-icons/Restore.vue';
20
20
  import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue';
21
- import __sfc_main$k from '../../dialogs/CnRelationLinkModal.vue.js';
22
- import __sfc_main$j from '../../dialogs/CnWidgetStyleEditorModal.vue.js';
23
- import __sfc_main$1 from '../CnActionsMenu/CnActionsMenu.vue.js';
24
- import __sfc_main$h from '../CnBodySections/CnBodySections.vue.js';
25
- import __sfc_main$2 from '../CnBuildiqEditButton/CnBuildiqEditButton.vue.js';
26
- import __sfc_main$7 from '../CnDashboardGrid/CnDashboardGrid.vue.js';
27
- import __sfc_main$5 from '../CnDetailWidgetHost/CnDetailWidgetHost.vue.js';
28
- import __sfc_main$f from '../CnFollowToggle/CnFollowToggle.vue.js';
29
- import __sfc_main$6 from '../CnFormDialog/CnFormDialog.vue.js';
30
- import __sfc_main$9 from '../CnLifecycleActions/CnLifecycleActions.vue.js';
31
- import __sfc_main$3 from '../CnLockedBanner/CnLockedBanner.vue.js';
32
- import __sfc_main$4 from '../CnNextStepCard/CnNextStepCard.vue.js';
33
- import __sfc_main$g from '../CnRelatedCollections/CnRelatedCollections.vue.js';
34
- import __sfc_main$8 from '../CnSegmentedControl/CnSegmentedControl.vue.js';
35
- import __sfc_main$d from '../CnSummaryAggregates/CnSummaryAggregates.vue.js';
36
- import __sfc_main$i from '../CnTranslatedBadge/CnTranslatedBadge.vue.js';
37
- import __sfc_main$e from './CnDetailHeaderChips.vue.js';
21
+ import __sfc_main from '../../dialogs/CnAccessPurposeDialog.vue.js';
22
+ import __sfc_main$l from '../../dialogs/CnRelationLinkModal.vue.js';
23
+ import __sfc_main$k from '../../dialogs/CnWidgetStyleEditorModal.vue.js';
24
+ import __sfc_main$2 from '../CnActionsMenu/CnActionsMenu.vue.js';
25
+ import __sfc_main$i from '../CnBodySections/CnBodySections.vue.js';
26
+ import __sfc_main$3 from '../CnBuildiqEditButton/CnBuildiqEditButton.vue.js';
27
+ import __sfc_main$8 from '../CnDashboardGrid/CnDashboardGrid.vue.js';
28
+ import __sfc_main$6 from '../CnDetailWidgetHost/CnDetailWidgetHost.vue.js';
29
+ import __sfc_main$g from '../CnFollowToggle/CnFollowToggle.vue.js';
30
+ import __sfc_main$7 from '../CnFormDialog/CnFormDialog.vue.js';
31
+ import __sfc_main$a from '../CnLifecycleActions/CnLifecycleActions.vue.js';
32
+ import __sfc_main$4 from '../CnLockedBanner/CnLockedBanner.vue.js';
33
+ import __sfc_main$5 from '../CnNextStepCard/CnNextStepCard.vue.js';
34
+ import __sfc_main$h from '../CnRelatedCollections/CnRelatedCollections.vue.js';
35
+ import __sfc_main$9 from '../CnSegmentedControl/CnSegmentedControl.vue.js';
36
+ import __sfc_main$e from '../CnSummaryAggregates/CnSummaryAggregates.vue.js';
37
+ import __sfc_main$j from '../CnTranslatedBadge/CnTranslatedBadge.vue.js';
38
+ import __sfc_main$f from './CnDetailHeaderChips.vue.js';
38
39
  import { resolveRecordSource } from '../../composables/indexSources.js';
39
40
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js';
40
41
  import { normalizeLook } from '../../composables/useLook.js';
@@ -49,22 +50,23 @@ import { isAppInstalled } from '../../utils/appInstalled.js';
49
50
  import { compactLayoutRows } from '../../utils/dashboardPlacement.js';
50
51
  import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js';
51
52
  import { stageOf, normalisePinnedAction, stageEntry, MAX_QUICK_ACTIONS, groupMenuEntries, resolveNextStep, pillCountSource, resolveCountPill, resolvePill, pillLook } from '../../utils/detailActionModel.js';
52
- import { visibilityContextRefs, widgetConditionData, fetchVisibilityRows, detailWidgetVisible } from '../../utils/detailWidgetVisibility.js';
53
+ import { detailWidgetVisible, visibilityContextRefs, widgetConditionData, fetchVisibilityRows } from '../../utils/detailWidgetVisibility.js';
53
54
  import { reportBindingProblems } from '../../utils/diagnostics.js';
54
55
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js';
55
56
  import { firstFilled } from '../../utils/fillTemplate.js';
56
57
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js';
57
58
  import { resolveHeaderMeta, isActivityWidget } from '../../utils/headerMeta.js';
59
+ import { prefixUrl } from '../../utils/headers.js';
58
60
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js';
59
61
  import { setReadState } from '../../utils/recordInteractions.js';
60
62
  import { saveEdited } from '../../utils/saveEdited.js';
61
63
  import { slotRenders } from '../../utils/slotContent.js';
62
64
  import { readVisibleWhenPath, isLocallyDecidableVisibleWhen, evaluateVisibleWhen } from '../../utils/visibleWhen.js';
63
65
  import { isDataWidgetDef, isRelatedWidgetDef, isGeoWidgetDef, isContentOnlyWidgetDef, isCardWidgetDef, widgetContentOf, resolveRegistryRenderer, widgetTitleOf } from '../../utils/widgetDispatch.js';
64
- import __sfc_main$a from '../CnActionButtons/CnActionButtons.vue.js';
65
- import __sfc_main$b from '../CnBreadcrumbs/CnBreadcrumbs.vue.js';
66
- import __sfc_main from '../CnIcon/CnIcon.vue.js';
67
- import __sfc_main$c from '../CnStatusBadge/CnStatusBadge.vue.js';
66
+ import __sfc_main$b from '../CnActionButtons/CnActionButtons.vue.js';
67
+ import __sfc_main$c from '../CnBreadcrumbs/CnBreadcrumbs.vue.js';
68
+ import __sfc_main$1 from '../CnIcon/CnIcon.vue.js';
69
+ import __sfc_main$d from '../CnStatusBadge/CnStatusBadge.vue.js';
68
70
  import { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js';
69
71
  import '../CnWidgetGrid/registerDashboardWidgets.js';
70
72
  import { useObjectStore } from '../../store/useObjectStore.js';
@@ -140,6 +142,25 @@ const PAGE_REFRESH_CHANNEL = 'cn:page:refresh';
140
142
  /** Surfaces understood by the pluggable integration registry (AD-19). */
141
143
  const INTEGRATION_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'];
142
144
 
145
+ /** The session storage key that lists the records whose access purpose is recorded. */
146
+ const ACCESS_PURPOSE_SESSION_KEY = 'cn-access-purposes';
147
+
148
+ /**
149
+ * The records whose access purpose this browser session recorded. Empty when
150
+ * session storage cannot be read (a private window, blocked site data).
151
+ *
152
+ * @return {Array<string>} The record keys.
153
+ */
154
+ function readGrantedPurposes() {
155
+ try {
156
+ const raw = window.sessionStorage.getItem(ACCESS_PURPOSE_SESSION_KEY);
157
+ const list = raw ? JSON.parse(raw) : [];
158
+ return Array.isArray(list) ? list.filter((key) => typeof key === 'string') : []
159
+ } catch {
160
+ return []
161
+ }
162
+ }
163
+
143
164
  /**
144
165
  * CnDetailPage — Generic detail/overview page.
145
166
  *
@@ -216,6 +237,7 @@ var script = {
216
237
 
217
238
  components: {
218
239
  ChevronLeft,
240
+ CnAccessPurposeDialog: __sfc_main,
219
241
  ChevronRight,
220
242
  NcActionButton,
221
243
  NcActionCaption,
@@ -224,35 +246,35 @@ var script = {
224
246
  NcButton,
225
247
  NcEmptyContent,
226
248
  NcLoadingIcon,
227
- CnIcon: __sfc_main,
249
+ CnIcon: __sfc_main$1,
228
250
  AlertCircleOutline,
229
251
  ArrowLeft,
230
252
  FileQuestionOutline,
231
253
  InformationOutline,
232
254
  Refresh,
233
- CnActionsMenu: __sfc_main$1,
234
- CnBuildiqEditButton: __sfc_main$2,
235
- CnLockedBanner: __sfc_main$3,
236
- CnNextStepCard: __sfc_main$4,
237
- CnDetailWidgetHost: __sfc_main$5,
238
- CnFormDialog: __sfc_main$6,
239
- CnDashboardGrid: __sfc_main$7,
240
- CnSegmentedControl: __sfc_main$8,
255
+ CnActionsMenu: __sfc_main$2,
256
+ CnBuildiqEditButton: __sfc_main$3,
257
+ CnLockedBanner: __sfc_main$4,
258
+ CnNextStepCard: __sfc_main$5,
259
+ CnDetailWidgetHost: __sfc_main$6,
260
+ CnFormDialog: __sfc_main$7,
261
+ CnDashboardGrid: __sfc_main$8,
262
+ CnSegmentedControl: __sfc_main$9,
241
263
  ViewDashboardOutline,
242
- CnLifecycleActions: __sfc_main$9,
243
- CnActionButtons: __sfc_main$a,
244
- CnBreadcrumbs: __sfc_main$b,
245
- CnStatusBadge: __sfc_main$c,
246
- CnSummaryAggregates: __sfc_main$d,
247
- CnDetailHeaderChips: __sfc_main$e,
248
- CnFollowToggle: __sfc_main$f,
264
+ CnLifecycleActions: __sfc_main$a,
265
+ CnActionButtons: __sfc_main$b,
266
+ CnBreadcrumbs: __sfc_main$c,
267
+ CnStatusBadge: __sfc_main$d,
268
+ CnSummaryAggregates: __sfc_main$e,
269
+ CnDetailHeaderChips: __sfc_main$f,
270
+ CnFollowToggle: __sfc_main$g,
249
271
  DockRight,
250
272
  EyeOffOutline: EyeOffOutlineIcon,
251
- CnRelatedCollections: __sfc_main$g,
252
- CnBodySections: __sfc_main$h,
253
- CnTranslatedBadge: __sfc_main$i,
254
- CnWidgetStyleEditorModal: __sfc_main$j,
255
- CnRelationLinkModal: __sfc_main$k,
273
+ CnRelatedCollections: __sfc_main$h,
274
+ CnBodySections: __sfc_main$i,
275
+ CnTranslatedBadge: __sfc_main$j,
276
+ CnWidgetStyleEditorModal: __sfc_main$k,
277
+ CnRelationLinkModal: __sfc_main$l,
256
278
  Check,
257
279
  Cog,
258
280
  Plus,
@@ -313,6 +335,7 @@ var script = {
313
335
  const isBoard = computed(() => this.isBoard);
314
336
  const tabsLabel = computed(() => this.resolvedTabsLabel);
315
337
  const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows));
338
+ const canCheckItems = computed(() => this.recordCanCheckItems);
316
339
  return {
317
340
  // The page's own look reaches the tabs, the cards and the activity
318
341
  // below it, also when it came from the `look` prop and not the app.
@@ -323,6 +346,15 @@ var script = {
323
346
  },
324
347
  },
325
348
 
349
+ // The record source's checklist writes, for a `checklist` widget.
350
+ cnRecordChecklist: {
351
+ get canCheck() {
352
+ return canCheckItems.value
353
+ },
354
+
355
+ check: (itemId, checked) => this.checkSourceItem(itemId, checked),
356
+ },
357
+
326
358
  // The open body tab, reported by a `tabs` widget, so a side entry
327
359
  // with `tabs` shows only beside those tabs.
328
360
  cnDetailReportTab: (id) => {
@@ -720,6 +752,20 @@ var script = {
720
752
  default: '',
721
753
  },
722
754
 
755
+ /**
756
+ * App routes that replace a record source's own route for a verb, by
757
+ * verb id (manifest `config.verbEndpoints`), such as
758
+ * `{ "complete": "/apps/dossiq/api/case-tasks/{id}/complete" }` for an
759
+ * app that checks something of its own before the engine completes the
760
+ * task. `{id}` is the record id. Only read with `entitySource`.
761
+ *
762
+ * @type {object|null}
763
+ */
764
+ verbEndpoints: {
765
+ type: Object,
766
+ default: null,
767
+ },
768
+
723
769
  /**
724
770
  * OpenRegister register slug. Pair with `schema` to opt into the
725
771
  * schema-driven mode: the page fuses the two into an internal
@@ -1283,9 +1329,11 @@ var script = {
1283
1329
  * shows nothing at 0. With `resolve: { register, schema, labelField?,
1284
1330
  * colorField?, iconField? }` the pill shows the referenced record's
1285
1331
  * label, takes its tone from that record's colour (an NL Design System
1286
- * hue such as `red`, or a badge role) and shows its icon.
1332
+ * hue such as `red`, or a badge role) and shows its icon. `values`
1333
+ * draws a `field` pill only for those raw values (a priority pill for
1334
+ * `high` and `urgent` only).
1287
1335
  *
1288
- * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object}|null}
1336
+ * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object, values?: Array<string>}|null}
1289
1337
  */
1290
1338
  typePill: {
1291
1339
  type: Object,
@@ -1463,6 +1511,28 @@ var script = {
1463
1511
  default: () => ({}),
1464
1512
  },
1465
1513
 
1514
+ /**
1515
+ * Ask why the reader opens the record before the page shows it, and
1516
+ * record the answer (manifest `config.accessPurpose`). `{ when, options,
1517
+ * url, title?, description? }`: `when` is a condition in the
1518
+ * `visibleWhen` grammar on the record and its `visibilityContext`
1519
+ * records (DqSociaalDomein: the case type lists special categories of
1520
+ * personal data); `options` the purposes offered (`{ value, label }`,
1521
+ * "Other" with a written purpose is always added); `url` the app
1522
+ * endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}`
1523
+ * filled in). The page shows nothing of the record until the endpoint
1524
+ * accepts, and does not ask again for the same record in the same
1525
+ * browser session. A refusal keeps the record closed. `null` (the
1526
+ * default) asks nothing.
1527
+ *
1528
+ * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
1529
+ * @type {object|null}
1530
+ */
1531
+ accessPurpose: {
1532
+ type: Object,
1533
+ default: null,
1534
+ },
1535
+
1466
1536
  /**
1467
1537
  * Whether the current user administers this instance. Shows the
1468
1538
  * `adminOnly` header actions. `null` (the default) reads it from
@@ -1705,6 +1775,14 @@ var script = {
1705
1775
 
1706
1776
  data() {
1707
1777
  return {
1778
+ /** The session keys of the records whose access purpose is recorded. */
1779
+ accessPurposeGranted: readGrantedPurposes(),
1780
+ /** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */
1781
+ accessPurposeAsking: true,
1782
+ /** Whether a purpose is being recorded. */
1783
+ accessPurposeBusy: false,
1784
+ /** Why the last purpose was refused. */
1785
+ accessPurposeError: '',
1708
1786
  /** The id of the open body tab a `tabs` widget reported, '' before one did. */
1709
1787
  activeBodyTab: '',
1710
1788
  /**
@@ -1755,6 +1833,8 @@ var script = {
1755
1833
  * menu dispatches without reaching back into that component.
1756
1834
  */
1757
1835
  menuHeaderActions: [],
1836
+ /** Whether a record-source verb is in flight, so a double click cannot run it twice. */
1837
+ sourceVerbBusy: false,
1758
1838
  /** Lifecycle transitions CnLifecycleActions hands the Actions menu. */
1759
1839
  lifecycleMenuEntries: [],
1760
1840
  /** Whether the per-widget style/config editor modal is open. */
@@ -2206,7 +2286,64 @@ var script = {
2206
2286
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card
2207
2287
  */
2208
2288
  showsBody() {
2209
- return !this.objectNotFound && !this.showLoadingState && !this.error && !this.empty
2289
+ return !this.objectNotFound && !this.contentWithheld && !this.showLoadingState && !this.error && !this.empty
2290
+ },
2291
+
2292
+ /**
2293
+ * Whether the loaded record asks for a purpose before it opens
2294
+ * (`accessPurpose.when` holds for it). Undecided, and so withheld,
2295
+ * while the record or the records its condition reads are on their way.
2296
+ *
2297
+ * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
2298
+ * @return {boolean}
2299
+ */
2300
+ accessPurposeRequired() {
2301
+ const gate = this.accessPurpose;
2302
+ if (!gate || typeof gate !== 'object' || !gate.when || !this.widgetVisibilityData || this.visibilityRowsPending) {
2303
+ return false
2304
+ }
2305
+ return detailWidgetVisible({ visibleWhen: gate.when }, this.widgetVisibilityData)
2306
+ },
2307
+
2308
+ /**
2309
+ * Whether the page holds the record back: it asks for a purpose and
2310
+ * none is recorded for this record in this session yet, or it cannot
2311
+ * tell yet whether it asks.
2312
+ *
2313
+ * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
2314
+ * @return {boolean}
2315
+ */
2316
+ contentWithheld() {
2317
+ const gate = this.accessPurpose;
2318
+ if (!gate || typeof gate !== 'object' || !gate.when || this.objectNotFound) {
2319
+ return false
2320
+ }
2321
+ if (this.accessPurposeGranted.includes(this.accessPurposeKey)) {
2322
+ return false
2323
+ }
2324
+ const undecided = !this.widgetVisibilityData || this.visibilityRowsPending;
2325
+ return undecided || this.accessPurposeRequired
2326
+ },
2327
+
2328
+ /**
2329
+ * The session key of the purpose recorded for this record.
2330
+ *
2331
+ * @return {string}
2332
+ */
2333
+ accessPurposeKey() {
2334
+ return `cn-access-purpose:${this.resolvedObjectType || ''}:${this.objectId || ''}`
2335
+ },
2336
+
2337
+ /**
2338
+ * The purposes offered, through the app's translate.
2339
+ *
2340
+ * @return {Array<{value: string, label: string}>}
2341
+ */
2342
+ accessPurposeOptions() {
2343
+ const options = Array.isArray(this.accessPurpose?.options) ? this.accessPurpose.options : [];
2344
+ return options
2345
+ .filter((option) => option && typeof option.value === 'string' && option.value !== '')
2346
+ .map((option) => ({ value: option.value, label: this.cnTranslate(option.label || option.value) }))
2210
2347
  },
2211
2348
 
2212
2349
  /**
@@ -2695,7 +2832,70 @@ var script = {
2695
2832
  const inMenu = pinned.size > 0
2696
2833
  ? this.menuHeaderActions.filter((entry) => !pinned.has(entry.id))
2697
2834
  : this.menuHeaderActions;
2698
- return [...lifecycle, ...inMenu]
2835
+ return [...this.sourceVerbMenuEntries, ...lifecycle, ...inMenu]
2836
+ },
2837
+
2838
+ /**
2839
+ * The verbs the record source offers on the record in view, read off
2840
+ * the record's own state by the source. Empty without a source that
2841
+ * offers verbs, before the record is read, and when it was not found.
2842
+ *
2843
+ * @return {Array<object>} The verbs.
2844
+ * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs
2845
+ */
2846
+ sourceVerbs() {
2847
+ const source = this.recordSource;
2848
+ if (!source || typeof source.verbs !== 'function' || typeof source.runVerb !== 'function') {
2849
+ return []
2850
+ }
2851
+ if (this.objectNotFound || !this.currentObject) {
2852
+ return []
2853
+ }
2854
+ const verbs = source.verbs(this.currentObject);
2855
+ return Array.isArray(verbs) ? verbs.filter((verb) => verb && typeof verb.id === 'string' && verb.id !== '') : []
2856
+ },
2857
+
2858
+ /**
2859
+ * Whether the record source can tick the checklist of the record in
2860
+ * view: it writes items, and says this record can still be ticked.
2861
+ *
2862
+ * @return {boolean}
2863
+ * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source
2864
+ */
2865
+ recordCanCheckItems() {
2866
+ const source = this.recordSource;
2867
+ if (!source || typeof source.checkItem !== 'function' || !this.currentObject || this.objectNotFound) {
2868
+ return false
2869
+ }
2870
+ return typeof source.canCheckItems !== 'function' || source.canCheckItems(this.currentObject) === true
2871
+ },
2872
+
2873
+ /**
2874
+ * The source verbs drawn as header buttons.
2875
+ *
2876
+ * @return {Array<object>} The verbs.
2877
+ */
2878
+ sourceVerbButtons() {
2879
+ return this.sourceVerbs.filter((verb) => verb.menu !== true)
2880
+ },
2881
+
2882
+ /**
2883
+ * The source verbs listed in the Actions menu, as menu entries.
2884
+ *
2885
+ * @return {Array<object>} Menu-ready entries.
2886
+ */
2887
+ sourceVerbMenuEntries() {
2888
+ return this.sourceVerbs
2889
+ .filter((verb) => verb.menu === true)
2890
+ .map((verb) => ({
2891
+ id: `cn-source-verb-${verb.id}`,
2892
+ label: verb.label,
2893
+ testid: `cn-detail-page-verb-${verb.id}`,
2894
+ iconName: typeof verb.icon === 'string' ? verb.icon : '',
2895
+ disabled: this.sourceVerbBusy,
2896
+ pressed: null,
2897
+ run: () => this.runSourceVerb(verb),
2898
+ }))
2699
2899
  },
2700
2900
 
2701
2901
  /**
@@ -3685,6 +3885,16 @@ var script = {
3685
3885
  this.syncSidebarState();
3686
3886
  },
3687
3887
 
3888
+ contentWithheld() {
3889
+ this.syncSidebarState();
3890
+ },
3891
+
3892
+ /** Another record asks its own purpose: the dialog opens again for it. */
3893
+ accessPurposeKey() {
3894
+ this.accessPurposeAsking = true;
3895
+ this.accessPurposeError = '';
3896
+ },
3897
+
3688
3898
  // Deep, like `sidebarTabs` / `sidebarProps` below: the manifest editors
3689
3899
  // mutate `page.config.sidebar` IN PLACE, so the reference CnPageRenderer
3690
3900
  // forwards never changes. A shallow watcher missed every such edit —
@@ -3861,6 +4071,52 @@ var script = {
3861
4071
  },
3862
4072
 
3863
4073
  methods: {
4074
+ /**
4075
+ * A text of the access purpose gate through the app's translate, '' when unset.
4076
+ *
4077
+ * @param {string} key `title` or `description`.
4078
+ * @return {string}
4079
+ */
4080
+ accessPurposeText(key) {
4081
+ const text = this.accessPurpose && this.accessPurpose[key];
4082
+ return typeof text === 'string' && text !== '' ? this.cnTranslate(text) : ''
4083
+ },
4084
+
4085
+ /**
4086
+ * Record the reader's purpose with the app's endpoint, then open the
4087
+ * record and remember it for this browser session. A refusal (or a
4088
+ * failure to record) keeps the record closed and says why.
4089
+ *
4090
+ * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
4091
+ * @param {{purpose: string, note: string}} answer The chosen purpose.
4092
+ * @return {Promise<void>}
4093
+ */
4094
+ async recordAccessPurpose(answer) {
4095
+ const url = String(this.accessPurpose?.url || '').replace(/\{objectId\}/g, encodeURIComponent(String(this.objectId || '')));
4096
+ if (url === '') {
4097
+ this.accessPurposeError = translate('nextcloud-vue', 'This page cannot record a purpose, so the record stays closed.');
4098
+ return
4099
+ }
4100
+ this.accessPurposeBusy = true;
4101
+ this.accessPurposeError = '';
4102
+ try {
4103
+ const { default: axios } = await import('@nextcloud/axios');
4104
+ await axios.post(prefixUrl(url), { purpose: answer.purpose, note: answer.note });
4105
+ const key = this.accessPurposeKey;
4106
+ this.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])];
4107
+ try {
4108
+ window.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted));
4109
+ } catch {
4110
+ // Without session storage the purpose is asked again on the next visit.
4111
+ }
4112
+ } catch (error) {
4113
+ const data = error?.response?.data;
4114
+ this.accessPurposeError = (data && (data.message || data.error)) || translate('nextcloud-vue', 'Your purpose could not be recorded, so the record stays closed.');
4115
+ } finally {
4116
+ this.accessPurposeBusy = false;
4117
+ }
4118
+ },
4119
+
3864
4120
  /**
3865
4121
  * The widget definitions with each `tabs` widget's tabs narrowed and
3866
4122
  * ordered by `pickedTabIds`; the definitions as given without a pick.
@@ -4318,6 +4574,65 @@ var script = {
4318
4574
  this.$emit('transitioned', payload);
4319
4575
  },
4320
4576
 
4577
+ /**
4578
+ * Tick or untick one checklist item through the record source. The
4579
+ * source replaces its record with the answer; a refusal is shown with
4580
+ * the server's message and leaves the item as it was.
4581
+ *
4582
+ * @param {string} itemId The item id.
4583
+ * @param {boolean} checked The new state.
4584
+ * @return {Promise<boolean>} Whether the write was taken.
4585
+ * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source
4586
+ */
4587
+ async checkSourceItem(itemId, checked) {
4588
+ const source = this.recordSource;
4589
+ const objectId = this.objectId;
4590
+ if (!this.recordCanCheckItems || !objectId) {
4591
+ return false
4592
+ }
4593
+ const outcome = await source.checkItem(String(objectId), String(itemId), checked === true);
4594
+ if (!outcome || outcome.ok !== true) {
4595
+ const { showError } = await import('../../node_modules/@nextcloud/dialogs/dist/index.js');
4596
+ showError(String(outcome?.message || '').trim() || translate('nextcloud-vue', 'This could not be done. Try again.'));
4597
+ return false
4598
+ }
4599
+ return true
4600
+ },
4601
+
4602
+ /**
4603
+ * Run a record-source verb on the record in view. The source keeps its
4604
+ * record in step with the answer, so the header, pills and widgets
4605
+ * redraw from it. A refusal is shown with the server's own message,
4606
+ * which names the verb and the reason.
4607
+ *
4608
+ * @param {{id: string, label: string, successMessage?: string}} verb The verb.
4609
+ * @return {Promise<void>}
4610
+ * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs
4611
+ */
4612
+ async runSourceVerb(verb) {
4613
+ const source = this.recordSource;
4614
+ const objectId = this.objectId;
4615
+ if (!source || !objectId || this.sourceVerbBusy) {
4616
+ return
4617
+ }
4618
+ this.sourceVerbBusy = true;
4619
+ try {
4620
+ const url = this.verbEndpoints && typeof this.verbEndpoints[verb.id] === 'string' ? this.verbEndpoints[verb.id] : undefined;
4621
+ const outcome = await source.runVerb(String(objectId), verb.id, url ? { url } : {});
4622
+ const { showError, showSuccess } = await import('../../node_modules/@nextcloud/dialogs/dist/index.js');
4623
+ if (!outcome || outcome.ok !== true) {
4624
+ showError(String(outcome?.message || '').trim() || translate('nextcloud-vue', 'This could not be done. Try again.'));
4625
+ return
4626
+ }
4627
+ if (verb.successMessage) {
4628
+ showSuccess(verb.successMessage);
4629
+ }
4630
+ this.$emit('transitioned', { action: verb.id, to: String(outcome.record?.state ?? ''), object: outcome.record || null });
4631
+ } finally {
4632
+ this.sourceVerbBusy = false;
4633
+ }
4634
+ },
4635
+
4321
4636
  /**
4322
4637
  * Re-fetch the page's object after a lifecycle transition so the new state
4323
4638
  * (and any other server-side mutations the guard applied) render.
@@ -5094,15 +5409,24 @@ var script = {
5094
5409
 
5095
5410
  /**
5096
5411
  * Whether a side entry belongs beside the open body tab: an entry with
5097
- * `tabs` (tab ids) shows only while one of them is open; an entry
5098
- * without shows beside every tab. Edit mode shows them all.
5412
+ * `tabs` (tab ids) shows only while one of them is open; an entry with
5413
+ * `notTabs` stands aside while one of those is open (a tab that draws
5414
+ * the side column as its own, DqZaakAssistent); an entry with neither
5415
+ * shows beside every tab. Edit mode shows them all.
5099
5416
  *
5100
5417
  * @spec openspec/changes/detail-side-cards-per-tab/specs/detail-page/spec.md#requirement-a-side-card-can-follow-the-open-tab
5418
+ * @spec openspec/changes/side-card-not-on-tabs/specs/detail-page/spec.md#requirement-a-side-card-can-stand-aside-for-a-tab
5101
5419
  * @param {object} widget The side entry's definition.
5102
5420
  * @return {boolean}
5103
5421
  */
5104
5422
  sideWidgetOnTab(widget) {
5105
- if (this.editingBody || !Array.isArray(widget.tabs) || widget.tabs.length === 0) {
5423
+ if (this.editingBody) {
5424
+ return true
5425
+ }
5426
+ if (Array.isArray(widget.notTabs) && widget.notTabs.map((id) => String(id)).includes(this.activeBodyTab)) {
5427
+ return false
5428
+ }
5429
+ if (!Array.isArray(widget.tabs) || widget.tabs.length === 0) {
5106
5430
  return true
5107
5431
  }
5108
5432
  return widget.tabs.map((id) => String(id)).includes(this.activeBodyTab)
@@ -5538,7 +5862,8 @@ var script = {
5538
5862
  return
5539
5863
  }
5540
5864
  const r = this.resolvedSidebar;
5541
- const hasRecord = !this.objectNotFound && !this.awaitingObject;
5865
+ // A record held back for its access purpose shows nothing in the sidebar either.
5866
+ const hasRecord = !this.objectNotFound && !this.awaitingObject && !this.contentWithheld;
5542
5867
  if (this.sidebarActive && hasRecord && this.resolvedObjectType && this.objectId) {
5543
5868
  const merged = this.mergeSidebarSources(r);
5544
5869
  // Seed `open` only on the inactive→active edge (first activation