@conduction/nextcloud-vue 2.139.0 → 2.141.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 (144) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +9 -2
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  5. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
  6. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +28 -4
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +2 -2
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +64 -1
  15. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +76 -37
  17. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  19. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +106 -0
  20. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +30 -7
  22. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +5 -3
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +59 -3
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +44 -34
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  37. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +28 -1
  38. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +7 -1
  41. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  42. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
  43. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -2
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  51. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  52. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
  53. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -3
  57. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +2 -0
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +20 -0
  70. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
  72. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +1 -1
  74. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +12 -1
  75. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
  76. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +1 -1
  77. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +1 -1
  79. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +23 -1
  80. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -1
  81. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +43 -13
  82. package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -1
  83. package/dist/esm/composables/indexSources.js +5 -2
  84. package/dist/esm/composables/indexSources.js.map +1 -1
  85. package/dist/esm/composables/useTaskInboxStore.js +4 -3
  86. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +3 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +6 -1
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/library-style.js +1 -1
  92. package/dist/esm/nextcloud-vue.css +961 -605
  93. package/dist/esm/utils/builtInFormatters.js +23 -1
  94. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  95. package/dist/esm/utils/fillTemplate.js +7 -1
  96. package/dist/esm/utils/fillTemplate.js.map +1 -1
  97. package/dist/esm/utils/groupSublines.js +120 -0
  98. package/dist/esm/utils/groupSublines.js.map +1 -0
  99. package/dist/esm/utils/myTeam.js +24 -4
  100. package/dist/esm/utils/myTeam.js.map +1 -1
  101. package/dist/esm/utils/resolveFilterTokens.js +5 -4
  102. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  103. package/dist/esm/utils/transitionInputs.js +43 -2
  104. package/dist/esm/utils/transitionInputs.js.map +1 -1
  105. package/dist/nextcloud-vue.cjs.js +1731 -846
  106. package/dist/nextcloud-vue.cjs.js.map +2 -2
  107. package/dist/nextcloud-vue.css +961 -605
  108. package/l10n/en.json +3 -1
  109. package/l10n/nl.json +6 -1
  110. package/package.json +1 -1
  111. package/src/components/CnAppRoot/CnAppRoot.vue +9 -2
  112. package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
  113. package/src/components/CnCellRenderer/CnCellRenderer.vue +30 -6
  114. package/src/components/CnChartWidget/CnChartWidget.md +2 -0
  115. package/src/components/CnChartWidget/CnChartWidget.vue +121 -5
  116. package/src/components/CnConversationThread/CnConversationWidget.vue +126 -1
  117. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +6 -3
  119. package/src/components/CnFactsWidget/CnFactsWidget.vue +72 -5
  120. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +28 -1
  121. package/src/components/CnFilesBrowser/filesBrowser.js +7 -1
  122. package/src/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
  123. package/src/components/CnIndexPage/CnIndexPage.vue +30 -2
  124. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +101 -3
  125. package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
  126. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +28 -3
  127. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
  128. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
  129. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +43 -9
  130. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +68 -2
  131. package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +43 -1
  132. package/src/composables/indexSources.js +5 -2
  133. package/src/composables/useTaskInboxStore.js +4 -3
  134. package/src/css/dialog.css +13 -1
  135. package/src/css/look-board-detail.css +12 -9
  136. package/src/css/look-board-index.css +90 -7
  137. package/src/css/look-board.css +102 -2
  138. package/src/css/table.css +25 -0
  139. package/src/utils/builtInFormatters.js +22 -0
  140. package/src/utils/fillTemplate.js +7 -1
  141. package/src/utils/groupSublines.js +125 -0
  142. package/src/utils/myTeam.js +24 -4
  143. package/src/utils/resolveFilterTokens.js +5 -4
  144. package/src/utils/transitionInputs.js +42 -1
package/l10n/en.json CHANGED
@@ -2380,7 +2380,9 @@
2380
2380
  "You save the order along with a view.": "You save the order along with a view.",
2381
2381
  "me": "me",
2382
2382
  "Open profile": "Open profile",
2383
- "Link a contact from your address book.": "Link a contact from your address book."
2383
+ "Link a contact from your address book.": "Link a contact from your address book.",
2384
+ "The value could not be shown.": "The value could not be shown.",
2385
+ "No value": "No value"
2384
2386
  },
