@conduction/nextcloud-vue 2.108.0 → 2.109.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 (155) hide show
  1. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js +10 -0
  2. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
  3. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
  4. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
  6. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
  8. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
  9. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
  13. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
  14. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
  15. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
  16. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
  17. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
  18. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
  19. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
  20. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
  21. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
  22. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
  23. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
  24. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  26. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
  27. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  28. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  29. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
  34. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
  35. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
  36. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
  37. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
  38. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
  45. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
  46. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
  47. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
  48. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
  49. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
  50. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
  51. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
  52. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
  53. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
  54. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
  55. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
  56. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
  57. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
  58. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
  59. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
  60. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
  61. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
  64. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
  65. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
  66. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
  67. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
  68. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  69. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  70. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
  71. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  72. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
  73. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
  74. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +246 -0
  75. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
  76. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
  77. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
  78. package/dist/esm/index.js +12 -0
  79. package/dist/esm/index.js.map +1 -1
  80. package/dist/esm/l10n/en.json.js +106 -1
  81. package/dist/esm/l10n/en.json.js.map +1 -1
  82. package/dist/esm/l10n/nl.json.js +105 -1
  83. package/dist/esm/l10n/nl.json.js.map +1 -1
  84. package/dist/esm/library-style.js +1 -1
  85. package/dist/esm/nextcloud-vue.css +495 -61
  86. package/dist/esm/utils/actionsDispatcher.js +6 -1
  87. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  88. package/dist/esm/utils/builtInModals.js +3 -1
  89. package/dist/esm/utils/builtInModals.js.map +1 -1
  90. package/dist/esm/utils/duplicateWarning.js +62 -0
  91. package/dist/esm/utils/duplicateWarning.js.map +1 -0
  92. package/dist/esm/utils/errors.js +4 -0
  93. package/dist/esm/utils/errors.js.map +1 -1
  94. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  95. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  96. package/dist/esm/utils/openDuplicateWarning.js +39 -0
  97. package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
  98. package/dist/esm/utils/optionsFrom.js +69 -2
  99. package/dist/esm/utils/optionsFrom.js.map +1 -1
  100. package/dist/esm/utils/planTree.js +57 -0
  101. package/dist/esm/utils/planTree.js.map +1 -0
  102. package/dist/esm/utils/redactionSegments.js +53 -0
  103. package/dist/esm/utils/redactionSegments.js.map +1 -0
  104. package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
  105. package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
  106. package/dist/esm/utils/searchRefusal.js +62 -0
  107. package/dist/esm/utils/searchRefusal.js.map +1 -0
  108. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  109. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  110. package/dist/nextcloud-vue.cjs.js +17409 -12832
  111. package/dist/nextcloud-vue.cjs.js.map +2 -2
  112. package/dist/nextcloud-vue.css +495 -61
  113. package/l10n/en.json +106 -1
  114. package/l10n/nl.json +105 -1
  115. package/package.json +1 -1
  116. package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
  117. package/src/components/CnAddressSearch/index.js +12 -0
  118. package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
  119. package/src/components/CnAssignDialog/index.js +12 -0
  120. package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
  121. package/src/components/CnDecisionListWidget/index.js +12 -0
  122. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
  123. package/src/components/CnDuplicateWarning/index.js +12 -0
  124. package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
  125. package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
  126. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
  127. package/src/components/CnNcDashboardList/index.js +12 -0
  128. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
  129. package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
  130. package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
  131. package/src/components/CnPlanWidget/index.js +12 -0
  132. package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
  133. package/src/components/CnRedactionEditor/index.js +13 -0
  134. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
  135. package/src/components/CnRetentionWidget/index.js +12 -0
  136. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  137. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
  138. package/src/components/CnWorkQueue/CnWorkQueue.vue +456 -0
  139. package/src/components/CnWorkQueue/index.js +12 -0
  140. package/src/components/index.js +9 -0
  141. package/src/css/index-page.css +24 -0
  142. package/src/index.js +12 -0
  143. package/src/schemas/app-manifest-v2.schema.json +119 -3
  144. package/src/utils/actionsDispatcher.js +6 -1
  145. package/src/utils/builtInModals.js +3 -1
  146. package/src/utils/duplicateWarning.js +59 -0
  147. package/src/utils/errors.js +4 -0
  148. package/src/utils/libraryWidgetKeys.js +4 -0
  149. package/src/utils/openDuplicateWarning.js +35 -0
  150. package/src/utils/optionsFrom.js +68 -1
  151. package/src/utils/planTree.js +54 -0
  152. package/src/utils/redactionSegments.js +50 -0
  153. package/src/utils/registerNcDashboardWidgets.js +57 -0
  154. package/src/utils/searchRefusal.js +59 -0
  155. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.132.0",
