@conduction/nextcloud-vue 2.82.0 → 2.84.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 (178) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  13. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  14. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  17. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  18. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  21. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
  22. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +14 -0
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  30. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  31. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  42. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  43. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  50. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
  53. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
  55. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  57. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
  58. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
  61. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
  62. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
  64. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  70. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
  71. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  72. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
  73. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  74. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
  75. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
  76. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
  78. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  83. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  84. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +4 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  89. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
  90. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
  92. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
  94. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
  95. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
  97. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  99. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  106. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  107. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  108. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  109. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +16 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +41 -4
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +900 -537
  116. package/dist/esm/utils/buildManifest.js +37 -3
  117. package/dist/esm/utils/buildManifest.js.map +1 -1
  118. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  119. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  120. package/dist/esm/utils/fetchAggregate.js +25 -2
  121. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  122. package/dist/esm/utils/formatMetric.js +39 -3
  123. package/dist/esm/utils/formatMetric.js.map +1 -1
  124. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  125. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  126. package/dist/esm/utils/sentinelTokens.js +9 -0
  127. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  128. package/dist/esm/utils/switchValues.js +62 -0
  129. package/dist/esm/utils/switchValues.js.map +1 -0
  130. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  131. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  132. package/dist/nextcloud-vue.cjs.js +36711 -34909
  133. package/dist/nextcloud-vue.cjs.js.map +2 -2
  134. package/dist/nextcloud-vue.css +900 -537
  135. package/l10n/en.json +16 -1
  136. package/l10n/nl.json +41 -4
  137. package/package.json +1 -1
  138. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
  139. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  140. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  141. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  142. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
  143. package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
  144. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  145. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  146. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  147. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  148. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  149. package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
  150. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  151. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  152. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
  153. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
  154. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  155. package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
  156. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
  157. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  158. package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
  159. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
  160. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
  161. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  162. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  163. package/src/css/form-field.css +14 -0
  164. package/src/css/form-widget.css +11 -0
  165. package/src/css/index-page.css +11 -0
  166. package/src/css/index.css +1 -0
  167. package/src/css/look-board-reports.css +175 -0
  168. package/src/css/look-board.css +44 -1
  169. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  170. package/src/schemas/app-manifest-v2.schema.json +24 -3
  171. package/src/utils/buildManifest.js +36 -2
  172. package/src/utils/detailWidgetVisibility.js +145 -0
  173. package/src/utils/fetchAggregate.js +24 -1
  174. package/src/utils/formatMetric.js +38 -2
  175. package/src/utils/resolveManifestTokens.js +1 -0
  176. package/src/utils/sentinelTokens.js +9 -0
  177. package/src/utils/switchValues.js +59 -0
  178. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -183,7 +183,7 @@ export default {
183
183
  /** Channel options `{ value, label }`. */
184
184
  channelOptions() {
185
185
  return Array.isArray(this.content.channels) && this.content.channels.length > 0
186
- ? this.content.channels
186
+ ? this.content.channels.map((channel) => ({ ...channel, label: this.copy(channel && channel.label) }))
187
187
  : [
188
188
  { value: 'telefoon', label: t('nextcloud-vue', 'Phone') },
189
189
  { value: 'email', label: t('nextcloud-vue', 'Email') },
@@ -204,10 +204,11 @@ export default {
204
204
  const prop = properties[this.content.outcomeField || 'outcome']
205
205
  if (prop && Array.isArray(prop.enum) && prop.enum.length > 0) {
206
206
  const labels = prop['x-enum-labels'] || prop.enumLabels || {}
207
- const tr = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
208
- return prop.enum.map((value) => ({ value, label: tr(labels[value] || String(value)) }))
207
+ return prop.enum.map((value) => ({ value, label: this.copy(labels[value] || String(value)) }))
209
208
  }
210
- return Array.isArray(this.content.outcomes) ? this.content.outcomes : []
209
+ return Array.isArray(this.content.outcomes)
210
+ ? this.content.outcomes.map((outcome) => ({ ...outcome, label: this.copy(outcome && outcome.label) }))
211
+ : []
211
212
  },
212
213
 
213
214
  /**
@@ -280,8 +281,7 @@ export default {
280
281
  // `content.submitLabel` names what the form creates, e.g. "Save contact moment".
281
282
  registerLabel() {
282
283
  if (this.content.submitLabel) {
283
- const tr = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
284
- return tr(this.content.submitLabel)
284
+ return this.copy(this.content.submitLabel)
285
285
  }
286
286
  return t('nextcloud-vue', 'Register')
287
287
  },
@@ -359,6 +359,29 @@ export default {
359
359
  },
360
360
 
361
361
  /** The default channel value (first configured channel, else `telefoon`). */
362
+ /**
363
+ * A label the manifest supplies (a channel, an outcome, the submit
364
+ * label), translated: the app's own translation first, since the words
365
+ * are the app's, then the library's catalogue, then the text as
366
+ * written. Before, a channel label was shown as written and the submit
367
+ * label only reached the app's translate, so pipelinq showed "Phone"
368
+ * and "Save contact moment" in English to a Dutch reader.
369
+ *
370
+ * @spec openspec/changes/interaction-form-fits-and-reads-dutch/specs/interaction-form-widget/spec.md#requirement-the-interaction-form-fits-its-card-and-reads-in-the-readers-language
371
+ * @param {string} key The label as the manifest writes it.
372
+ * @return {string} The translated label.
373
+ */
374
+ copy(key) {
375
+ if (typeof key !== 'string' || key === '') {
376
+ return key
377
+ }
378
+ const host = typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : key
379
+ if (typeof host === 'string' && host !== '' && host !== key) {
380
+ return host
381
+ }
382
+ return t('nextcloud-vue', key)
383
+ },
384
+
362
385
  firstChannel() {
363
386
  const ch = (this.content && this.content.channels) || []
364
387
  return (Array.isArray(ch) && ch.length > 0 && ch[0].value) || 'telefoon'
@@ -121,7 +121,7 @@ export default {
121
121
  | `icon` | Object\|Function | `null` | MDI icon component shown in the card header |
122
122
  | `objectType` | String | `''` | Registered object type slug used by `objectStore.saveObject()` |
123
123
  | `store` | Object | `null` | Explicit Pinia objectStore instance; auto-detected via Pinia when omitted |
124
- | `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget } }` |
124
+ | `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. |
125
125
  | `columns` | Number | `3` | Number of CSS grid columns in the widget |
126
126
  | `collapsedFields` (`collapsed-fields`) | Number | `null` | Number of fields shown while collapsed, with a "Show all N fields" button for the rest. `null` fits the cell: the whole rows that fit stay, and the button only appears when the cell overflows |
127
127
  | `hideEmpty` | Boolean | `false` | Hide fields with no value instead of rendering an em dash. For discriminated supertypes (one `ticket` schema holding request/complaint/contactmoment), so the grid is type-aware without the schema enumerating which fields belong to which variant. Read grid only — the field being edited, a field with unsaved changes, and the Edit form stay visible; `false`/`0` are values and are never hidden. |
@@ -418,6 +418,7 @@ import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
418
418
  import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
419
419
  import { schemaRefSlug } from '../../utils/schemaRefSlug.js'
420
420
  import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
421
+ import { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
421
422
  import { CnIcon } from '../CnIcon/index.js'
422
423
  import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
423
424
  import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
@@ -939,13 +940,18 @@ export default {
939
940
  gridRow: (this.resolvedOverrides[field.key] && this.resolvedOverrides[field.key].gridRow) || 1,
940
941
  }))
941
942
 
943
+ // Per-field condition: `overrides[key].visibleWhen` shows a field only
944
+ // for the objects it belongs to, such as a person's preferred name on
945
+ // a client that can also be an organisation.
946
+ const shown = withSpans.filter((field) => this.isVisibleByCondition(field))
947
+
942
948
  if (!this.hideEmpty) {
943
- return withSpans
949
+ return shown
944
950
  }
945
951
 
946
952
  // Keep a field the user is currently editing or has pending changes for,
947
953
  // otherwise it would vanish mid-edit the moment its value is cleared.
948
- return withSpans.filter((field) => field.key === this.editingField
954
+ return shown.filter((field) => field.key === this.editingField
949
955
  || field.key in this.dirtyFields
950
956
  || !this.isEmptyValue((this.objectData || {})[field.key]))
951
957
  },
@@ -1709,6 +1715,29 @@ export default {
1709
1715
  return !field.readOnly
1710
1716
  },
1711
1717
 
1718
+ /**
1719
+ * Evaluate a field's `visibleWhen` override against the object.
1720
+ *
1721
+ * The rule is the shared visibility predicate in its LOCAL mode
1722
+ * (`{ field, op, value }`, `all` / `any`, `appInstalled`), evaluated by
1723
+ * `evaluateVisibleWhenLocal` against the object with its unsaved
1724
+ * changes, so a field appears or goes the moment the field it depends
1725
+ * on changes. A rule that needs a request (`endpoint`, `source`) cannot
1726
+ * be decided here and is ignored: the field shows, as it did before
1727
+ * the rule existed.
1728
+ *
1729
+ * @param {object} field - Resolved field definition.
1730
+ * @return {boolean} True when the field is shown.
1731
+ */
1732
+ isVisibleByCondition(field) {
1733
+ const override = this.resolvedOverrides[field.key]
1734
+ const rule = override ? override.visibleWhen : undefined
1735
+ if (rule === undefined || rule === null || !isLocallyDecidableVisibleWhen(rule)) {
1736
+ return true
1737
+ }
1738
+ return evaluateVisibleWhenLocal(rule, { ...(this.objectData || {}), ...this.dirtyFields })
1739
+ },
1740
+
1712
1741
  /**
1713
1742
  * Evaluate a field's conditional read-only rule against the object data.
1714
1743
  *
@@ -4,7 +4,10 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <fieldset class="cn-radio-cards" data-testid="cn-radio-cards" :aria-busy="busy ? 'true' : 'false'">
7
+ <fieldset class="cn-radio-cards"
8
+ :class="{ 'cn-radio-cards--list': isList }"
9
+ data-testid="cn-radio-cards"
10
+ :aria-busy="busy ? 'true' : 'false'">
8
11
  <legend v-if="label" class="cn-radio-cards__legend">
9
12
  {{ label }}
10
13
  </legend>
@@ -52,7 +55,7 @@ let uid = 0
52
55
  * underneath, so the choice is perceivable without colour and works from the
53
56
  * keyboard.
54
57
  *
55
- * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md
58
+ * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md
56
59
  */
57
60
  export default {
58
61
  name: 'CnRadioCardsWidget',
@@ -72,6 +75,7 @@ export default {
72
75
  * - `options` (required) — `[{ value, label, description? }]`.
73
76
  * - `label` — a legend over the cards (visually shown).
74
77
  * - `columns` — 1, 2 or 3 (default 2).
78
+ * - `variant` — `cards` (the default) or `list`: a compact list of radio buttons, one per line, with the description under the label and no card around each (board DcBeheer). A list is one column.
75
79
  * - `readOnly`.
76
80
  *
77
81
  * @type {object}
@@ -133,7 +137,20 @@ export default {
133
137
  return this.tr(this.content.label || '')
134
138
  },
135
139
 
140
+ /**
141
+ * Whether the options draw as a compact list (`variant: "list"`).
142
+ *
143
+ * @return {boolean}
144
+ * @spec openspec/changes/screens-radio-cards-list/specs/radio-cards/spec.md#requirement-radio-cards-can-draw-as-a-compact-list
145
+ */
146
+ isList() {
147
+ return this.content.variant === 'list'
148
+ },
149
+
136
150
  columns() {
151
+ if (this.isList) {
152
+ return 1
153
+ }
137
154
  return [1, 3].includes(this.content.columns) ? this.content.columns : 2
138
155
  },
139
156
 
@@ -185,7 +202,7 @@ export default {
185
202
  *
186
203
  * @param {object} option The chosen option.
187
204
  * @return {Promise<void>}
188
- * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
205
+ * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
189
206
  */
190
207
  async choose(option) {
191
208
  if ((!this.objectUrl && !this.hasSave) || !this.content.field) {
@@ -310,6 +327,40 @@ export default {
310
327
  color: var(--color-text-maxcontrast);
311
328
  }
312
329
 
330
+ /* `variant: "list"`: one radio button per line, the description under the
331
+ label, no card around each (board DcBeheer, "Soort organisatie"). */
332
+ .cn-radio-cards--list .cn-radio-cards__grid {
333
+ gap: 8px;
334
+ }
335
+
336
+ .cn-radio-cards--list .cn-radio-cards__card,
337
+ .cn-radio-cards--list .cn-radio-cards__card--selected {
338
+ padding: 0;
339
+ border: 0;
340
+ border-radius: 0;
341
+ background: transparent;
342
+ box-shadow: none;
343
+ font-size: 15px;
344
+ }
345
+
346
+ .cn-radio-cards--list .cn-radio-cards__dot {
347
+ width: 18px;
348
+ height: 18px;
349
+ margin-top: 2px;
350
+ }
351
+
352
+ .cn-radio-cards--list .cn-radio-cards__card--selected .cn-radio-cards__dot {
353
+ border-width: 5px;
354
+ }
355
+
356
+ .cn-radio-cards--list .cn-radio-cards__title {
357
+ font-weight: 400;
358
+ }
359
+
360
+ .cn-radio-cards--list .cn-radio-cards__description {
361
+ font-size: 13px;
362
+ }
363
+
313
364
  @media (max-width: 640px) {
314
365
  .cn-radio-cards__grid--2,
315
366
  .cn-radio-cards__grid--3 {
@@ -212,7 +212,9 @@ export default {
212
212
  /**
213
213
  * The list config: `{ register, schema, filter?, sort?, limit?, extend?,
214
214
  * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
215
- * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText? }`.
215
+ * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
216
+ * valueLabels? }`. `valueLabels` maps a field's stored values to the labels
217
+ * the templates show.
216
218
  *
217
219
  * @type {object}
218
220
  */
@@ -441,7 +443,40 @@ export default {
441
443
  * @return {string} The text, '' when a token is empty.
442
444
  */
443
445
  fill(template, row) {
444
- return typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), row, new Date(), this.templateContext) : ''
446
+ return typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), this.labelledRow(row), new Date(), this.templateContext) : ''
447
+ },
448
+
449
+ /**
450
+ * The row as the templates read it: a field named in `valueLabels`
451
+ * (`{ channel: { phone: "Phone", telefoon: "Phone" } }`) shows the
452
+ * translated label of its stored value, so "{channel}" reads "Telefoon"
453
+ * for both spellings the data holds. A value without a label stays as
454
+ * stored. The pill, the badges and the sort read the stored value.
455
+ *
456
+ * @param {object} row The row.
457
+ * @return {object} The row, or a shallow copy with labelled fields.
458
+ * @spec openspec/changes/screens-record-list-value-labels/specs/record-list-value-labels/spec.md#requirement-a-record-list-template-can-show-a-label-for-a-stored-value
459
+ */
460
+ labelledRow(row) {
461
+ const map = this.content?.valueLabels
462
+ if (!row || typeof row !== 'object' || !map || typeof map !== 'object') {
463
+ return row
464
+ }
465
+ let out = row
466
+ for (const [field, labels] of Object.entries(map)) {
467
+ const raw = row[field]
468
+ if (!labels || typeof labels !== 'object' || (typeof raw !== 'string' && typeof raw !== 'number')) {
469
+ continue
470
+ }
471
+ const label = labels[String(raw)]
472
+ if (typeof label === 'string' && label !== '') {
473
+ if (out === row) {
474
+ out = { ...row }
475
+ }
476
+ out[field] = this.translate(label)
477
+ }
478
+ }
479
+ return out
445
480
  },
446
481
 
447
482
  /**
@@ -32,9 +32,33 @@
32
32
  Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).
33
33
  -->
34
34
  <template>
35
- <div class="cn-reports-page">
35
+ <div class="cn-reports-page" :class="{ 'cn-reports-page--board': isBoard }">
36
36
  <div class="cn-reports-page__header">
37
- <h2 class="cn-reports-page__title">
37
+ <!-- The header buttons (`headerButtons`) beside the title, as the
38
+ board's "Downloaden" and the buildiq square (board DcRapportages).
39
+ Without them, and outside the board look, the title stands alone
40
+ as before. -->
41
+ <div v-if="headerButtonsRow.length > 0" class="cn-reports-page__heading-row">
42
+ <h2 class="cn-reports-page__title">
43
+ {{ resolvedTitle }}
44
+ </h2>
45
+ <div class="cn-reports-page__header-buttons" data-testid="cn-reports-header-buttons">
46
+ <template v-for="button in headerButtonsRow" :key="button.key">
47
+ <CnBuildiqEditButton v-if="button.action === '__buildiq'" />
48
+ <NcButton
49
+ v-else
50
+ :variant="button.variant"
51
+ :data-testid="`cn-reports-header-button-${button.key}`"
52
+ @click="onHeaderButton(button)">
53
+ <template v-if="button.icon" #icon>
54
+ <CnIcon :name="button.icon" :size="20" />
55
+ </template>
56
+ {{ button.label }}
57
+ </NcButton>
58
+ </template>
59
+ </div>
60
+ </div>
61
+ <h2 v-else class="cn-reports-page__title">
38
62
  {{ resolvedTitle }}
39
63
  </h2>
40
64
  <p v-if="resolvedDescription" class="cn-reports-page__description">
@@ -42,7 +66,44 @@
42
66
  </p>
43
67
  </div>
44
68
 
45
- <div v-if="categoryOptions.length > 1" class="cn-reports-page__filter">
69
+ <!-- The board look (board DcRapportages): one section per category,
70
+ headed by its label, instead of the category filter; each card
71
+ draws its icon in a tile. -->
72
+ <template v-if="isBoard">
73
+ <p v-if="resolvedCards.length === 0"
74
+ class="cn-reports-page__empty"
75
+ data-testid="cn-reports-empty">
76
+ {{ resolvedEmptyLabel }}
77
+ </p>
78
+ <section v-for="section in boardSections"
79
+ :key="section.key"
80
+ class="cn-reports-page__section"
81
+ data-testid="cn-reports-section">
82
+ <h3 v-if="section.label" class="cn-reports-page__section-title">
83
+ {{ section.label }}
84
+ </h3>
85
+ <ul class="cn-reports-page__grid" data-testid="cn-reports-grid">
86
+ <li v-for="card in section.cards" :key="card.id" class="cn-reports-page__cell">
87
+ <a class="cn-reports-page__card"
88
+ data-testid="cn-report-card"
89
+ :href="hrefFor(card)"
90
+ @click="open(card, $event)">
91
+ <span v-if="card.icon" class="cn-reports-page__card-icon" aria-hidden="true">
92
+ <CnIcon :name="card.icon" :size="22" />
93
+ </span>
94
+ <span class="cn-reports-page__card-text">
95
+ <span class="cn-reports-page__card-title">{{ card.label }}</span>
96
+ <span v-if="card.description" class="cn-reports-page__card-description">
97
+ {{ card.description }}
98
+ </span>
99
+ </span>
100
+ </a>
101
+ </li>
102
+ </ul>
103
+ </section>
104
+ </template>
105
+
106
+ <div v-if="!isBoard && categoryOptions.length > 1" class="cn-reports-page__filter">
46
107
  <label class="cn-reports-page__filter-label" :for="filterId">
47
108
  {{ resolvedCategoryLabel }}
48
109
  </label>
@@ -58,13 +119,13 @@
58
119
  </select>
59
120
  </div>
60
121
 
61
- <p v-if="visibleCards.length === 0"
122
+ <p v-if="!isBoard && visibleCards.length === 0"
62
123
  class="cn-reports-page__empty"
63
124
  data-testid="cn-reports-empty">
64
125
  {{ resolvedEmptyLabel }}
65
126
  </p>
66
127
 
67
- <ul v-else class="cn-reports-page__grid" data-testid="cn-reports-grid">
128
+ <ul v-else-if="!isBoard" class="cn-reports-page__grid" data-testid="cn-reports-grid">
68
129
  <li v-for="card in visibleCards" :key="card.id" class="cn-reports-page__cell">
69
130
  <a class="cn-reports-page__card"
70
131
  data-testid="cn-report-card"
@@ -97,7 +158,12 @@
97
158
  * The Reports page: one surface listing an app's reports as cards.
98
159
  */
99
160
  import { translate as t } from '@nextcloud/l10n'
161
+ import { NcButton } from '@nextcloud/vue'
100
162
  import { defineAsyncComponent } from 'vue'
163
+ import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
164
+ import CnIcon from '../CnIcon/CnIcon.vue'
165
+ import { useLook } from '../../composables/useLook.js'
166
+ import { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
101
167
  import { followLinkClick } from '../../utils/linkNavigation.js'
102
168
 
103
169
  export default {
@@ -107,9 +173,18 @@ export default {
107
173
  // Loaded only for a page that declares widgets: a cards-only reports
108
174
  // page does not pull the dashboard in.
109
175
  CnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),
176
+ CnBuildiqEditButton,
177
+ CnIcon,
178
+ NcButton,
110
179
  },
111
180
 
112
181
  inject: {
182
+ /**
183
+ * The v2 registry and the legacy customComponents map, provided by
184
+ * CnAppRoot: a header action's named handler resolves against them.
185
+ */
186
+ cnRegistry: { default: () => ({}) },
187
+ cnCustomComponents: { default: () => ({}) },
113
188
  /**
114
189
  * The consuming app's translate function, provided by CnAppRoot.
115
190
  * Defaults to identity so the page still renders outside one.
@@ -228,6 +303,49 @@ export default {
228
303
  default: null,
229
304
  },
230
305
 
306
+ /**
307
+ * Buttons beside the title (`config.headerButtons`), as the board's
308
+ * "Downloaden". Each is `{ label, action, variant?, icon?, id? }`:
309
+ * `action` is the id of a `headerActions` entry, which says what the
310
+ * click does. Under the board look the buildiq square sits between the
311
+ * secondary buttons and the primary one. Empty (the default) changes
312
+ * nothing.
313
+ *
314
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
315
+ * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}
316
+ */
317
+ headerButtons: {
318
+ type: Array,
319
+ default: null,
320
+ },
321
+
322
+ /**
323
+ * The page's header actions (`config.headerActions`), in the shape of
324
+ * an index page's: a header button names one by id. `handler` is a
325
+ * function, `navigate` (with `route`, and `params`), `emit`, `none`,
326
+ * or a registry name; `type: "navigate"` with a `target` URL opens
327
+ * that URL in a new tab. Every click also emits `header-action`
328
+ * `{ action, id }`, except for `handler: "none"`.
329
+ *
330
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
331
+ * @type {Array<object>|null}
332
+ */
333
+ headerActions: {
334
+ type: Array,
335
+ default: null,
336
+ },
337
+
338
+ /**
339
+ * The look to draw in: `board` or `nextcloud`. Absent: the look
340
+ * CnAppRoot provides (`cnLook`), else `nextcloud`.
341
+ *
342
+ * @type {string|null}
343
+ */
344
+ look: {
345
+ type: String,
346
+ default: null,
347
+ },
348
+
231
349
  /**
232
350
  * Translate function. Falls back to injected `cnTranslate`.
233
351
  *
@@ -239,6 +357,13 @@ export default {
239
357
  },
240
358
  },
241
359
 
360
+ emits: ['header-action'],
361
+
362
+ setup(props) {
363
+ const { isBoard } = useLook(props)
364
+ return { isBoard }
365
+ },
366
+
242
367
  data() {
243
368
  return {
244
369
  activeCategory: 'all',
@@ -283,6 +408,8 @@ export default {
283
408
  'emptyLabel',
284
409
  'widgets',
285
410
  'layout',
411
+ 'headerButtons',
412
+ 'headerActions',
286
413
  ]) {
287
414
  if (this[key] !== null && this[key] !== undefined) {
288
415
  fromProps[key] = this[key]
@@ -322,6 +449,51 @@ export default {
322
449
  })
323
450
  },
324
451
 
452
+ /**
453
+ * The declared header buttons that have a label and an action,
454
+ * translated, each with a stable key and a variant.
455
+ *
456
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
457
+ * @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.
458
+ */
459
+ resolvedHeaderButtons() {
460
+ const declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []
461
+ return declared
462
+ .filter((button) => button && typeof button === 'object'
463
+ && typeof button.action === 'string' && button.action !== ''
464
+ && typeof button.label === 'string' && button.label !== '')
465
+ .map((button) => ({
466
+ key: String(button.id || button.action),
467
+ label: this.tr(button.label),
468
+ action: button.action,
469
+ variant: button.variant === 'primary' ? 'primary' : 'secondary',
470
+ icon: typeof button.icon === 'string' ? button.icon : '',
471
+ }))
472
+ },
473
+
474
+ /**
475
+ * The header row in the order it renders. Without the board look:
476
+ * the declared buttons in the declared order. Under the board look:
477
+ * the secondary buttons, the buildiq square, then the primary ones,
478
+ * as the board draws "Downloaden" and the square. No declared button
479
+ * leaves the header as it was, without the square.
480
+ *
481
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
482
+ * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.
483
+ */
484
+ headerButtonsRow() {
485
+ const buttons = this.resolvedHeaderButtons
486
+ if (buttons.length === 0 || !this.isBoard) {
487
+ return buttons
488
+ }
489
+ const square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }
490
+ return [
491
+ ...buttons.filter((button) => button.variant !== 'primary'),
492
+ square,
493
+ ...buttons.filter((button) => button.variant === 'primary'),
494
+ ]
495
+ },
496
+
325
497
  /**
326
498
  * The heading.
327
499
  *
@@ -385,6 +557,30 @@ export default {
385
557
  return options
386
558
  },
387
559
 
560
+ /**
561
+ * The board look's sections: the cards without a category first, under
562
+ * no heading, then one section per declared category that holds a
563
+ * card, in the declared order, headed by its label.
564
+ *
565
+ * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look
566
+ * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.
567
+ */
568
+ boardSections() {
569
+ const declared = this.config.categories || {}
570
+ const sections = []
571
+ const loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))
572
+ if (loose.length > 0) {
573
+ sections.push({ key: '__none', label: '', cards: loose })
574
+ }
575
+ for (const key of Object.keys(declared)) {
576
+ const cards = this.resolvedCards.filter((card) => card.category === key)
577
+ if (cards.length > 0) {
578
+ sections.push({ key, label: this.tr(declared[key]), cards })
579
+ }
580
+ }
581
+ return sections
582
+ },
583
+
388
584
  /**
389
585
  * The cards the active filter admits.
390
586
  *
@@ -487,6 +683,47 @@ export default {
487
683
  }
488
684
  },
489
685
 
686
+ /**
687
+ * A header button was clicked: run the `headerActions` entry it names
688
+ * and emit `header-action`, as an index page's header button does.
689
+ *
690
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
691
+ * @param {{action: string}} button The resolved button.
692
+ * @return {void}
693
+ */
694
+ onHeaderButton(button) {
695
+ const id = button.action
696
+ const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []
697
+ const entry = actions.find((action) => action && action.id === id) || null
698
+ const handler = entry ? entry.handler : undefined
699
+ if (handler === 'none') {
700
+ return
701
+ }
702
+ if (typeof handler === 'function') {
703
+ handler({ actionId: id })
704
+ } else if (handler === 'navigate' && entry.route) {
705
+ const params = (entry.params && typeof entry.params === 'object') ? entry.params : null
706
+ if (this.$router && typeof this.$router.push === 'function') {
707
+ this.$router.push(params ? { name: entry.route, params } : { name: entry.route })
708
+ }
709
+ } else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {
710
+ window.open(entry.target, '_blank', 'noopener,noreferrer')
711
+ } else if (typeof handler === 'string' && handler !== 'emit') {
712
+ const resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)
713
+ if (typeof resolved === 'function') {
714
+ resolved({ actionId: id })
715
+ }
716
+ }
717
+ /**
718
+ * A header button was clicked (`headerButtons`), after its
719
+ * `headerActions` entry ran; not for `handler: "none"`.
720
+ *
721
+ * @event header-action
722
+ * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.
723
+ */
724
+ this.$emit('header-action', { action: id, id })
725
+ },
726
+
490
727
  /**
491
728
  * Navigate by route NAME on a plain click; a modified or middle click
492
729
  * is left to the browser so it can open the card's href elsewhere.
@@ -518,6 +755,27 @@ export default {
518
755
  margin: 0 0 4px;
519
756
  }
520
757
 
758
+ .cn-reports-page__heading-row {
759
+ display: flex;
760
+ flex-wrap: wrap;
761
+ align-items: flex-start;
762
+ justify-content: space-between;
763
+ gap: 12px 24px;
764
+ }
765
+
766
+ .cn-reports-page__heading-row .cn-reports-page__title {
767
+ flex: 1 1 320px;
768
+ min-width: 0;
769
+ }
770
+
771
+ .cn-reports-page__header-buttons {
772
+ display: flex;
773
+ flex: none;
774
+ flex-wrap: wrap;
775
+ align-items: center;
776
+ gap: 10px;
777
+ }
778
+
521
779
  .cn-reports-page__description {
522
780
  margin: 0 0 16px;
523
781
  color: var(--color-text-maxcontrast);