2385
2387
  "plurals": {
2386
2388
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2630,7 +2630,12 @@
2630
2630
  "You save the order along with a view.": "De sortering bewaart u mee in een weergave.",
2631
2631
  "me": "ik",
2632
2632
  "Open profile": "Profiel openen",
2633
- "Link a contact from your address book.": "Koppel een contact uit je adresboek."
2633
+ "Link a contact from your address book.": "Koppel een contact uit je adresboek.",
2634
+ "The value could not be shown.": "De waarde kon niet worden getoond.",
2635
+ "Save as draft": "Opslaan als concept",
2636
+ "Draft saved at {time}": "Concept opgeslagen om {time}",
2637
+ "The draft could not be saved": "Het concept kon niet worden opgeslagen",
2638
+ "No value": "Geen waarde"
2634
2639
  },
2635
2640
  "plurals": {
2636
2641
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.139.0",
3
+ "version": "2.141.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -770,9 +770,10 @@ import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
770
770
  import { BUILT_IN_KB_PROVIDERS } from '../../utils/kbSearchProviders.js'
771
771
  import { createManifestTranslate } from '../../utils/manifestTranslate.js'
772
772
  import { installModalStack, uninstallModalStack } from '../../utils/modalStack.js'
773
+ import { loadMyTeam } from '../../utils/myTeam.js'
773
774
  import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
774
775
  import { resolveBrand } from '../../utils/resolveBrand.js'
775
- import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
776
+ import { dropOptionalUnresolved, MY_TEAM_TOKEN, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
776
777
  import { pageOffersSavedViews } from '../../utils/savedViewHelpers.js'
777
778
  import { normalizeSavedViewMenu, SAVED_VIEW_MENU_KEY, SAVED_VIEW_MENU_PREFIX, syncSavedViewMenu, toggleSavedViewMenuEntry, withSavedViewMenu } from '../../utils/savedViewMenu.js'
778
779
  import { passesContextPredicates } from '../../utils/visibleIfContext.js'
@@ -4475,7 +4476,13 @@ export default {
4475
4476
  async _fetchMenuItemCount(item) {
4476
4477
  try {
4477
4478
  const { register, schema, filter } = item.count
4478
- const resolved = resolveFilterTokens(filter && typeof filter === 'object' ? filter : {}, {})
4479
+ const raw = filter && typeof filter === 'object' ? filter : {}
4480
+ // `@myTeam` counts as the list filters: once the team is read,
4481
+ // every team the app answers, and without one no team at all.
4482
+ if (Object.values(raw).some((value) => value === MY_TEAM_TOKEN || (Array.isArray(value) && value.includes(MY_TEAM_TOKEN)))) {
4483
+ await loadMyTeam()
4484
+ }
4485
+ const resolved = dropOptionalUnresolved(resolveFilterTokens(raw, {}))
4479
4486
  const url = generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}`)
4480
4487
  const { data } = await axios.get(url, { params: { _limit: 1, ...resolved } })
4481
4488
  const total = data?.total
@@ -89,6 +89,7 @@ import { generateUrl } from '@nextcloud/router'
89
89
  import { NcButton, NcCheckboxRadioSwitch, NcTextArea } from '@nextcloud/vue'
90
90
  import { fetchEndpointSource, interpolateUrlTokens } from '../../composables/useEndpointSource.js'
91
91
  import { fillTemplate } from '../../utils/fillTemplate.js'
92
+ import { groupSublines } from '../../utils/groupSublines.js'
92
93
  import { readPath } from '../../utils/readPath.js'
93
94
  import { searchNextcloudGroups, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
94
95
  import { CnDialog } from '../CnDialog/index.js'
@@ -134,7 +135,10 @@ export function withTypedAssignee(found, typed, label) {
134
135
  * `eyebrow` a template filled from the record.
135
136
  * - `source`: an endpointSource to read the record from, else `object`.
136
137
  * - `assignee`: `{ kinds?, label?, placeholder?, hint?, currentPath?,
137
- * currentHint?, currentAs?, allowTyped?, field?, kindField? }`. `kinds` is `["group"]`
138
+ * currentHint?, currentAs?, allowTyped?, field?, kindField?, describe? }`.
139
+ * `describe` (`{ register, schema, groupField?, textField? }`) names the
140
+ * records that hold a team's description; each team option then reads
141
+ * "{count} members · {description}", the count where the viewer may read it. `kinds` is `["group"]`
138
142
  * (default), `["user"]` or both; `currentPath` reads who has it now from the
139
143
  * record and `currentHint` ("Now with {current}.", the placeholder named
140
144
  * by `currentAs`) says so, in place of `hint`; `allowTyped`
@@ -398,8 +402,12 @@ export default {
398
402
  if (seq !== this.searchSeq) {
399
403
  return
400
404
  }
405
+ const lines = await groupSublines(groups.map((g) => g.id), typed, this.assigneeConfig.describe || null)
406
+ if (seq !== this.searchSeq) {
407
+ return
408
+ }
401
409
  const found = [
402
- ...groups.map((g) => ({ id: g.id, label: g.label, secondary: g.subline || '', icon: 'AccountGroupOutline', kind: 'group' })),
410
+ ...groups.map((g) => ({ id: g.id, label: g.label, secondary: lines[g.id] || g.subline || '', icon: 'AccountGroupOutline', kind: 'group' })),
403
411
  ...users.map((u) => ({ id: u.id, label: u.label, secondary: u.subline || '', avatar: true, kind: 'user' })),
404
412
  ]
405
413
  this.options = this.assigneeConfig.allowTyped ? withTypedAssignee(found, typed, t('nextcloud-vue', 'Use "{name}"', { name: typed })) : found
@@ -56,7 +56,7 @@
56
56
  <!-- Built-in "label" widget: the value's label (an enum's translated
57
57
  label) as plain text, where a list wants words and no pill. -->
58
58
  <template v-else-if="widget === 'label'">
59
- <span v-if="hasValue" :title="rawTitle">{{ labelledValue }}</span>
59
+ <span v-if="hasValue" :title="rawTitle">{{ shownLabel }}</span>
60
60
  <span v-else class="cn-cell-renderer__dash">—</span>
61
61
  </template>
62
62
 
@@ -65,7 +65,7 @@
65
65
  shapes it; the colour stays keyed on the raw value. -->
66
66
  <template v-else-if="widget === 'badge'">
67
67
  <CnStatusBadge v-if="hasValue"
68
- :label="labelledValue"
68
+ :label="shownLabel"
69
69
  :variant="badgeVariant"
70
70
  :colorMap="badgeColorMap"
71
71
  :colorKey="badgeColorKey"
@@ -438,7 +438,7 @@ import { ageInDays, boardDateLocale, formatAge, formatBoardDate, formatRelativeD
438
438
  import { daysUntil, parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'
439
439
  import { pillLook } from '../../utils/detailActionModel.js'
440
440
  import { resolveDue } from '../../utils/dueText.js'
441
- import { formatBoardDateTime } from '../../utils/fillTemplate.js'
441
+ import { fillTemplate, formatBoardDateTime } from '../../utils/fillTemplate.js'
442
442
  import { formatMoney } from '../../utils/formatMetric.js'
443
443
  import { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'
444
444
  import { objectFieldValue } from '../../utils/objectName.js'
@@ -1455,6 +1455,22 @@ export default {
1455
1455
  * @return {string}
1456
1456
  * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-a-built-in-cell-widget-shows-an-enum-value-by-its-label
1457
1457
  */
1458
+ /**
1459
+ * What a `label` or `badge` cell shows: its label, worded by
1460
+ * `widgetProps.textTemplate` when given (translated, filled from the
1461
+ * row, `{value}` the label; DqPostvak "Lijkt op 2026-0079").
1462
+ *
1463
+ * @return {string}
1464
+ * @spec openspec/changes/cell-text-template/specs/cell-text-template/spec.md
1465
+ */
1466
+ shownLabel() {
1467
+ const template = this.widgetProps && this.widgetProps.textTemplate
1468
+ if (typeof template !== 'string' || template === '') {
1469
+ return this.labelledValue
1470
+ }
1471
+ return fillTemplate(this.cnTranslate(template), { ...(this.row || {}), value: this.labelledValue })
1472
+ },
1473
+
1458
1474
  labelledValue() {
1459
1475
  if (!this.hasFormatter && this.isEnum && this.hasValue && typeof this.value !== 'object') {
1460
1476
  return this.enumLabel
@@ -1721,8 +1737,9 @@ export default {
1721
1737
 
1722
1738
  /**
1723
1739
  * The swatch colour — read from the sibling row field named by
1724
- * `format.colorField` (default `'color'`). Returns null when no usable
1725
- * colour string is present (the dot is then omitted).
1740
+ * `format.colorField` (default `'color'`), through `format.colorMap`
1741
+ * when given (a stored name such as `blue` to a CSS colour). Returns
1742
+ * null when no usable colour is present (the dot is then omitted).
1726
1743
  *
1727
1744
  * @return {string|null}
1728
1745
  */
@@ -1732,7 +1749,14 @@ export default {
1732
1749
  }
1733
1750
  const field = (this.format && this.format.colorField) || 'color'
1734
1751
  const c = this.row && this.row[field]
1735
- return (typeof c === 'string' && c.trim() !== '') ? c : null
1752
+ if (typeof c !== 'string' || c.trim() === '') {
1753
+ return null
1754
+ }
1755
+ const map = this.format && this.format.colorMap
1756
+ if (map && typeof map === 'object') {
1757
+ return typeof map[c] === 'string' && map[c] !== '' ? map[c] : null
1758
+ }
1759
+ return c
1736
1760
  },
1737
1761
 
1738
1762
  formattedValue() {
@@ -109,6 +109,7 @@ Fallback slot — shown when ApexCharts is not available:
109
109
  | `barMax` | Number | `null` | `bar-list`: the value a full track stands for (100 for percentages); null takes the largest value |
110
110
  | `matrixLabelWidth` | Number | `null` | `matrix`: the row-label column's width in px; a long label wraps (DqProcesanalyse "Dagen per fase": 240). Null sizes the column to its longest label, at least 120px |
111
111
  | `footnote` | String | `''` | A line under the chart, through the host's translate: under the rows of a `bar-list` or `matrix`, under the plot of a `line`, `area` or `bar` chart |
112
+ | `notice` | Object | `null` | A toned note card under the chart: `{ lead?, text?, values?, labels?, tone? }`. `{name}` in `lead` (bold) and `text` is filled from `values[name]`, a literal or a record token (`@object.<field>`); `labels[name]` (`"rows"`, `"columns"`, `"values"` or a map) turns a key into words. `tone`: info, success, warning, error. Hidden while a value is missing |
112
113
  | `referenceLines` | Array | `[]` | Dashed horizontal reference lines on the value axis of a line, area or bar chart, such as a norm: `[{ value, label?, color? }]`. The label goes through the host's translate; the colour defaults to the theme's error colour (board DqDoorlooptijd). `valuePath` reads the value from the endpoint answer instead, `{value}` in the label writes it, and a line without a value is not drawn. |
113
114
  | `categoryFormat` | String | `''` | How date categories read on the x-axis: `month` ("jan") or `month-year` ("jan 2026"), in the user's language, or `week` for ISO week keys ("2026-W40" as "w40"). Categories that are not dates are left as is; a bare year key stays a year. |
114
115
  | `emptyCellLabel` | String | `''` | `matrix`: the text of a cell without a value ("geen"), through the host's translate. Empty leaves the cell blank |
@@ -134,3 +135,4 @@ The `dataSource` OBJECT-form `aggregate` (Wave 3, #91) — `{ groupBy, metric?:
134
135
  | Prop | Type | Default | Description |
135
136
  |------|------|---------|-------------|
136
137
  | `widgetId` | String | `''` | Matches `cn:widget:refresh` event-bus events; on a matching id the chart re-queries its `dataSource`. Also exposes a ref-callable `refresh()`. |
138
+ | `barValueAtEnd` | Boolean | `false` | A `bar-list` without a track: each bar as long as its share and its value right after the bar's end (DqProcesanalyse "Doorlooptijd per status"). False keeps the track and the value column. |
@@ -65,7 +65,7 @@
65
65
  share of `barMax` (default the largest value), the value at the
66
66
  end. Rows navigate when the source declares a drilldown. -->
67
67
  <div v-else-if="type === 'bar-list'" class="cn-chart-widget__html" data-testid="cn-chart-widget-bar-list">
68
- <ul class="cn-chart-widget__bar-list">
68
+ <ul class="cn-chart-widget__bar-list" :class="{ 'cn-chart-widget__bar-list--value-at-end': barValueAtEnd }">
69
69
  <li v-for="row in barListRows" :key="row.index" class="cn-chart-widget__bar-row">
70
70
  <component
71
71
  :is="row.drill ? 'button' : 'div'"
@@ -75,10 +75,16 @@
75
75
  data-testid="cn-chart-widget-bar-row"
76
76
  @click="row.drill ? onDataPointSelection(null, null, { dataPointIndex: row.index }) : null">
77
77
  <span class="cn-chart-widget__bar-label">{{ row.label }}</span>
78
- <span class="cn-chart-widget__bar-track" aria-hidden="true">
79
- <span class="cn-chart-widget__bar-fill" :style="{ width: `${row.share}%` }" />
78
+ <span v-if="barValueAtEnd" class="cn-chart-widget__bar-track">
79
+ <span class="cn-chart-widget__bar-fill" aria-hidden="true" :style="{ flexBasis: `${row.share}%` }" />
80
+ <span class="cn-chart-widget__bar-value">{{ row.text }}</span>
80
81
  </span>
81
- <span class="cn-chart-widget__bar-value">{{ row.text }}</span>
82
+ <template v-else>
83
+ <span class="cn-chart-widget__bar-track" aria-hidden="true">
84
+ <span class="cn-chart-widget__bar-fill" :style="{ width: `${row.share}%` }" />
85
+ </span>
86
+ <span class="cn-chart-widget__bar-value">{{ row.text }}</span>
87
+ </template>
82
88
  </component>
83
89
  </li>
84
90
  </ul>
@@ -148,12 +154,25 @@
148
154
  data-testid="cn-chart-widget-footnote">
149
155
  {{ hostTranslate(footnote) }}
150
156
  </p>
157
+ <!-- `notice`: a toned note card under the chart whose words carry
158
+ values from the page record (DqHandhaving "Advies: ..."). -->
159
+ <NcNoteCard
160
+ v-if="noticeParts"
161
+ :type="noticeParts.type"
162
+ :data-tone="noticeParts.type"
163
+ class="cn-chart-widget__notice"
164
+ data-testid="cn-chart-widget-notice">
165
+ <p class="cn-chart-widget__notice-text">
166
+ <strong v-if="noticeParts.lead">{{ noticeParts.lead }}</strong>{{ noticeParts.lead && noticeParts.text ? ' ' : '' }}{{ noticeParts.text }}
167
+ </p>
168
+ </NcNoteCard>
151
169
  </div>
152
170
  </template>
153
171
 
154
172
  <script>
155
173
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
156
174
  import { getLanguage, translate as t } from '@nextcloud/l10n'
175
+ import { NcNoteCard } from '@nextcloud/vue'
157
176
  import { inject, markRaw, ref } from 'vue'
158
177
  import VueApexCharts from 'vue3-apexcharts'
159
178
  import { useDataSource } from '../../composables/useDataSource.js'
@@ -405,7 +424,7 @@ function formatterFor(vf) {
405
424
  export default {
406
425
  name: 'CnChartWidget',
407
426
 
408
- components: { CnEndpointNotice },
427
+ components: { CnEndpointNotice, NcNoteCard },
409
428
 
410
429
  inject: {
411
430
  /**
@@ -455,6 +474,18 @@ export default {
455
474
  default: null,
456
475
  },
457
476
 
477
+ /**
478
+ * A `bar-list` without a track: each bar as long as its share and its
479
+ * value right after the bar's end (DqProcesanalyse "Doorlooptijd per
480
+ * status"). False, the default, keeps the track and the value column.
481
+ *
482
+ * @spec openspec/changes/chart-bar-list-value-at-end/specs/chart-bar-list-value-at-end/spec.md#requirement-a-bar-list-can-put-each-value-at-its-bars-end
483
+ */
484
+ barValueAtEnd: {
485
+ type: Boolean,
486
+ default: false,
487
+ },
488
+
458
489
  /**
459
490
  * A `matrix`'s row-label column width in px. Null, the default, sizes
460
491
  * the column to its longest label (at least 120px); a width wraps a
@@ -467,6 +498,24 @@ export default {
467
498
  default: null,
468
499
  },
469
500
 
501
+ /**
502
+ * A toned note card under the chart whose words carry values from the
503
+ * page record: `{ lead?, text?, values?, labels?, tone? }`. `lead`
504
+ * (bold) and `text` go through the host's translate, then each
505
+ * `{name}` is filled from `values[name]`, a literal or a token such as
506
+ * `@object.recommendedIntervention`. `labels[name]` turns a key into
507
+ * words: `"rows"`, `"columns"` or `"values"` read the matrix's
508
+ * cellSource label maps, an object is its own map. `tone` is info
509
+ * (default), success, warning or error. Hidden while a value is
510
+ * missing, so a half-filled sentence never shows.
511
+ *
512
+ * @spec openspec/changes/chart-notice/specs/chart-notice/spec.md#requirement-a-chart-can-carry-a-notice-with-record-values
513
+ */
514
+ notice: {
515
+ type: Object,
516
+ default: null,
517
+ },
518
+
470
519
  /**
471
520
  * A line under the chart ("Klik een balk om de zaken te zien"),
472
521
  * through the host's translate: under the rows of a `bar-list` or
@@ -1601,6 +1650,38 @@ export default {
1601
1650
  }
1602
1651
  },
1603
1652
 
1653
+ /**
1654
+ * The notice's bold lead, text and note-card type, values filled in;
1655
+ * null when there is none or a value is missing.
1656
+ *
1657
+ * @return {{lead: string, text: string, type: string}|null} The notice.
1658
+ * @spec openspec/changes/chart-notice/specs/chart-notice/spec.md#requirement-a-chart-can-carry-a-notice-with-record-values
1659
+ */
1660
+ noticeParts() {
1661
+ const def = this.notice
1662
+ if (!def || typeof def !== 'object' || (!def.lead && !def.text)) {
1663
+ return null
1664
+ }
1665
+ const declared = def.values && typeof def.values === 'object' ? def.values : {}
1666
+ const resolved = resolveFilterTokens(declared, this.chartTokenCtx())
1667
+ const source = this.cellSource && typeof this.cellSource === 'object' ? this.cellSource : {}
1668
+ const maps = { rows: source.rowLabels, columns: source.columnLabels, values: source.valueLabels }
1669
+ const words = {}
1670
+ for (const name of Object.keys(declared)) {
1671
+ const raw = resolved[name]
1672
+ if (raw === null || raw === undefined || raw === '' || (typeof raw === 'string' && raw.charAt(0) === '@')) {
1673
+ return null
1674
+ }
1675
+ const ref = def.labels && def.labels[name]
1676
+ const map = typeof ref === 'string' ? maps[ref] : ref
1677
+ const label = map && typeof map === 'object' && typeof map[String(raw)] === 'string' ? map[String(raw)] : null
1678
+ words[name] = label ? this.hostTranslate(label) : String(raw)
1679
+ }
1680
+ const fill = (key) => (key ? this.hostTranslate(key).replace(/\{(\w+)\}/g, (match, name) => (name in words ? words[name] : match)) : '')
1681
+ const type = ['info', 'success', 'warning', 'error'].includes(def.tone) ? def.tone : 'info'
1682
+ return { lead: fill(def.lead), text: fill(def.text), type }
1683
+ },
1684
+
1604
1685
  /**
1605
1686
  * The matrix cell of the record (`highlight: { row, column }`, tokens
1606
1687
  * such as `@object.behaviour` resolved), as row and column indexes. A
@@ -2943,11 +3024,46 @@ export default {
2943
3024
  white-space: nowrap;
2944
3025
  }
2945
3026
 
3027
+ /* `barValueAtEnd`: no track; the bar takes its share of the row and gives way
3028
+ to the value right after its end, so the longest bar still fits. */
3029
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-line {
3030
+ grid-template-columns: minmax(80px, 130px) minmax(0, 1fr);
3031
+ }
3032
+
3033
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-track {
3034
+ align-items: center;
3035
+ background: none;
3036
+ display: flex;
3037
+ gap: 6px;
3038
+ height: auto;
3039
+ overflow: visible;
3040
+ }
3041
+
3042
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-fill {
3043
+ border-radius: 4px;
3044
+ flex: 0 1 auto;
3045
+ height: 18px;
3046
+ min-width: 2px;
3047
+ }
3048
+
3049
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-value {
3050
+ flex: none;
3051
+ text-align: start;
3052
+ }
3053
+
2946
3054
  .cn-chart-widget__footnote {
2947
3055
  color: var(--color-text-maxcontrast);
2948
3056
  margin: 0;
2949
3057
  }
2950
3058
 
3059
+ .cn-chart-widget__notice {
3060
+ margin: 12px 0 0;
3061
+ }
3062
+
3063
+ .cn-chart-widget__notice-text {
3064
+ margin: 0;
3065
+ }
3066
+
2951
3067
  /* Under an ApexCharts plot the footnote is a sibling of the canvas, so it keeps
2952
3068
  its own line height while the canvas takes the rest of a fitted column. */
2953
3069
  .cn-chart-widget > .cn-chart-widget__footnote {
@@ -6,6 +6,7 @@
6
6
  <template>
7
7
  <div class="cn-case-widget-card">
8
8
  <CnConversationThread
9
+ ref="thread"
9
10
  :messages="messages"
10
11
  :title="tr(content.title || '')"
11
12
  :subtitle="tr(content.subtitle || '')"
@@ -34,7 +35,7 @@
34
35
  data-testid="cn-conversation-templates"
35
36
  @apply="applyTemplate($event, setDraft)" />
36
37
  </template>
37
- <template v-if="replyOptions.length > 0" #reply-actions>
38
+ <template v-if="replyOptions.length > 0 || draftUrl" #reply-actions="{ draft }">
38
39
  <NcCheckboxRadioSwitch
39
40
  v-for="option in replyOptions"
40
41
  :key="option.key"
@@ -42,6 +43,19 @@
42
43
  :data-testid="`cn-conversation-option-${option.key}`">
43
44
  {{ tr(option.label) }}
44
45
  </NcCheckboxRadioSwitch>
46
+ <span v-if="draftUrl && draftStatus"
47
+ class="cn-conversation-widget__draft-status"
48
+ role="status"
49
+ data-testid="cn-conversation-draft-status">
50
+ {{ draftStatus }}
51
+ </span>
52
+ <NcButton
53
+ v-if="draftUrl"
54
+ data-testid="cn-conversation-save-draft"
55
+ :disabled="savingDraft"
56
+ @click="saveDraft(draft)">
57
+ {{ t('nextcloud-vue', 'Save as draft') }}
58
+ </NcButton>
45
59
  </template>
46
60
  <template v-if="headerAction || statusBadgeText" #header-actions>
47
61
  <span v-if="statusBadgeText"
@@ -71,11 +85,13 @@
71
85
 
72
86
  <script>
73
87
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
88
+ import { translate as t } from '@nextcloud/l10n'
74
89
  import { NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
75
90
  import CnIcon from '../CnIcon/CnIcon.vue'
76
91
  import CnTemplateSelect from '../CnTemplateSelect/CnTemplateSelect.vue'
77
92
  import CnConversationThread from './CnConversationThread.vue'
78
93
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
94
+ import { formatTimeOfDay } from '../../utils/boardDate.js'
79
95
  import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'
80
96
  import { readPath } from '../../utils/readPath.js'
81
97
  import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
@@ -178,6 +194,10 @@ export default {
178
194
  acting: '',
179
195
  /** The threads `statusBadge` reads. */
180
196
  badgeRows: [],
197
+ /** What the last draft save said: when it was saved, or that it failed. */
198
+ draftStatus: '',
199
+ /** Whether a draft is being saved. */
200
+ savingDraft: false,
181
201
  }
182
202
  },
183
203
 
@@ -315,6 +335,21 @@ export default {
315
335
  * @return {string} The URL, or '' when the content names none.
316
336
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
317
337
  */
338
+ /**
339
+ * The reply draft's url (`content.draft.url`, `@objectId` filled in),
340
+ * or '' without one.
341
+ *
342
+ * @return {string}
343
+ * @spec openspec/changes/conversation-reply-draft/specs/conversation-reply-draft/spec.md#requirement-a-reply-can-be-saved-as-a-draft-and-comes-back
344
+ */
345
+ draftUrl() {
346
+ const url = this.content.draft && this.content.draft.url
347
+ if (typeof url !== 'string' || url === '') {
348
+ return ''
349
+ }
350
+ return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
351
+ },
352
+
318
353
  endpoint() {
319
354
  const url = this.content.endpoint
320
355
  if (typeof url !== 'string' || url === '') {
@@ -340,19 +375,101 @@ export default {
340
375
  handler() {
341
376
  this.loadSource()
342
377
  this.loadBadge()
378
+ this.loadDraft()
343
379
  },
344
380
  },
345
381
  },
346
382
 
347
383
  mounted() {
348
384
  subscribe('cn:page:refresh', this.onPageRefresh)
385
+ subscribe('cn:conversation:reply-sent', this.onReplySentElsewhere)
349
386
  },
350
387
 
351
388
  beforeUnmount() {
352
389
  unsubscribe('cn:page:refresh', this.onPageRefresh)
390
+ unsubscribe('cn:conversation:reply-sent', this.onReplySentElsewhere)
353
391
  },
354
392
 
355
393
  methods: {
394
+ t,
395
+
396
+ /**
397
+ * Read the saved reply draft into the reply box.
398
+ *
399
+ * @return {Promise<void>}
400
+ * @spec openspec/changes/conversation-reply-draft/specs/conversation-reply-draft/spec.md#requirement-a-reply-can-be-saved-as-a-draft-and-comes-back
401
+ */
402
+ async loadDraft() {
403
+ if (!this.draftUrl) {
404
+ return
405
+ }
406
+ try {
407
+ const { default: axios } = await import('@nextcloud/axios')
408
+ const { generateUrl } = await import('@nextcloud/router')
409
+ const { data } = await axios.get(generateUrl(this.draftUrl))
410
+ const text = data && typeof data.text === 'string' ? data.text : ''
411
+ if (text !== '' && this.$refs.thread) {
412
+ this.$refs.thread.setDraft(text)
413
+ }
414
+ } catch {
415
+ // No saved draft to read leaves the box empty.
416
+ }
417
+ },
418
+
419
+ /**
420
+ * Store the reply box as the draft and say when it was saved.
421
+ *
422
+ * @param {string} text The reply box's text.
423
+ * @return {Promise<void>}
424
+ */
425
+ async saveDraft(text) {
426
+ this.savingDraft = true
427
+ try {
428
+ await this.storeDraft(String(text ?? ''))
429
+ this.draftStatus = t('nextcloud-vue', 'Draft saved at {time}', { time: formatTimeOfDay(new Date()) })
430
+ } catch {
431
+ this.draftStatus = t('nextcloud-vue', 'The draft could not be saved')
432
+ } finally {
433
+ this.savingDraft = false
434
+ }
435
+ },
436
+
437
+ /**
438
+ * A reply on this record went out through a dialog or another surface
439
+ * (`cn:conversation:reply-sent` with `{ objectId }`): the saved draft
440
+ * and the reply box are cleared.
441
+ *
442
+ * @param {{objectId?: string}} payload The record the reply was on.
443
+ * @return {Promise<void>}
444
+ * @spec openspec/changes/conversation-reply-draft/specs/conversation-reply-draft/spec.md#requirement-a-reply-can-be-saved-as-a-draft-and-comes-back
445
+ */
446
+ async onReplySentElsewhere(payload) {
447
+ if (!this.draftUrl || String(payload?.objectId ?? '') !== String(this.objectId ?? '')) {
448
+ return
449
+ }
450
+ if (this.$refs.thread) {
451
+ this.$refs.thread.setDraft('')
452
+ }
453
+ this.draftStatus = ''
454
+ try {
455
+ await this.storeDraft('')
456
+ } catch {
457
+ // The next save overwrites a draft that could not be cleared.
458
+ }
459
+ },
460
+
461
+ /**
462
+ * PUT the draft text.
463
+ *
464
+ * @param {string} text The text; '' clears the draft.
465
+ * @return {Promise<void>}
466
+ */
467
+ async storeDraft(text) {
468
+ const { default: axios } = await import('@nextcloud/axios')
469
+ const { generateUrl } = await import('@nextcloud/router')
470
+ await axios.put(generateUrl(this.draftUrl), { text })
471
+ },
472
+
356
473
  /**
357
474
  * Read the messages again when the page refreshes (a dialog sent one).
358
475
  *
@@ -691,6 +808,9 @@ export default {
691
808
  if (typeof text !== 'string' || text === '') {
692
809
  return
693
810
  }
811
+ if (this.draftUrl) {
812
+ this.storeDraft('')
813
+ }
694
814
  this.sentHere.push({
695
815
  id: `sent-${this.sentHere.length}`,
696
816
  time: new Date().toISOString(),
@@ -722,4 +842,9 @@ export default {
722
842
  border-radius: var(--border-radius-large);
723
843
  padding: calc(5 * var(--default-grid-baseline));
724
844
  }
845
+
846
+ .cn-conversation-widget__draft-status {
847
+ color: var(--color-text-maxcontrast);
848
+ font-size: 13px;
849
+ }
725
850
  </style>
@@ -1150,6 +1150,7 @@ const CHART_PROP_KEYS = [
1150
1150
  'endpointSource',
1151
1151
  // `bar-list` / `matrix` presentation keys.
1152
1152
  'barMax',
1153
+ 'barValueAtEnd',
1153
1154
  'matrixLabelWidth',
1154
1155
  'footnote',
1155
1156
  'highlight',
@@ -6,6 +6,7 @@
6
6
  'cn-table-container--board': isBoardLook,
7
7
  'cn-table-container--title-plain': isBoardLook && rowTitle === 'plain',
8
8
  'cn-table-container--title-text': isBoardLook && rowTitle === 'text',
9
+ 'cn-table-container--title-strong': isBoardLook && rowTitle === 'strong',
9
10
  'cn-table-container--scrollable': scrollable,
10
11
  'cn-table-container--borderless': borderless,
11
12
  'cn-table-container--fill': fillHeight,
@@ -856,8 +857,10 @@ export default {
856
857
  * the 15px bold title without an underline and a 13px muted secondary
857
858
  * line, as the PqTickets and OcPublicaties boards draw it; `text` is an
858
859
  * ordinary cell, for a log table whose first column is a date
859
- * (DqZaakContact). The row stays clickable either way. The Nextcloud
860
- * look ignores it.
860
+ * (DqZaakContact); `strong` is the cell's own size at weight 600 in the
861
+ * main ink without an underline, the name column of a card's list
862
+ * (DqZaaktypeStatussen). The row stays clickable either way. The
863
+ * Nextcloud look ignores it.
861
864
  *
862
865
  * @spec openspec/changes/screens-table-rows-parity/specs/index-list-board-look/spec.md#requirement-a-row-title-can-be-plain-text
863
866
  * @spec openspec/changes/board-log-table-plain-cells/specs/board-log-table-plain-cells/spec.md#requirement-a-tables-title-column-can-read-as-plain-text
@@ -865,7 +868,7 @@ export default {
865
868
  rowTitle: {
866
869
  type: String,
867
870
  default: 'link',
868
- validator: (value) => ['link', 'plain', 'text'].includes(value),
871
+ validator: (value) => ['link', 'plain', 'text', 'strong'].includes(value),
869
872
  },
870
873
 
871
874
  /**