6
+ "version": "2.133.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -594,7 +594,10 @@
594
594
  "responsePath": { "type": "string", "minLength": 1, "description": "Where the rows sit in the answer." },
595
595
  "valueField": { "type": "string", "minLength": 1 },
596
596
  "labelField": { "type": "string", "minLength": 1 },
597
- "groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." }
597
+ "groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." },
598
+ "where": { "type": "object", "description": "Keep the rows whose fields equal these values; a value may be `@answer.<key>`, another field of the form (object-list-edit-in-place)." },
599
+ "itemsPath": { "type": "string", "minLength": 1, "description": "Read the options from the list at this path in each row (object-list-edit-in-place)." },
600
+ "hideWhenEmpty": { "type": "boolean", "description": "Leave the field out while the source gives no options (object-list-edit-in-place)." }
598
601
  }
599
602
  },
600
603
  {
@@ -607,7 +610,10 @@
607
610
  "filter": { "type": "object", "description": "Filters on the objects, as a list filter takes them." },
608
611
  "valueField": { "type": "string", "minLength": 1 },
609
612
  "labelField": { "type": "string", "minLength": 1 },
610
- "groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." }
613
+ "groupField": { "type": "string", "minLength": 1, "description": "Groups the options by this field, groups in the order they first appear (form-small-features)." },
614
+ "where": { "type": "object", "description": "Keep the rows whose fields equal these values; a value may be `@answer.<key>`, another field of the form (object-list-edit-in-place)." },
615
+ "itemsPath": { "type": "string", "minLength": 1, "description": "Read the options from the list at this path in each row (object-list-edit-in-place)." },
616
+ "hideWhenEmpty": { "type": "boolean", "description": "Leave the field out while the source gives no options (object-list-edit-in-place)." }
611
617
  }
612
618
  }
613
619
  ]
@@ -2343,6 +2349,115 @@
2343
2349
  }
2344
2350
  }
2345
2351
  },
