@conduction/nextcloud-vue 2.56.0-beta.34 → 2.56.0-beta.36

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/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  6. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  7. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  8. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  10. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  11. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
  17. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +5 -0
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  23. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  24. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  31. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  35. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  36. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  39. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  45. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  51. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  52. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  53. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  54. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  55. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  56. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  57. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  59. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  61. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
  62. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  65. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  66. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  67. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  68. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  74. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  75. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  78. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  79. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  81. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  82. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  83. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  84. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  85. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  86. package/dist/esm/index.js +1 -0
  87. package/dist/esm/index.js.map +1 -1
  88. package/dist/esm/l10n/en.json.js +16 -1
  89. package/dist/esm/l10n/en.json.js.map +1 -1
  90. package/dist/esm/l10n/nl.json.js +38 -1
  91. package/dist/esm/l10n/nl.json.js.map +1 -1
  92. package/dist/esm/library-style.js +1 -1
  93. package/dist/esm/nextcloud-vue.css +525 -318
  94. package/dist/esm/utils/buildManifest.js +37 -3
  95. package/dist/esm/utils/buildManifest.js.map +1 -1
  96. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  97. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  98. package/dist/esm/utils/formatMetric.js +39 -3
  99. package/dist/esm/utils/formatMetric.js.map +1 -1
  100. package/dist/esm/utils/gridStack.js +40 -1
  101. package/dist/esm/utils/gridStack.js.map +1 -1
  102. package/dist/esm/utils/humaniseKey.js +41 -0
  103. package/dist/esm/utils/humaniseKey.js.map +1 -0
  104. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  105. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  106. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  107. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  108. package/dist/esm/utils/sentinelTokens.js +9 -0
  109. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  110. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  111. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  112. package/dist/nextcloud-vue.cjs.js +2250 -708
  113. package/dist/nextcloud-vue.cjs.js.map +2 -2
  114. package/dist/nextcloud-vue.css +525 -318
  115. package/l10n/en.json +16 -1
  116. package/l10n/nl.json +38 -1
  117. package/package.json +1 -1
  118. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  119. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  120. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  121. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
  122. package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
  123. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  124. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  125. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  126. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  127. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  128. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  129. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  130. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
  131. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  132. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  133. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  134. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  135. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  136. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  137. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  138. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  139. package/src/css/form-field.css +14 -0
  140. package/src/css/form-widget.css +11 -0
  141. package/src/css/kpi-card.css +13 -0
  142. package/src/css/look-board-reports.css +58 -0
  143. package/src/css/look-board.css +31 -0
  144. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  145. package/src/index.js +2 -0
  146. package/src/schemas/app-manifest-v2.schema.json +23 -2
  147. package/src/utils/buildManifest.js +36 -2
  148. package/src/utils/detailWidgetVisibility.js +145 -0
  149. package/src/utils/formatMetric.js +38 -2
  150. package/src/utils/gridStack.js +39 -0
  151. package/src/utils/humaniseKey.js +38 -0
  152. package/src/utils/notificationRuleLabel.js +5 -23
  153. package/src/utils/resolveManifestTokens.js +1 -0
  154. package/src/utils/sentinelTokens.js +9 -0
  155. package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json CHANGED