2352
+ {
2353
+ "description": "decision-list built-in widget (CnDecisionListWidget): per-row sign-offs, each with an answer, its fields and a reason, sent to an app endpoint (2.133.0). Texts go through the host translate.",
2354
+ "if": {
2355
+ "properties": { "widgetKey": { "const": "decision-list" } },
2356
+ "required": ["widgetKey"]
2357
+ },
2358
+ "then": {
2359
+ "properties": {
2360
+ "props": {
2361
+ "type": "object",
2362
+ "properties": {
2363
+ "content": {
2364
+ "type": "object",
2365
+ "properties": {
2366
+ "source": { "type": "object", "additionalProperties": true, "description": "An endpointSource: `url`, `params?`, `responsePath?`." },
2367
+ "itemsPath": { "type": "string", "minLength": 1},
2368
+ "idField": { "type": "string", "minLength": 1},
2369
+ "title": { "type": "string", "description": "A template filled from a row." },
2370
+ "meta": { "type": "string", "description": "A template filled from a row." },
2371
+ "answers": { "type": "array", "items": { "type": "object", "required": ["value", "label"], "additionalProperties": true, "properties": { "value": {}, "label": { "type": "string", "minLength": 1}, "fields": { "type": "array", "items": { "type": "object", "required": ["field"], "additionalProperties": true, "properties": { "field": { "type": "string", "minLength": 1}, "type": { "type": "string", "enum": ["date", "text"] }, "label": { "type": "string" }, "required": { "type": "boolean" } }} }} }},
2372
+ "reason": { "description": "`{ label?, required?, field? }` (required by default, sent as `field`, default `reason`); `false` drops it.", "anyOf": [{ "type": "boolean", "const": false}, { "type": "object", "additionalProperties": false, "properties": { "label": { "type": "string" }, "required": { "type": "boolean" }, "field": { "type": "string", "minLength": 1} }}] },
2373
+ "submit": { "type": "object", "required": ["request"], "additionalProperties": true, "properties": { "label": { "type": "string" }, "request": { "type": "object", "required": ["url"], "additionalProperties": true, "properties": { "method": { "type": "string", "minLength": 1}, "url": { "type": "string", "minLength": 1}, "body": { "type": "object", "additionalProperties": true} }} }},
2374
+ "answerField": { "type": "string", "minLength": 1},
2375
+ "emptyText": { "type": "string" },
2376
+ "errorTitle": { "type": "string" },
2377
+ "answerLabel": { "type": "string" }
2378
+ }
2379
+ }
2380
+ }
2381
+ }
2382
+ }
2383
+ }
2384
+ },
2385
+ {
2386
+ "description": "retention built-in widget (CnRetentionWidget): a record's stored retention, and a recompute for archivists (2.133.0). Texts go through the host translate.",
2387
+ "if": {
2388
+ "properties": { "widgetKey": { "const": "retention" } },
2389
+ "required": ["widgetKey"]
2390
+ },
2391
+ "then": {
2392
+ "properties": {
2393
+ "props": {
2394
+ "type": "object",
2395
+ "properties": {
2396
+ "content": {
2397
+ "type": "object",
2398
+ "properties": {
2399
+ "title": { "type": "string" },
2400
+ "subtitle": { "type": "string" },
2401
+ "statusField": { "type": "string", "minLength": 1},
2402
+ "archivedValues": { "type": "array", "items": { "type": "string" } },
2403
+ "archivistGroups": { "type": "array", "items": { "type": "string", "minLength": 1}, "description": "Groups that may recompute (default [\"archivaris\"]; admins always may)." },
2404
+ "recompute": { "type": "boolean", "description": "false hides the recompute control." }
2405
+ }
2406
+ }
2407
+ }
2408
+ }
2409
+ }
2410
+ }
2411
+ },
2412
+ {
2413
+ "description": "plan built-in widget (CnPlanWidget): a record's planned steps from OpenRegister's case layer (2.133.0).",
2414
+ "if": {
2415
+ "properties": { "widgetKey": { "const": "plan" } },
2416
+ "required": ["widgetKey"]
2417
+ },
2418
+ "then": {
2419
+ "properties": {
2420
+ "props": {
2421
+ "type": "object",
2422
+ "properties": {
2423
+ "content": {
2424
+ "type": "object",
2425
+ "properties": {
2426
+ "emptyText": { "type": "string", "description": "Through the host translate." }
2427
+ }
2428
+ }
2429
+ }
2430
+ }
2431
+ }
2432
+ }
2433
+ },
2434
+ {
2435
+ "description": "work-queue built-in widget (CnWorkQueue): one queue of everything waiting on the reader, in groups that can be hidden until tomorrow (2.133.0).",
2436
+ "if": {
2437
+ "properties": { "widgetKey": { "const": "work-queue" } },
2438
+ "required": ["widgetKey"]
2439
+ },
2440
+ "then": {
2441
+ "properties": {
2442
+ "props": {
2443
+ "type": "object",
2444
+ "properties": {
2445
+ "content": {
2446
+ "type": "object",
2447
+ "properties": {
2448
+ "source": { "type": "object", "additionalProperties": true, "description": "An endpointSource answering `{ groups, hiddenGroups?, unavailable? }`." },
2449
+ "hide": { "type": "string", "minLength": 1, "description": "A url template with `{group}` that hides a group until tomorrow." },
2450
+ "show": { "type": "string", "minLength": 1, "description": "A url template with `{group}` that shows a hidden group again." },
2451
+ "itemHref": { "type": "string", "minLength": 1, "description": "A template filled from an item, for items without `route` or `href`." },
2452
+ "waitingParties": { "type": "object", "additionalProperties": { "type": "string" } },
2453
+ "emptyText": { "type": "string" }
2454
+ }
2455
+ }
2456
+ }
2457
+ }
2458
+ }
2459
+ }
2460
+ },
2346
2461
  {
2347
2462
  "description": "audit-trail built-in widget (CnAuditTrailWidget, Wave 1): the object change-log card. All props are optional — register/schema/objectId normally arrive via the detail-page object-context merge; explicit props win.",
2348
2463
  "if": {
@@ -5287,6 +5402,7 @@
5287
5402
  }
5288
5403
  },
5289
5404
  "search": { "type": "boolean", "default": true, "description": "type='index': `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page. Wins over `inlineSearch` (2.127.0)." },
5405
+ "searchRefusalHint": { "type": "string", "description": "type='index': what to do after a refused search, through the app's translate; the default explains AND, OR, NOT and quotes (2.133.0)." },
5290
5406
  "searchPlacement": { "type": "string", "enum": ["", "toolbar"], "default": "", "description": "type='index', board look: where the search field sits. `toolbar` puts it on row 1 before the Filter button and the view switch instead of opening row 2 (DqTeamwachtrij, 2.127.0)." },
5291
5407
  "sortSelectOptions": { "type": "array", "description": "type='index': the orders the index sidebar's Sort section offers in the list, card and map views, `{ value, label, order?, default?, source? }`. An option with a `source` takes its rows from an app endpoint, in that endpoint's order, for an order the server cannot sort by; the page's search, sidebar filters and pager run over those rows in the browser, and it is chosen by its value. `default: true` opens the page on it unless the address carries an order; a column header click in the table view leaves it. When the endpoint fails the list falls back to the option's `order` and shows `failedNote` (2.131.0).", "items": { "type": "object", "required": ["value", "label"], "properties": { "value": { "type": "string", "minLength": 1 }, "label": { "type": "string", "minLength": 1 }, "order": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object" }, { "type": "array" } ] }, "default": { "type": "boolean" }, "source": { "type": "object", "required": ["url"], "additionalProperties": false, "properties": { "url": { "type": "string", "minLength": 1, "description": "An app path, resolved with generateUrl and fetched with the session." }, "params": { "type": "object", "description": "Query parameters sent with the request." }, "rowsPath": { "type": "string", "minLength": 1, "description": "Where the entries sit in the answer; `results` by default, a bare array as is." }, "rowPath": { "type": "string", "minLength": 1, "description": "Where each entry carries its row; entries without one are left out." }, "carry": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "Entry fields copied onto each row, such as a tier a card's head pill reads." }, "idPath": { "type": "string", "minLength": 1, "description": "The row's identity for `annotate`; `id` by default." }, "annotate": { "type": "boolean", "description": "Copy the carried fields onto the rows of every other order too, matched on idPath." }, "searchFields": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "The fields the search box matches; every top-level text by default." }, "failedNote": { "type": "string", "minLength": 1, "description": "Shown above the list when the endpoint failed and the option's order stands in, through the app's translate." } } } } } },
5292
5408
  "defaultOrder": { "description": "type='index', self-fetch: the order the page opens on when neither the address nor sortKeys gives one: `\"-createdAt\"`, `\"priority:desc,title\"`, an `_order` map or a `[{ key, order }]` list (2.127.0).", "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] } }, { "type": "array", "items": { "type": "object", "required": ["key"], "properties": { "key": { "type": "string", "minLength": 1 }, "order": { "type": "string", "enum": ["asc", "desc"] } } } } ] },
@@ -1021,7 +1021,12 @@ export function dispatchAction(action, context = {}) {
1021
1021
  if (op === 'delete') {
1022
1022
  return store.deleteObject(type, id)
1023
1023
  }
1024
- // patch: the row's object merged with the action's values.
1024
+ // patch: only the action's values, as a PATCH, so every other key
1025
+ // keeps its stored value. A store without patchObject writes the
1026
+ // row's object merged with the values.
1027
+ if (typeof store.patchObject === 'function') {
1028
+ return store.patchObject(type, id, { ...(action.values || {}) })
1029
+ }
1025
1030
  return store.saveObject(type, { ...row, ...(action.values || {}), id })
1026
1031
  }
1027
1032
 
@@ -13,13 +13,15 @@ import { defineAsyncComponent } from 'vue'
13
13
  /**
14
14
  * The built-in modals, as registry entries (`{ kind: 'modal', component }`):
15
15
  * `CnActionDialog`, the generic summary, checklist, reason and confirm dialog,
16
- * its `config` in the action's `props`. Loaded on first open.
16
+ * and `CnAssignDialog`, hand a record to another user or team; each takes its
17
+ * `config` in the action's `props`. Loaded on first open.
17
18
  *
18
19
  * @type {Readonly<Record<string, {kind: string, component: object}>>}
19
20
  * @spec openspec/changes/action-dialog/specs/action-dialog/spec.md#requirement-a-manifest-configures-an-action-dialog
20
21
  */
21
22
  export const BUILT_IN_MODALS = Object.freeze({
22
23
  CnActionDialog: { kind: 'modal', component: defineAsyncComponent(() => import('../components/CnActionDialog/CnActionDialog.vue')) },
24
+ CnAssignDialog: { kind: 'modal', component: defineAsyncComponent(() => import('../components/CnAssignDialog/CnAssignDialog.vue')) },
23
25
  })
24
26
 
25
27
  /**
@@ -0,0 +1,59 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * What a duplicate warning offers, decided in one place, and the candidate an
6
+ * app sends to OpenRegister's duplicate check. None of this enforces
7
+ * anything: the server refuses a duplicate on the write. These helpers make
8
+ * sure the user is told before pressing Create rather than after.
9
+ *
10
+ * @module utils/duplicateWarning
11
+ */
12
+
13
+ /**
14
+ * What the duplicate warning shows and which buttons it offers.
15
+ *
16
+ * A policy other than `block` reads as `warn`, as on the server. `checked:
17
+ * false` means the check never answered: nothing is shown, because the write
18
+ * still refuses a real duplicate.
19
+ *
20
+ * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
21
+ * @param {object} state The state to decide from.
22
+ * @param {Array<object>} [state.matches] The scored matches.
23
+ * @param {boolean} [state.checked] Whether the check ran (default true).
24
+ * @param {string} [state.policy] `warn` or `block`.
25
+ * @param {boolean} [state.mayOverride] Whether this account may file over a block.
26
+ * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}} What to draw.
27
+ */
28
+ export function duplicateAffordances(state) {
29
+ const source = state || {}
30
+ const matches = Array.isArray(source.matches) ? source.matches : []
31
+ const show = source.checked !== false && matches.length > 0
32
+ const blocked = show && source.policy === 'block'
33
+ const mayOverride = source.mayOverride === true
34
+ return {
35
+ show,
36
+ blocked,
37
+ mayContinue: show && (!blocked || mayOverride),
38
+ needsReason: blocked && mayOverride,
39
+ }
40
+ }
41
+
42
+ /**
43
+ * The candidate to send to the duplicate check, without blank fields.
44
+ *
45
+ * OpenRegister scores two empty strings as a match, and a form sends `''`
46
+ * for every field nobody filled in, so blanks would match almost everything.
47
+ *
48
+ * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
49
+ * @param {object} payload The form's values.
50
+ * @return {object} The candidate.
51
+ */
52
+ export function candidateWithoutBlanks(payload) {
53
+ return Object.fromEntries(Object.entries(payload || {}).filter(([, value]) => {
54
+ if (value === null || value === undefined || value === '') {
55
+ return false
56
+ }
57
+ return !Array.isArray(value) || value.length > 0
58
+ }))
59
+ }
@@ -9,6 +9,7 @@ import { translateValidationMessage } from './validationMessages.js'
9
9
  * @property {object|null} details Validation errors or additional details
10
10
  * @property {boolean} isValidation Whether this is a validation error (400/422)
11
11
  * @property {object|null} fields Per-field validation errors
12
+ * @property {number|null} [position] The 1-based character a refused search term stopped at (400 only)
12
13
  */
13
14
 
14
15
  /**
@@ -62,6 +63,7 @@ export async function parseResponseError(response, type) {
62
63
  const status = response.status
63
64
  let details = null
64
65
  let fields = null
66
+ let position = null
65
67
  let message
66
68
 
67
69
  try {
@@ -74,6 +76,7 @@ export async function parseResponseError(response, type) {
74
76
  } else if (body && typeof body === 'object') {
75
77
  details = body.errors || body.error || body.message || null
76
78
  fields = body.validationErrors || body.errors || null
79
+ position = Number.isInteger(body.position) && body.position > 0 ? body.position : null
77
80
  }
78
81
  } catch {
79
82
  // Response body is not JSON
@@ -88,6 +91,7 @@ export async function parseResponseError(response, type) {
88
91
  details,
89
92
  isValidation: true,
90
93
  fields,
94
+ position,
91
95
  toString() {
92
96
  return this.message
93
97
  },
@@ -59,6 +59,7 @@ export const BUILT_IN_WIDGET_KEYS = Object.freeze([
59
59
  'form-renderer',
60
60
  'map-viewer',
61
61
  'notice',
62
+ 'decision-list',
62
63
  'object-geo',
63
64
  'card-grid',
64
65
  'nav-card-grid',
@@ -134,10 +135,12 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
134
135
  'object-table',
135
136
  'party-actions',
136
137
  'people',
138
+ 'plan',
137
139
  'quicklinks',
138
140
  'radio-cards',
139
141
  'record-list',
140
142
  'related',
143
+ 'retention',
141
144
  'saved-view',
142
145
  'saved-views',
143
146
  'sections',
@@ -159,6 +162,7 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
159
162
  'video',
160
163
  'week-strip',
161
164
  'working-calendar',
165
+ 'work-queue',
162
166
  'workspace-filter',
163
167
  ])
164
168
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Open CnDuplicateWarning from code (a create override, a store action) and
6
+ * wait for the user's answer.
7
+ *
8
+ * @module utils/openDuplicateWarning
9
+ */
10
+
11
+ import { createApp } from 'vue'
12
+ import CnDuplicateWarning from '../components/CnDuplicateWarning/CnDuplicateWarning.vue'
13
+
14
+ /**
15
+ * Mount the warning on the page and resolve with the decision: `{ reason,
16
+ * over }` when the user files anyway, null when they cancel.
17
+ *
18
+ * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
19
+ * @param {object} props CnDuplicateWarning's props (`matches`, `records`, `policy`, `mayOverride`, ...).
20
+ * @return {Promise<{reason: string, over: Array<string>}|null>} The decision.
21
+ */
22
+ export function openDuplicateWarning(props) {
23
+ return new Promise((resolve) => {
24
+ const host = document.createElement('div')
25
+ document.body.appendChild(host)
26
+ let app = null
27
+ const finish = (decision) => {
28
+ app.unmount()
29
+ host.remove()
30
+ resolve(decision)
31
+ }
32
+ app = createApp(CnDuplicateWarning, { ...(props || {}), onClose: () => finish(null), onFile: (decision) => finish(decision) })
33
+ app.mount(host)
34
+ })
35
+ }
@@ -113,7 +113,7 @@ export async function loadOptionsFrom(from, ctx = {}) {
113
113
  return []
114
114
  }
115
115
  const data = await fetchEndpointSource(source, ctx, { force: true })
116
- const rows = Array.isArray(data) ? data : []
116
+ const rows = narrowRows(Array.isArray(data) ? data : [], from)
117
117
  const seen = new Set()
118
118
  const out = []
119
119
  for (const row of rows) {
@@ -131,6 +131,73 @@ export async function loadOptionsFrom(from, ctx = {}) {
131
131
  return out
132
132
  }
133
133
 
134
+ /**
135
+ * The rows a source answers, narrowed: `where` keeps the rows whose fields
136
+ * equal its values (`{ type: 'string' }`), and `itemsPath` replaces each
137
+ * remaining row by the list at that path (the formats of one property type).
138
+ *
139
+ * @param {Array<unknown>} rows The rows.
140
+ * @param {{where?: object, itemsPath?: string}} from The `optionsFrom` block.
141
+ * @return {Array<unknown>} The rows the options are read from.
142
+ */
143
+ function narrowRows(rows, from) {
144
+ let out = rows
145
+ if (isPlainObject(from.where)) {
146
+ out = out.filter((row) => row && typeof row === 'object' && Object.entries(from.where).every(([key, value]) => String(readPath(row, key)) === String(value)))
147
+ }
148
+ if (typeof from.itemsPath === 'string' && from.itemsPath !== '') {
149
+ out = out.flatMap((row) => {
150
+ const items = readPath(row, from.itemsPath)
151
+ return Array.isArray(items) ? items : []
152
+ })
153
+ }
154
+ return out
155
+ }
156
+
157
+ /**
158
+ * An `optionsFrom` block with every `@answer.<key>` string replaced by that
159
+ * form value, so a source can depend on another field (`where: { type:
160
+ * '@answer.propertyType' }`). A value the form does not hold becomes ''.
161
+ *
162
+ * @param {unknown} value The block, or a part of it.
163
+ * @param {object} formData The form values.
164
+ * @return {unknown} The block with the form values filled in.
165
+ */
166
+ export function resolveFormTokens(value, formData) {
167
+ if (typeof value === 'string' && value.startsWith('@answer.')) {
168
+ const held = (formData || {})[value.slice('@answer.'.length)]
169
+ return held === undefined || held === null ? '' : held
170
+ }
171
+ if (Array.isArray(value)) {
172
+ return value.map((v) => resolveFormTokens(v, formData))
173
+ }
174
+ if (isPlainObject(value)) {
175
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveFormTokens(v, formData)]))
176
+ }
177
+ return value
178
+ }
179
+
180
+ /**
181
+ * The form fields an `optionsFrom` block reads through `@answer.<key>`.
182
+ *
183
+ * @param {unknown} from The block.
184
+ * @return {Array<string>} The keys.
185
+ */
186
+ export function formTokenKeys(from) {
187
+ const keys = new Set()
188
+ const walk = (v) => {
189
+ if (typeof v === 'string' && v.startsWith('@answer.')) {
190
+ keys.add(v.slice('@answer.'.length))
191
+ } else if (Array.isArray(v)) {
192
+ v.forEach(walk)
193
+ } else if (isPlainObject(v)) {
194
+ Object.values(v).forEach(walk)
195
+ }
196
+ }
197
+ walk(from)
198
+ return [...keys]
199
+ }
200
+
134
201
  /**
135
202
  * The options whose label holds the query, ignoring case. An empty query
136
203
  * keeps them all.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * The plan rows of OpenRegister's case layer as a tree, and the moves a
6
+ * user may ask for on one item. The moves are advisory: OpenRegister decides.
7
+ *
8
+ * @module utils/planTree
9
+ */
10
+
11
+ /** Plan item states after which nothing more can happen. */
12
+ export const TERMINAL_STATES = ['completed', 'terminated', 'disabled']
13
+
14
+ /**
15
+ * The plan rows as a tree: stages with their items, each level by position.
16
+ *
17
+ * @param {Array<object>} items The plan rows (`id`, `parentItemId`, `position`).
18
+ * @return {Array<object>} The roots, each with `children`.
19
+ */
20
+ export function planTree(items) {
21
+ const rows = Array.isArray(items) ? items : []
22
+ const byId = new Map(rows.map((row) => [row.id, { ...row, children: [] }]))
23
+ const roots = []
24
+ for (const row of rows) {
25
+ const node = byId.get(row.id)
26
+ const parent = row.parentItemId === null || row.parentItemId === undefined ? undefined : byId.get(row.parentItemId)
27
+ if (parent === undefined) {
28
+ roots.push(node)
29
+ } else {
30
+ parent.children.push(node)
31
+ }
32
+ }
33
+ const byPosition = (a, b) => (a.position ?? 0) - (b.position ?? 0)
34
+ for (const node of byId.values()) {
35
+ node.children.sort(byPosition)
36
+ }
37
+ return roots.sort(byPosition)
38
+ }
39
+
40
+ /**
41
+ * The target states a user may ask for. Advisory only: OpenRegister decides.
42
+ *
43
+ * @param {object} item The plan item.
44
+ * @return {string[]} The targets.
45
+ */
46
+ export function offeredTransitions(item) {
47
+ if (!item || TERMINAL_STATES.includes(item.state)) {
48
+ return []
49
+ }
50
+ if (item.type === 'milestone') {
51
+ return item.state === 'available' ? ['completed', 'terminated'] : []
52
+ }
53
+ return item.state === 'active' ? ['completed', 'terminated'] : ['terminated']
54
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * A document text cut into plain and marked segments, for the redaction
6
+ * editor to draw the passages in place.
7
+ *
8
+ * @module utils/redactionSegments
9
+ */
10
+
11
+ /**
12
+ * The text as segments: plain ones and marked ones carrying the passage's
13
+ * verdict (`open` before it has one). Where passages overlap the longest wins,
14
+ * and every occurrence of a passage is marked.
15
+ *
16
+ * @param {string} text The document text.
17
+ * @param {Array<{value: string, verdict: (string|null)}>} passages The passages.
18
+ * @return {Array<{text: string, verdict: (string|null)}>} The segments.
19
+ */
20
+ export function redactionSegments(text, passages) {
21
+ const source = String(text || '')
22
+ const ranges = []
23
+ const ordered = (Array.isArray(passages) ? passages : [])
24
+ .filter((passage) => passage && String(passage.value || '') !== '')
25
+ .sort((left, right) => right.value.length - left.value.length)
26
+ for (const passage of ordered) {
27
+ let from = source.indexOf(passage.value)
28
+ while (from !== -1) {
29
+ const to = from + passage.value.length
30
+ if (ranges.every((range) => to <= range.from || from >= range.to)) {
31
+ ranges.push({ from, to, verdict: passage.verdict || 'open' })
32
+ }
33
+ from = source.indexOf(passage.value, to)
34
+ }
35
+ }
36
+ ranges.sort((left, right) => left.from - right.from)
37
+ const segments = []
38
+ let at = 0
39
+ for (const range of ranges) {
40
+ if (range.from > at) {
41
+ segments.push({ text: source.slice(at, range.from), verdict: null })
42
+ }
43
+ segments.push({ text: source.slice(range.from, range.to), verdict: range.verdict })
44
+ at = range.to
45
+ }
46
+ if (at < source.length) {
47
+ segments.push({ text: source.slice(at), verdict: null })
48
+ }
49
+ return segments
50
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Register an app's Nextcloud Dashboard widgets from configuration.
6
+ *
7
+ * @module utils/registerNcDashboardWidgets
8
+ */
9
+
10
+ import { translate } from '@nextcloud/l10n'
11
+ import { createApp } from 'vue'
12
+ import CnNcDashboardList from '../components/CnNcDashboardList/CnNcDashboardList.vue'
13
+
14
+ /**
15
+ * Register each widget with `OCA.Dashboard.register(id, …)`. A widget mounts
16
+ * CnNcDashboardList with its `config`, or its own `component` with `props`
17
+ * and the widget's `title`, as its own Vue app with the app's translate provided as `cnTranslate`. The
18
+ * Dashboard host draws the widget's title, so the component must not.
19
+ *
20
+ * The widget ids are the ones the app's PHP `IWidget::getId()` returns; the
21
+ * server side stays the app's.
22
+ *
23
+ * @spec openspec/changes/nc-dashboard-widgets/specs/nc-dashboard-widgets/spec.md#requirement-dashboard-widgets-are-configuration
24
+ * @param {Array<{id: string, config?: object, component?: object, props?: object}>} widgets The widgets.
25
+ * @param {object} options Options.
26
+ * @param {string} options.appId The app id, for its translations.
27
+ * @param {(app: object) => void} [options.setup] Called on each widget's Vue app before mounting (to `use` a pinia, say).
28
+ * @param {object} [options.dashboard] The `OCA.Dashboard` registry (default `window.OCA.Dashboard`).
29
+ * @return {string[]} The ids registered; none when the Dashboard is absent.
30
+ */
31
+ export function registerNcDashboardWidgets(widgets, options = {}) {
32
+ const dashboard = options.dashboard || (typeof window !== 'undefined' && window.OCA && window.OCA.Dashboard) || null
33
+ if (!dashboard || typeof dashboard.register !== 'function' || !Array.isArray(widgets)) {
34
+ return []
35
+ }
36
+ const appId = options.appId || 'nextcloud-vue'
37
+ const cnTranslate = (key, vars) => translate(appId, key, vars)
38
+ const registered = []
39
+ for (const widget of widgets) {
40
+ if (!widget || typeof widget.id !== 'string' || widget.id === '') {
41
+ continue
42
+ }
43
+ dashboard.register(widget.id, (el, context = {}) => {
44
+ const title = context.widget && context.widget.title ? context.widget.title : ''
45
+ const app = widget.component
46
+ ? createApp(widget.component, { ...(widget.props || {}), title })
47
+ : createApp(CnNcDashboardList, { config: widget.config || {} })
48
+ app.provide('cnTranslate', cnTranslate)
49
+ if (typeof options.setup === 'function') {
50
+ options.setup(app)
51
+ }
52
+ app.mount(el)
53
+ })
54
+ registered.push(widget.id)
55
+ }
56
+ return registered
57
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * A refused search, said where it was typed. OpenRegister refuses a `_search`
6
+ * term it cannot parse (an unclosed bracket or quote) with `400 { error,
7
+ * position, term }` rather than running it as a literal, which would return
8
+ * nothing and read as an honest empty result.
9
+ *
10
+ * @module utils/searchRefusal
11
+ */
12
+
13
+ /** `<reason> at position <n>.`, the shape OpenRegister builds the message in. */
14
+ const POSITION_IN_MESSAGE = /\bat position (\d+)\.?\s*$/
15
+
16
+ /**
17
+ * The 1-based character a refusal points at, or null.
18
+ *
19
+ * @param {object} error The ApiError.
20
+ * @return {number|null}
21
+ */
22
+ function positionOf(error) {
23
+ const carried = error.position ?? (error.details && typeof error.details === 'object' ? error.details.position : undefined)
24
+ if (Number.isInteger(carried) && carried > 0) {
25
+ return carried
26
+ }
27
+ const match = POSITION_IN_MESSAGE.exec(String(error.message || ''))
28
+ const parsed = match ? Number(match[1]) : 0
29
+ return parsed > 0 ? parsed : null
30
+ }
31
+
32
+ /**
33
+ * A search refusal, or null when this failure is not one: not a 400, no term
34
+ * typed, or no position to point at (then it is a plain failed fetch).
35
+ *
36
+ * @spec openspec/changes/index-search-refusal/specs/index-search-refusal/spec.md#requirement-a-refused-search-is-said-where-it-was-typed
37
+ * @param {object|null} error The ApiError the store recorded.
38
+ * @param {string} term The term the user typed.
39
+ * @return {{message: string, position: number, term: string, before: string, at: string, after: string}|null}
40
+ */
41
+ export function readSearchRefusal(error, term) {
42
+ const typed = typeof term === 'string' ? term : ''
43
+ if (!error || typed.trim() === '' || Number(error.status) !== 400) {
44
+ return null
45
+ }
46
+ const position = positionOf(error)
47
+ if (position === null) {
48
+ return null
49
+ }
50
+ const index = Math.min(position - 1, typed.length)
51
+ return {
52
+ message: String(error.message || ''),
53
+ position,
54
+ term: typed,
55
+ before: typed.slice(0, index),
56
+ at: typed.slice(index, index + 1),
57
+ after: typed.slice(index + 1),
58
+ }
59
+ }