@@ -1,5 +1,16 @@
1
1
  {
2
2
  "translations": {
3
+ "Channel": "Channel",
4
+ "Outcome": "Outcome",
5
+ "Chat": "Chat",
6
+ "Counter": "Counter",
7
+ "Social media": "Social media",
8
+ "Letter": "Letter",
9
+ "Save contact moment": "Save contact moment",
10
+ "Saving…": "Saving…",
11
+ "Subject is required": "Subject is required",
12
+ "Cannot save right now": "Cannot save right now",
13
+ "Failed to save interaction": "Failed to save interaction",
3
14
  "You cannot see these files": "You cannot see these files",
4
15
  "Delete {name}? This cannot be undone here.": "Delete {name}? This cannot be undone here.",
5
16
  "Linked items": "Linked items",
@@ -326,6 +337,9 @@
326
337
  "Constant": "Constant",
327
338
  "Copy": "Copy",
328
339
  "Copied": "Copied",
340
+ "Copy the name": "Copy the name",
341
+ "Copy the value": "Copy the value",
342
+ "Copying did not work. Select the text and copy it yourself.": "Copying did not work. Select the text and copy it yourself.",
329
343
  "Show": "Show",
330
344
  "Copy {label}": "Copy {label}",
331
345
  "Show {label}": "Show {label}",
@@ -1403,7 +1417,8 @@
1403
1417
  "This answer was refused.": "This answer was refused.",
1404
1418
  "No value ({count})": "No value ({count})",
1405
1419
  "{start} to {end}": "{start} to {end}",
1406
- "Breadcrumb": "Breadcrumb"
1420
+ "Breadcrumb": "Breadcrumb",
1421
+ "Moving to {stage}": "Moving to {stage}"
1407
1422
  },
1408
1423
  "plurals": {
1409
1424
  "{count} selected": [
package/l10n/nl.json CHANGED
@@ -1,5 +1,16 @@
1
1
  {
2
2
  "translations": {
3
+ "Channel": "Kanaal",
4
+ "Outcome": "Uitkomst",
5
+ "Chat": "Chat",
6
+ "Counter": "Balie",
7
+ "Social media": "Sociale media",
8
+ "Letter": "Brief",
9
+ "Save contact moment": "Contactmoment opslaan",
10
+ "Saving…": "Opslaan…",
11
+ "Subject is required": "Onderwerp is verplicht",
12
+ "Cannot save right now": "Opslaan kan nu niet",
13
+ "Failed to save interaction": "Het contactmoment is niet opgeslagen",
3
14
  "You cannot see these files": "Je kunt deze bestanden niet zien",
4
15
  "Delete {name}? This cannot be undone here.": "{name} verwijderen? Dit kun je hier niet ongedaan maken.",
5
16
  "Linked items": "Gekoppelde items",
@@ -348,6 +359,9 @@
348
359
  "Constant": "Constante",
349
360
  "Copy": "Kopiëren",
350
361
  "Copied": "Gekopieerd",
362
+ "Copy the name": "Naam kopiëren",
363
+ "Copy the value": "Waarde kopiëren",
364
+ "Copying did not work. Select the text and copy it yourself.": "Kopiëren lukte niet. Selecteer de tekst en kopieer hem zelf.",
351
365
  "Show": "Tonen",
352
366
  "Copy {label}": "{label} kopiëren",
353
367
  "Show {label}": "{label} tonen",
@@ -1547,7 +1561,30 @@
1547
1561
  "This answer was refused.": "Dit antwoord is geweigerd.",
1548
1562
  "No value ({count})": "Geen waarde ({count})",
1549
1563
  "{start} to {end}": "{start} tot {end} uur",
1550
- "Breadcrumb": "Kruimelpad"
1564
+ "Breadcrumb": "Kruimelpad",
1565
+ "Cannot move: this record has no id, so the move would create a duplicate instead of updating it.": "Verplaatsen kan niet: dit record heeft geen id, dus de verplaatsing zou een kopie maken in plaats van het record bij te werken.",
1566
+ "Could not check whether this stage can be reached": "Kon niet controleren of deze fase bereikbaar is",
1567
+ "Could not check which stages can be reached": "Kon niet controleren welke fases bereikbaar zijn",
1568
+ "Could not load the stages": "De fases konden niet worden geladen",
1569
+ "Just a moment, the last move is still running": "Een moment, de vorige verplaatsing loopt nog",
1570
+ "Loading stages": "Fases laden",
1571
+ "Move to {stage}": "Naar {stage} verplaatsen",
1572
+ "Moved to {stage}": "Verplaatst naar {stage}",
1573
+ "No stages to show": "Geen fases om te tonen",
1574
+ "Not reachable from the current stage": "Niet bereikbaar vanuit de huidige fase",
1575
+ "Stages": "Fases",
1576
+ "The move could not be made": "De verplaatsing is niet gelukt",
1577
+ "The record is not loaded yet": "Het record is nog niet geladen",
1578
+ "The stages cannot be read for this record yet": "De fases van dit record zijn nog niet te lezen",
1579
+ "This move is not possible right now": "Deze verplaatsing kan nu niet",
1580
+ "This stage cannot be chosen right now": "Deze fase kan nu niet worden gekozen",
1581
+ "Are you sure you want to \"{action}\"?": "Weet je zeker dat je \"{action}\" wilt uitvoeren?",
1582
+ "Moved to {state}": "Verplaatst naar {state}",
1583
+ "Moved {title} to {state}": "{title} verplaatst naar {state}",
1584
+ "Transition failed": "De overgang is mislukt",
1585
+ "Closed": "Afgesloten",
1586
+ "Skip to {action}": "Ga naar {action}",
1587
+ "Moving to {stage}": "Bezig met verplaatsen naar {stage}"
1551
1588
  },
1552
1589
  "plurals": {
1553
1590
  "{count} selected": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.56.0-beta.34",
3
+ "version": "2.56.0-beta.36",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -154,7 +154,7 @@ export default {
154
154
  if (!this.register || !this.schema || !this.objectId) {
155
155
  return ''
156
156
  }
157
- return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`
157
+ return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`
158
158
  },
159
159
  },
160
160
 
@@ -82,6 +82,8 @@ export default {
82
82
  cnRegistry: { default: () => ({}) },
83
83
  /** Legacy custom-component map provided by CnAppRoot. */
84
84
  cnCustomComponents: { default: () => ({}) },
85
+ /** The host's translate function for manifest-authored strings, provided by CnAppRoot. */
86
+ cnTranslate: { default: null },
85
87
  },
86
88
 
87
89
  props: {
@@ -201,7 +203,7 @@ export default {
201
203
  return {
202
204
  id,
203
205
  name,
204
- title: (section && section.title) || '',
206
+ title: this.translateTitle((section && section.title) || ''),
205
207
  card: !!(section && section.card),
206
208
  colSpan: section && section.colSpan,
207
209
  placement: section && section.placement,
@@ -213,6 +215,25 @@ export default {
213
215
  },
214
216
 
215
217
  methods: {
218
+ /**
219
+ * A manifest-authored section title in the reader's language.
220
+ *
221
+ * The title is written in the manifest, so it belongs to the host
222
+ * app's catalogue, the same as every other manifest title the detail
223
+ * page shows. A section heading printed raw read "Asked about" to a
224
+ * Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).
225
+ *
226
+ * @param {string} title The title as written in the manifest.
227
+ * @return {string} The translated title, or '' when there is none.
228
+ * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
229
+ */
230
+ translateTitle(title) {
231
+ if (title === '' || typeof this.cnTranslate !== 'function') {
232
+ return title
233
+ }
234
+ return this.cnTranslate(title)
235
+ },
236
+
216
237
  /**
217
238
  * Resolve a section's host-app component by registry name. Precedence
218
239
  * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`
@@ -69,6 +69,18 @@ const VARIANT_COLORS = Object.freeze({
69
69
  * negative number: "-3 days left" is a puzzle, and an overdue case is the one
70
70
  * state a handler must not have to decode.
71
71
  *
72
+ * ## A settled record
73
+ *
74
+ * `settledField` names a property on the record that is truthy once the work
75
+ * the date belongs to is done, such as a case in its final status. A settled
76
+ * record reads `settledLabel` (default "Closed") in no threshold colour, never
77
+ * "overdue": a closed case missed nothing that can still be acted on, and a red
78
+ * count of days on it sends a handler after a case that needs nobody.
79
+ *
80
+ * ```js
81
+ * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }
82
+ * ```
83
+ *
72
84
  * ## An unset date
73
85
  *
74
86
  * Renders a dash and no threshold colour. A case with no deadline is not urgent
@@ -91,8 +103,10 @@ export default {
91
103
  * - `icon` — MDI icon name.
92
104
  * - `thresholds` — `{ warn, danger }` in days remaining.
93
105
  * - `showDate` — render the absolute date underneath (default true).
106
+ * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
107
+ * - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
94
108
  *
95
- * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}
109
+ * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
96
110
  */
97
111
  content: {
98
112
  type: Object,
@@ -115,6 +129,7 @@ export default {
115
129
  return {
116
130
  cnObjectContextInjected: inject('cnObjectContext', null),
117
131
  cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
132
+ cnTranslate: inject('cnTranslate', null),
118
133
  }
119
134
  },
120
135
 
@@ -166,17 +181,44 @@ export default {
166
181
  return Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)
167
182
  },
168
183
 
169
- /** @return {boolean} true once the date has passed. */
184
+ /**
185
+ * Whether the record says its work is done (`settledField` is truthy).
186
+ *
187
+ * @return {boolean} True for a settled record.
188
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
189
+ */
190
+ isSettled() {
191
+ const field = this.content?.settledField
192
+ if (!this.record || !field) {
193
+ return false
194
+ }
195
+ return Boolean(getByPath(this.record, field))
196
+ },
197
+
198
+ /**
199
+ * Whether the date has passed on a record still open.
200
+ *
201
+ * @return {boolean} True when overdue.
202
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
203
+ */
170
204
  isOverdue() {
171
- return this.daysRemaining !== null && this.daysRemaining < 0
205
+ return !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0
172
206
  },
173
207
 
174
208
  /**
175
209
  * The headline: the remaining time in words.
176
210
  *
177
211
  * @return {string} The headline text.
212
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
178
213
  */
179
214
  headline() {
215
+ if (this.isSettled) {
216
+ const label = this.content?.settledLabel
217
+ if (label) {
218
+ return typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label
219
+ }
220
+ return t('nextcloud-vue', 'Closed')
221
+ }
180
222
  const days = this.daysRemaining
181
223
  if (days === null) {
182
224
  return '—'
@@ -213,10 +255,11 @@ export default {
213
255
  * The threshold band this date falls in.
214
256
  *
215
257
  * @return {string} One of default / success / warning / error.
258
+ * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
216
259
  */
217
260
  variant() {
218
261
  const days = this.daysRemaining
219
- if (days === null) {
262
+ if (days === null || this.isSettled) {
220
263
  return 'default'
221
264
  }
222
265
  if (days < 0) {
@@ -28,21 +28,21 @@
28
28
 
29
29
  <div class="grid-stack">
30
30
  <div
31
- v-for="(item, index) in layout"
31
+ v-for="item in itemsInRowOrder"
32
32
  :key="resolveItemKey(item)"
33
33
  class="grid-stack-item"
34
34
  :class="{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }"
35
35
  role="group"
36
- :aria-label="resolveItemLabel(item, index)"
36
+ :aria-label="resolveItemLabel(item, layout.indexOf(item))"
37
37
  :aria-describedby="itemDescribedBy"
38
38
  :tabindex="itemTabindex"
39
39
  :gs-id="item.id"
40
40
  :gs-x="item.gridX"
41
- :gs-y="item.gridY"
41
+ :gs-y="scaledRow(item.gridY)"
42
42
  :gs-w="item.gridWidth"
43
- :gs-h="item.gridHeight"
43
+ :gs-h="scaledRow(item.gridHeight)"
44
44
  :gs-min-w="minWidth"
45
- :gs-min-h="minHeight"
45
+ :gs-min-h="minRows(item)"
46
46
  :gs-size-to-content="sizeToContentAttr(item)"
47
47
  @keydown="onItemKeydown($event, item)">
48
48
  <div class="grid-stack-item-content">
@@ -71,6 +71,7 @@
71
71
  <script>
72
72
  import { translate as t } from '@nextcloud/l10n'
73
73
  import { GridStack } from 'gridstack'
74
+ import { planGrowth } from '../../utils/gridStack.js'
74
75
  // `gridstack` is a peerDependency (see package.json), not bundled — this
75
76
  // component's JS and the consumer's GridStack stylesheet MUST come from the
76
77
  // exact same installed copy, because GridStack's JS writes layout state
@@ -99,6 +100,24 @@ let domIdCounter = 0
99
100
  */
100
101
  const MENU_ANCHOR_OFFSET = 8
101
102
 
103
+ /**
104
+ * How many GridStack rows a layout row is drawn as on a grid with
105
+ * size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.
106
+ */
107
+ const ROW_SPLIT = 8
108
+
109
+ /**
110
+ * Whether a layout holds a size-to-content cell (`sizeToContent` true or a
111
+ * positive row maximum).
112
+ *
113
+ * @param {Array<object>} layout The layout items.
114
+ * @return {boolean} True when one does.
115
+ */
116
+ function hasFitCell(layout) {
117
+ return Array.isArray(layout) && layout.some((item) => item
118
+ && (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))
119
+ }
120
+
102
121
  /**
103
122
  * CnDashboardGrid — Low-level grid layout engine powered by GridStack.
104
123
  *
@@ -304,10 +323,62 @@ export default {
304
323
  * on that.
305
324
  */
306
325
  reflowed: false,
326
+ /**
327
+ * How many GridStack rows one layout row is drawn as: 1, or
328
+ * ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,
329
+ * since GridStack reads the items' places from their attributes,
330
+ * and again when the grid is rebuilt.
331
+ */
332
+ rowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,
307
333
  }
308
334
  },
309
335
 
310
336
  computed: {
337
+ /**
338
+ * The layout's items top to bottom, then left to right: the order the
339
+ * items are in the page, so GridStack places them in that order.
340
+ *
341
+ * GridStack sizes a size-to-content cell while it adds the items, one
342
+ * at a time, and an item it adds onto a cell that has already grown
343
+ * moves itself below whatever it then collides with. In the layout's
344
+ * own order a card declared after the lists (pipelinq's full-width
345
+ * "client in focus" card) was added last, so when the banner above
346
+ * it grew it skipped past the stat tiles AND the lists and landed at
347
+ * the bottom. Added top to bottom, every card lands under the one
348
+ * above it. It is also the order a screen reader and the Tab key
349
+ * follow. An item without a row or column sorts after the items
350
+ * that have one, in the layout's order.
351
+ *
352
+ * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
353
+ * @return {Array<object>} The items in row order.
354
+ */
355
+ itemsInRowOrder() {
356
+ const items = Array.isArray(this.layout) ? [...this.layout] : []
357
+ const at = (value) => (Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER)
358
+ return items.sort((a, b) => (at(a.gridY) - at(b.gridY)) || (at(a.gridX) - at(b.gridX)))
359
+ },
360
+
361
+ /**
362
+ * Whether the grid draws each layout row as ROW_SPLIT finer rows. A
363
+ * size-to-content cell can only be a whole number of rows tall, so on
364
+ * 80px rows it kept up to a row of empty space under its content and
365
+ * a packing grid (`float: false`) could not close it (pipelinq and
366
+ * dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the
367
+ * cell ends within 10px of its content and the widgets below move up.
368
+ *
369
+ * Only in view mode, only on a packing grid with a size-to-content
370
+ * cell: every other grid is built exactly as before. Item positions
371
+ * and sizes scale with the rows, so every widget keeps its pixel
372
+ * place and size until a cell shrinks; the layout itself, what edit
373
+ * mode shows and what `layout-change` reports, stays in layout rows.
374
+ *
375
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
376
+ * @return {boolean} True to split the rows.
377
+ */
378
+ wantsRowSplit() {
379
+ return !this.editable && !this.float && hasFitCell(this.layout)
380
+ },
381
+
311
382
  /**
312
383
  * @return {boolean} Whether the keyboard lane is live — items are
313
384
  * focusable and the arrow keys reposition. Requires the opt-in prop,
@@ -374,6 +445,12 @@ export default {
374
445
  this.syncInteractivity()
375
446
  },
376
447
 
448
+ wantsRowSplit(wants) {
449
+ if (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {
450
+ this.rebuildGrid()
451
+ }
452
+ },
453
+
377
454
  layout: {
378
455
  deep: true,
379
456
  handler(newLayout) {
@@ -433,11 +510,75 @@ export default {
433
510
  }
434
511
  // A number is GridStack's "size to content, but never taller than N rows" (a maximum).
435
512
  if (Number.isFinite(v) && v > 0) {
436
- return String(v)
513
+ return String(v * this.rowSplit)
437
514
  }
438
515
  return undefined
439
516
  },
440
517
 
518
+ /**
519
+ * A layout row coordinate or height in GridStack rows (`rowSplit`).
520
+ *
521
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
522
+ * @param {number|undefined} value The value in layout rows.
523
+ * @return {number|undefined} The value in grid rows, or undefined when none is set.
524
+ */
525
+ scaledRow(value) {
526
+ return Number.isFinite(value) ? value * this.rowSplit : undefined
527
+ },
528
+
529
+ /**
530
+ * The smallest height an item may take, in grid rows: `minHeight`
531
+ * layout rows, except for a size-to-content cell on split rows, which
532
+ * may shrink to its content down to one grid row. The minimum is for
533
+ * resizing by hand, and that only happens on whole rows in edit mode.
534
+ *
535
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
536
+ * @param {object} item The layout item.
537
+ * @return {number} The minimum height in grid rows.
538
+ */
539
+ minRows(item) {
540
+ if (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {
541
+ return 1
542
+ }
543
+ return this.minHeight * this.rowSplit
544
+ },
545
+
546
+ /**
547
+ * Build the grid again with the row split `wantsRowSplit` asks for, as
548
+ * edit mode turns on or off: the items go back to their layout places in
549
+ * the new rows, and the size-to-content cells are measured again.
550
+ *
551
+ * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content
552
+ * @return {void}
553
+ */
554
+ rebuildGrid() {
555
+ this.disconnectContentObserver()
556
+ this.grid.destroy(false)
557
+ this.grid = null
558
+ this.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1
559
+ this.$nextTick(() => {
560
+ if (!this.$refs.gridContainer) {
561
+ return
562
+ }
563
+ // GridStack wrote its own places onto the items; put the layout's back.
564
+ for (const item of this.layout) {
565
+ const el = this.$refs.gridContainer.querySelector(`[gs-id="${item.id}"]`)
566
+ if (!el) {
567
+ continue
568
+ }
569
+ for (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {
570
+ if (value === undefined) {
571
+ el.removeAttribute(name)
572
+ } else {
573
+ el.setAttribute(name, String(value))
574
+ }
575
+ }
576
+ }
577
+ this.initGrid()
578
+ this.observeContentSizes()
579
+ })
580
+ },
581
+
441
582
  /**
442
583
  * Build the accessible name for a grid item. A custom `itemLabel` wins
443
584
  * outright; otherwise the first human-readable field on the item is used
@@ -492,6 +633,13 @@ export default {
492
633
  )
493
634
  },
494
635
 
636
+ /**
637
+ * Build the GridStack grid on the rendered items, install the growth
638
+ * wrapper (`keepOrderOnGrowth`) and listen for its changes.
639
+ *
640
+ * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
641
+ * @return {void}
642
+ */
495
643
  initGrid() {
496
644
  if (this.cellHeightCssVar && typeof document !== 'undefined' && document.documentElement) {
497
645
  document.documentElement.style.setProperty(this.cellHeightCssVar, `${this.cellHeight}px`)
@@ -499,7 +647,7 @@ export default {
499
647
  const el = this.$refs.gridContainer.querySelector('.grid-stack')
500
648
  this.grid = GridStack.init({
501
649
  column: this.columns,
502
- cellHeight: this.cellHeight,
650
+ cellHeight: this.cellHeight / this.rowSplit,
503
651
  margin: this.margin,
504
652
  float: this.float,
505
653
  animate: true,
@@ -508,6 +656,7 @@ export default {
508
656
  removable: false,
509
657
  ...(this.columnOpts ? { columnOpts: this.columnOpts } : {}),
510
658
  }, el)
659
+ this.keepOrderOnGrowth()
511
660
 
512
661
  this.grid.on('change', (_event, items) => {
513
662
  this.handleGridChange(items)
@@ -519,6 +668,87 @@ export default {
519
668
  this.syncInteractivity()
520
669
  },
521
670
 
671
+ /**
672
+ * Let a size-to-content cell that grows push the cards under it down in
673
+ * their order. GridStack resolves that collision itself, but not in
674
+ * order: when pipelinq's banner grew from two rows to three, the
675
+ * full-width card under the stat tiles landed below the lists, also
676
+ * with `float: false`. Every grow after the grid is built, whether
677
+ * GridStack measures it on resize or this grid's content observer asks
678
+ * for it, now first moves the cards the growth reaches down by the
679
+ * growth (`planGrowth`), bottom-most first, and only then grows the
680
+ * cell, so GridStack has no collision left to resolve. The wrapper is
681
+ * installed after `GridStack.init` has already measured the cells on
682
+ * load; the order on load comes from adding the items in row order
683
+ * (`itemsInRowOrder`). A cell that shrinks is left to GridStack as
684
+ * before.
685
+ *
686
+ * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
687
+ * @return {void}
688
+ */
689
+ keepOrderOnGrowth() {
690
+ const grid = this.grid
691
+ if (!grid || typeof grid.resizeToContent !== 'function' || typeof grid.update !== 'function') {
692
+ return
693
+ }
694
+ const resize = grid.resizeToContent.bind(grid)
695
+ grid.resizeToContent = (el) => {
696
+ const node = el && el.gridstackNode
697
+ const nodes = grid.engine && Array.isArray(grid.engine.nodes) ? grid.engine.nodes : null
698
+ const rows = node && nodes ? this.contentRows(el, node) : null
699
+ const plan = rows ? planGrowth(nodes, node, rows) : []
700
+ if (plan.length === 0) {
701
+ resize(el)
702
+ return
703
+ }
704
+ // Inside a batch the moves are not packed until the cell has grown.
705
+ const batching = Boolean(grid.engine.batchMode)
706
+ if (!batching) {
707
+ grid.batchUpdate()
708
+ }
709
+ for (const step of plan) {
710
+ grid.update(step.node.el, { y: step.y })
711
+ }
712
+ resize(el)
713
+ if (!batching) {
714
+ grid.batchUpdate(false)
715
+ }
716
+ }
717
+ },
718
+
719
+ /**
720
+ * The rows a size-to-content cell needs for its content, measured the
721
+ * way GridStack's `resizeToContent` measures it (the content's first
722
+ * child, plus the cell's own padding), within its size-to-content row
723
+ * maximum and its minimum and maximum height.
724
+ *
725
+ * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
726
+ * @param {HTMLElement} el The cell.
727
+ * @param {object} node Its GridStack node.
728
+ * @return {number|null} The rows, or null when it cannot be measured.
729
+ */
730
+ contentRows(el, node) {
731
+ const cell = this.grid.getCellHeight(true)
732
+ const item = el.querySelector('.grid-stack-item-content')
733
+ const child = item && item.firstElementChild
734
+ if (!el.clientHeight || !cell || !child || !Number.isFinite(node.h)) {
735
+ return null
736
+ }
737
+ const padding = el.clientHeight - item.clientHeight
738
+ const itemHeight = node.h * cell - padding
739
+ const wanted = child.getBoundingClientRect().height || itemHeight
740
+ let rows = Math.ceil((node.h * cell + wanted - itemHeight) / cell)
741
+ if (Number.isInteger(node.sizeToContent) && rows > node.sizeToContent) {
742
+ rows = node.sizeToContent
743
+ }
744
+ if (node.minH && rows < node.minH) {
745
+ rows = node.minH
746
+ } else if (node.maxH && rows > node.maxH) {
747
+ rows = node.maxH
748
+ }
749
+ return rows
750
+ },
751
+
522
752
  /**
523
753
  * Keep both editing lanes in step with whether an edit could actually be
524
754
  * kept: the pointer's handles, and — through `reflowed` feeding
@@ -586,6 +816,12 @@ export default {
586
816
  if (this.isReflowed()) {
587
817
  return
588
818
  }
819
+ // A grid on split rows is a view-mode grid: what changes in it is
820
+ // packing and size-to-content measuring, never a user's
821
+ // arrangement, and it is not reported as one.
822
+ if (this.rowSplit !== 1) {
823
+ return
824
+ }
589
825
  if (!items || items.length === 0) {
590
826
  return
591
827
  }
@@ -986,7 +1222,7 @@ export default {
986
1222
  // grid's own position back, which matches and does
987
1223
  // nothing here.
988
1224
  const x = item.gridX ?? node.x
989
- const y = item.gridY ?? node.y
1225
+ const y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y
990
1226
  if (x !== node.x || y !== node.y) {
991
1227
  // The layout said where it goes; GridStack's `change`
992
1228
  // for it is not a user's arrangement and is not
@@ -4587,6 +4587,11 @@ export default {
4587
4587
  if (columns !== undefined) {
4588
4588
  out.columns = Number(columns)
4589
4589
  }
4590
+ // `tiles: "box"`: each grid tile on a surface of its own.
4591
+ const tiles = content.tiles !== undefined ? content.tiles : props.tiles
4592
+ if (typeof tiles === 'string') {
4593
+ out.tiles = tiles
4594
+ }
4590
4595
  const contentEntries = content.entries !== undefined ? content.entries : props.entries
4591
4596
  if (contentEntries !== undefined) {
4592
4597
  out.entries = contentEntries
@@ -511,7 +511,7 @@ export default {
511
511
  * @return {string} The formatted string.
512
512
  */
513
513
  formatNumber(value) {
514
- return formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig))
514
+ return formatMetricValue(value, this.resolvedFormat, unwrapAppConfig(this.cnAppConfig), null, this.effectiveTranslate)
515
515
  },
516
516
 
517
517
  /**
@@ -446,6 +446,8 @@ When the schema-driven fetch answers 404 the page shows a not-found state whose
446
446
 
447
447
  `headerCard` (default `false`) draws the header as a bordered card. `headerWidget` (default `''`) names a widget in `widgets` to render inside the header under the title, without a card of its own and out of the body grid: the stages bars of the board's case card.
448
448
 
449
+ `visibilityContext` (default `{}`) names records the page reads beside the loaded one, `{ <name>: { field, register, schema } }`. A grid widget's `visibleWhen` is read against the record, and against those records as `@context.<name>.<path>`, so a case page can show a widget for some case types only (`{ "field": "@context.caseType.category", "op": "in", "value": ["Sociaal domein"] }`). A widget without `visibleWhen` shows as before; a conditional one stays out of the grid until the record and the named records have loaded.
450
+
449
451
  `showWidgetActions` (Boolean, default `true`): `false` (manifest `config.showWidgetActions: false`) drops the overflow Actions menu from the cards of the body grid and the side column, unless a widget definition sets `showActions: true`.
450
452
 
451
453
  ### Favourite and follow