@conduction/nextcloud-vue 2.104.0 → 2.105.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 (110) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +93 -1
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +6 -3
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -3
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +59 -1
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  17. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  18. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +1 -1
  19. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  20. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  22. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +2 -0
  23. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +32 -29
  25. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  27. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +73 -4
  28. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +74 -27
  30. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +54 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +3 -2
  35. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  36. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue.js +9 -0
  37. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue.js.map +1 -0
  38. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue2.js +88 -0
  39. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue2.js.map +1 -0
  40. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue3.js +39 -0
  41. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue3.js.map +1 -0
  42. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  43. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  44. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +3 -3
  45. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  46. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  47. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  48. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +16 -4
  49. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  50. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  51. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +11 -0
  52. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  53. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -4
  54. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  56. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  57. package/dist/esm/index.js +1 -0
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/l10n/en.json.js +4 -1
  60. package/dist/esm/l10n/en.json.js.map +1 -1
  61. package/dist/esm/l10n/nl.json.js +4 -1
  62. package/dist/esm/l10n/nl.json.js.map +1 -1
  63. package/dist/esm/library-style.js +1 -1
  64. package/dist/esm/nextcloud-vue.css +566 -295
  65. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  66. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  67. package/dist/esm/utils/nextcloudApps.js +45 -0
  68. package/dist/esm/utils/nextcloudApps.js.map +1 -0
  69. package/dist/esm/utils/scopeListLayout.js +9 -2
  70. package/dist/esm/utils/scopeListLayout.js.map +1 -1
  71. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  72. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  73. package/dist/esm/utils/visibleWhen.js +12 -4
  74. package/dist/esm/utils/visibleWhen.js.map +1 -1
  75. package/dist/nextcloud-vue.cjs.js +3054 -2517
  76. package/dist/nextcloud-vue.cjs.js.map +2 -2
  77. package/dist/nextcloud-vue.css +566 -295
  78. package/l10n/en.json +4 -1
  79. package/l10n/nl.json +4 -1
  80. package/package.json +1 -1
  81. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -1
  82. package/src/components/CnChartWidget/CnChartWidget.md +2 -0
  83. package/src/components/CnChartWidget/CnChartWidget.vue +93 -1
  84. package/src/components/CnDashboardPage/CnDashboardPage.vue +9 -6
  85. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  86. package/src/components/CnDetailPage/CnDetailPage.vue +59 -1
  87. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -1
  88. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +4 -1
  89. package/src/components/CnFlowsPage/CnFlowBoard.vue +132 -8
  90. package/src/components/CnIndexPage/CnIndexPage.md +1 -0
  91. package/src/components/CnIndexPage/CnIndexPage.vue +54 -1
  92. package/src/components/CnIndexPage/useSelfFetchList.js +3 -2
  93. package/src/components/CnNoticeWidget/CnNoticeWidget.vue +153 -0
  94. package/src/components/CnNoticeWidget/index.js +12 -0
  95. package/src/components/CnSettingsField/CnSettingsField.vue +4 -1
  96. package/src/components/CnSetupWizard/CnSetupWizard.vue +9 -1
  97. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +15 -4
  98. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  99. package/src/components/index.js +1 -0
  100. package/src/css/dialog.css +20 -2
  101. package/src/css/look-board-detail.css +77 -4
  102. package/src/css/look-board-index.css +36 -2
  103. package/src/css/look-board.css +97 -12
  104. package/src/index.js +1 -0
  105. package/src/schemas/app-manifest-v2.schema.json +36 -3
  106. package/src/utils/libraryWidgetKeys.js +1 -0
  107. package/src/utils/nextcloudApps.js +50 -0
  108. package/src/utils/scopeListLayout.js +9 -2
  109. package/src/utils/validateManifestV2.compiled.js +3 -3
  110. package/src/utils/visibleWhen.js +12 -4
package/l10n/en.json CHANGED
@@ -1724,7 +1724,10 @@
1724
1724
  "No contacts linked": "No contacts linked",
1725
1725
  "No relations yet": "No relations yet",
1726
1726
  "Period": "Period",
1727
- "On": "On"
1727
+ "On": "On",
1728
+ "Select a step on the canvas to see its settings.": "Select a step on the canvas to see its settings.",
1729
+ "Show runs": "Show runs",
1730
+ "Back to the step settings": "Back to the step settings"
1728
1731
  },
1729
1732
  "plurals": {
1730
1733
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -1937,7 +1937,10 @@
1937
1937
  "No contacts linked": "Geen contacten gekoppeld",
1938
1938
  "No relations yet": "Nog geen relaties",
1939
1939
  "Period": "Periode",
1940
- "On": "Aan"
1940
+ "On": "Aan",
1941
+ "Select a step on the canvas to see its settings.": "Kies een stap op het canvas om de instellingen te zien.",
1942
+ "Show runs": "Uitvoeringen tonen",
1943
+ "Back to the step settings": "Terug naar de stapinstellingen"
1941
1944
  },
1942
1945
  "plurals": {
1943
1946
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.104.0",
3
+ "version": "2.105.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": {
@@ -160,7 +160,7 @@
160
160
  <template #additional-items>
161
161
  <!-- The board card: support as one fact row, the SLA address on the same line. -->
162
162
  <div v-if="compactCard && showSupport && !supportAtEnd && !$slots.footer" class="cn-version-info__item" data-testid="cn-admin-settings-support-row">
163
- <span class="cn-version-info__label">{{ supportTitle }}:</span>
163
+ <span class="cn-version-info__label">{{ supportTitle }}</span>
164
164
  <span class="cn-version-info__value">
165
165
  <a :href="`mailto:${supportEmail}`">{{ supportEmail }}</a><template v-if="slaEmail">, {{ slaInline }} <a :href="`mailto:${slaEmail}`">{{ slaEmail }}</a></template>
166
166
  </span>
@@ -108,6 +108,8 @@ Fallback slot — shown when ApexCharts is not available:
108
108
  | `legendPosition` | String | `''` | Legend placement override (`top`/`bottom`/`left`/`right`); empty keeps the automatic placement |
109
109
  | `barMax` | Number | `null` | `bar-list`: the value a full track stands for (100 for percentages); null takes the largest value |
110
110
  | `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 |
111
+ | `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). |
112
+ | `categoryFormat` | String | `''` | How date categories read on the x-axis: `month` ("jan") or `month-year` ("jan 2026"), in the user's language. Categories that are not dates are left as is. |
111
113
  | `emptyCellLabel` | String | `''` | `matrix`: the text of a cell without a value ("geen"), through the host's translate. Empty leaves the cell blank |
112
114
  | `highlight` | Object | `null` | `matrix`: the cell to outline, `{ row, column, columnKeys? }`, tokens such as `@object.behaviour` resolved |
113
115
  | `cellSource` | Object | `null` | `matrix`: read the cells from records that each hold one cell (`{ register, schema, filter?, rowField, columnField, valueField, … }`), or with `cellsPath` from the cells inside one record, see [Bar list and matrix](#bar-list-and-matrix) |
@@ -220,6 +220,22 @@ function parseBucketKey(key) {
220
220
  )
221
221
  }
222
222
 
223
+ /**
224
+ * A theme colour by CSS variable name, as a literal: ApexCharts writes colours
225
+ * into SVG attributes, where `var()` does not resolve.
226
+ *
227
+ * @param {string} name The variable, e.g. `--color-error`.
228
+ * @param {string} fallback The colour when the variable is unset (or no DOM).
229
+ * @return {string} The colour.
230
+ */
231
+ function themeColor(name, fallback) {
232
+ if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') {
233
+ return fallback
234
+ }
235
+ const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
236
+ return value || fallback
237
+ }
238
+
223
239
  /** Event-bus channel CnWidgetWrapper's Refresh action broadcasts on. */
224
240
  const REFRESH_BUS_CHANNEL = 'cn:widget:refresh'
225
241
 
@@ -385,6 +401,31 @@ export default {
385
401
  default: '',
386
402
  },
387
403
 
404
+ /**
405
+ * Horizontal reference lines on the value axis of a line, area or bar
406
+ * chart, such as a norm: `[{ value, label?, color? }]`. Drawn dashed;
407
+ * the label goes through the host's translate. The colour defaults to
408
+ * the theme's error colour.
409
+ *
410
+ * @type {Array<{value: number, label?: string, color?: string}>}
411
+ */
412
+ referenceLines: {
413
+ type: Array,
414
+ default: () => [],
415
+ },
416
+
417
+ /**
418
+ * How date categories read on the x-axis: `month` ("jan") or
419
+ * `month-year` ("jan 2026"), in the user's language. A category that
420
+ * is not a date is shown as is. Empty shows the categories unchanged.
421
+ *
422
+ * @type {''|'month'|'month-year'}
423
+ */
424
+ categoryFormat: {
425
+ type: String,
426
+ default: '',
427
+ },
428
+
388
429
  /**
389
430
  * The `matrix` cell to outline, usually the record's on a detail page:
390
431
  * `{ row, column, columnKeys? }`, where `row` and `column` take tokens
@@ -1505,6 +1546,51 @@ export default {
1505
1546
  return this.footnote ? this.hostTranslate(this.footnote) : t('nextcloud-vue', 'Chart data')
1506
1547
  },
1507
1548
 
1549
+ /**
1550
+ * The x-axis categories as shown: with `categoryFormat`, date
1551
+ * categories as short months in the user's language.
1552
+ *
1553
+ * @return {Array<string|number>} The categories.
1554
+ */
1555
+ displayCategories() {
1556
+ const categories = Array.isArray(this.resolvedCategories) ? this.resolvedCategories : []
1557
+ if (this.categoryFormat !== 'month' && this.categoryFormat !== 'month-year') {
1558
+ return categories
1559
+ }
1560
+ const options = this.categoryFormat === 'month' ? { month: 'short' } : { month: 'short', year: 'numeric' }
1561
+ return categories.map((category) => {
1562
+ const date = typeof category === 'string' || typeof category === 'number' ? parseBucketKey(category) : null
1563
+ return date ? date.toLocaleDateString(userLocale(), options) : category
1564
+ })
1565
+ },
1566
+
1567
+ /**
1568
+ * The `referenceLines` as ApexCharts annotations: dashed, labelled,
1569
+ * in the theme's error colour unless a line names its own.
1570
+ *
1571
+ * @return {Array<object>} The annotations, possibly empty.
1572
+ */
1573
+ referenceAnnotations() {
1574
+ const lines = (Array.isArray(this.referenceLines) ? this.referenceLines : [])
1575
+ .filter((line) => line && Number.isFinite(Number(line.value)))
1576
+ if (lines.length === 0) {
1577
+ return []
1578
+ }
1579
+ const fallback = themeColor('--color-error', '#e04224')
1580
+ return lines.map((line) => {
1581
+ const color = line.color || fallback
1582
+ return {
1583
+ y: Number(line.value),
1584
+ x: Number(line.value),
1585
+ borderColor: color,
1586
+ strokeDashArray: 6,
1587
+ label: line.label
1588
+ ? { text: this.hostTranslate(line.label), borderColor: color, style: { color: '#fff', background: color } }
1589
+ : { text: '' },
1590
+ }
1591
+ })
1592
+ },
1593
+
1508
1594
  mergedOptions() {
1509
1595
  const isPieType = ['pie', 'donut', 'radialBar'].includes(this.type)
1510
1596
 
@@ -1592,7 +1678,7 @@ export default {
1592
1678
  if (!isPieType) {
1593
1679
  if (this.resolvedCategories.length > 0) {
1594
1680
  defaults.xaxis = {
1595
- categories: this.resolvedCategories,
1681
+ categories: this.displayCategories,
1596
1682
  labels: {
1597
1683
  style: {
1598
1684
  colors: 'var(--color-text-maxcontrast, #767676)',
@@ -1634,6 +1720,12 @@ export default {
1634
1720
  }
1635
1721
  }
1636
1722
 
1723
+ // Reference lines, such as a norm, on the value axis.
1724
+ if (!isPieType && this.referenceAnnotations.length > 0) {
1725
+ const axisKey = (this.type === 'bar' && this.horizontal) ? 'xaxis' : 'yaxis'
1726
+ defaults.annotations = { [axisKey]: this.referenceAnnotations }
1727
+ }
1728
+
1637
1729
  // Add labels for pie/donut
1638
1730
  if (isPieType && this.resolvedLabels.length > 0) {
1639
1731
  defaults.labels = this.resolvedLabels
@@ -347,12 +347,12 @@
347
347
  <label
348
348
  v-else
349
349
  class="cn-dashboard-page__page-filter">
350
- <span v-if="pf.label" class="cn-dashboard-page__page-filter-label">{{ pf.label }}</span>
350
+ <span v-if="pf.label" class="cn-dashboard-page__page-filter-label">{{ effectiveTranslate(pf.label) }}</span>
351
351
  <NcSelect
352
352
  :modelValue="selectedPageFilterOption(pf)"
353
- :options="pf.options || []"
353
+ :options="pageFilterSegments(pf)"
354
354
  :clearable="false"
355
- :inputLabel="pf.label || pf.key"
355
+ :inputLabel="pf.label ? effectiveTranslate(pf.label) : pf.key"
356
356
  label="label"
357
357
  :data-testid="'cn-page-filter-' + pf.key"
358
358
  @update:modelValue="onPageFilterChange(pf, $event)" />
@@ -1106,6 +1106,9 @@ const CHART_PROP_KEYS = [
1106
1106
  'highlight',
1107
1107
  'cellSource',
1108
1108
  'emptyCellLabel',
1109
+ // A norm line and month categories on a line chart (DqDoorlooptijd).
1110
+ 'referenceLines',
1111
+ 'categoryFormat',
1109
1112
  ]
1110
1113
 
1111
1114
  /**
@@ -2886,12 +2889,12 @@ export default {
2886
2889
  return null
2887
2890
  }
2888
2891
  const current = this.workspaceContext[pf.key]
2889
- return (pf.options || []).find((o) => o.value === current) || null
2892
+ return this.pageFilterSegments(pf).find((o) => o.value === current) || null
2890
2893
  },
2891
2894
 
2892
2895
  /**
2893
- * A page filter's options as segments, labels through the host
2894
- * translate.
2896
+ * A page filter's options, labels through the host translate: the
2897
+ * segments of a segmented filter and the options of a select.
2895
2898
  *
2896
2899
  * @spec openspec/changes/dashboard-board-termijnen/specs/dashboard-page/spec.md#requirement-a-page-filter-as-a-segmented-group-on-the-description-line
2897
2900
  * @param {object} pf The page-filter descriptor.
@@ -427,6 +427,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
427
427
  | `headerFields` | Array | `[]` | Fields shown as chips under the title (manifest `config.headerFields`). Each entry is a property key, or `{ key, format, labelField, colorField, warnWhenPast }`; `format` is `text` (default), `mono`, `badge`, `user` or `date`. A `$ref` value shows `labelField` of the referenced object and falls back to the raw id in mono; a badge maps `colorField` (`success`, `warning`, `error`, `info`, `neutral`) to a variant; `warnWhenPast` gives a past date the error variant. An empty value shows no chip, a page without the key shows no row. Each chip reads "Title: value" to a screen reader, and the row prints as plain text. |
428
428
  | `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. |
429
429
  | `sideColumnFrom` | String | `''` | A field path, read like a `visibleWhen` field (e.g. `@context.caseType.sideCards`), whose list of ids picks the `sideColumn` entries this record shows and their order. Any other value keeps `sideColumn` as declared. |
430
+ | `tabsFrom` | String | `''` | A field path, read like a `visibleWhen` field, whose list of tab ids picks the tabs of the page's `tabs` widgets and their order for this record: `@context.caseType.tabs` reads the list a case type keeps (DqAanbesteding: Inschrijvingen, Publicaties, Bestanden, Historie). A tab's id is its `id`, else its `widgetId`, or the string entry. An empty or missing list keeps the declared tabs; edit mode shows them all. |
430
431
  | `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
431
432
 
432
433
  ## The record as a place
@@ -2567,6 +2567,22 @@ export default {
2567
2567
  default: null,
2568
2568
  },
2569
2569
 
2570
+ /**
2571
+ * A field path whose value picks the tabs of the page's `tabs` widgets
2572
+ * and their order for this record (manifest `config.tabsFrom`), read
2573
+ * like a `visibleWhen` field: `@context.caseType.tabs` reads the list a
2574
+ * case type keeps. A non-empty list of tab ids (a tab's `id`, else its
2575
+ * `widgetId`, or the string entry itself) keeps those tabs in that
2576
+ * order and leaves the rest out. Any other value keeps the declared
2577
+ * tabs. Edit mode shows them all.
2578
+ *
2579
+ * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order
2580
+ */
2581
+ tabsFrom: {
2582
+ type: String,
2583
+ default: '',
2584
+ },
2585
+
2570
2586
  /**
2571
2587
  * A column of cards beside the body (manifest `config.sideColumn`),
2572
2588
  * for the record's facts: deadline, requester, handler, links. Each
@@ -4551,7 +4567,7 @@ export default {
4551
4567
  */
4552
4568
  bodyGridWidgets() {
4553
4569
  if (this.hasGridLayout) {
4554
- return this.widgets
4570
+ return this.pickTabs(this.widgets)
4555
4571
  }
4556
4572
  if (this.shouldRenderAutoBody) {
4557
4573
  return this.autoBodyWidgets || []
@@ -4559,6 +4575,21 @@ export default {
4559
4575
  return []
4560
4576
  },
4561
4577
 
4578
+ /**
4579
+ * The tab ids `tabsFrom` reads off this record, or null when it reads
4580
+ * no non-empty list (or the body is being edited).
4581
+ *
4582
+ * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order
4583
+ * @return {Array<string>|null}
4584
+ */
4585
+ pickedTabIds() {
4586
+ if (!this.tabsFrom || this.editingBody || !this.widgetVisibilityData) {
4587
+ return null
4588
+ }
4589
+ const ids = readVisibleWhenPath(this.widgetVisibilityData, this.tabsFrom)
4590
+ return Array.isArray(ids) && ids.length > 0 ? ids.map((id) => String(id)) : null
4591
+ },
4592
+
4562
4593
  /**
4563
4594
  * Whether the body renders as an adjustable widget grid (vs. the
4564
4595
  * hand-authored default slot). True for explicit grid pages and for the
@@ -4977,6 +5008,33 @@ export default {
4977
5008
  },
4978
5009
 
4979
5010
  methods: {
5011
+ /**
5012
+ * The widget definitions with each `tabs` widget's tabs narrowed and
5013
+ * ordered by `pickedTabIds`; the definitions as given without a pick.
5014
+ * A new definition is returned for a changed widget; the declared one
5015
+ * is never mutated.
5016
+ *
5017
+ * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order
5018
+ * @param {Array<object>} widgets The widget definitions.
5019
+ * @return {Array<object>}
5020
+ */
5021
+ pickTabs(widgets) {
5022
+ const ids = this.pickedTabIds
5023
+ if (!ids || !Array.isArray(widgets)) {
5024
+ return widgets
5025
+ }
5026
+ const idOf = (tab) => String(typeof tab === 'string' ? tab : (tab && (tab.id ?? tab.widgetId)) ?? '')
5027
+ return widgets.map((widget) => {
5028
+ if (!widget || widget.type !== 'tabs' || !Array.isArray(widget.content?.tabs)) {
5029
+ return widget
5030
+ }
5031
+ const tabs = ids
5032
+ .map((id) => widget.content.tabs.find((tab) => idOf(tab) === id))
5033
+ .filter(Boolean)
5034
+ return tabs.length > 0 ? { ...widget, content: { ...widget.content, tabs } } : widget
5035
+ })
5036
+ },
5037
+
4980
5038
  /**
4981
5039
  * The tag after an admin-only entry in the board look ("beheerder",
4982
5040
  * DqZaakMeer), or null for any other entry or look. It is drawn by
@@ -460,7 +460,7 @@ export default {
460
460
  * @return {object}
461
461
  */
462
462
  boardOptions() {
463
- const keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton']
463
+ const keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton', 'actionsMenu']
464
464
  const options = {}
465
465
  for (const key of keys) {
466
466
  if (this.board && this.board[key] !== undefined && this.board[key] !== null) {
@@ -36,7 +36,7 @@
36
36
  {{ t('nextcloud-vue', 'Suggest feature') }}
37
37
  </NcButton>
38
38
  <NcActions
39
- v-if="!tabs"
39
+ v-if="!tabs && actionsMenu"
40
40
  :menuName="t('nextcloud-vue', 'Actions')"
41
41
  :forceName="true"
42
42
  data-testid="cn-features-board-actions">
@@ -85,6 +85,7 @@
85
85
  </button>
86
86
  </div>
87
87
  <NcActions
88
+ v-if="actionsMenu"
88
89
  :menuName="t('nextcloud-vue', 'Actions')"
89
90
  :forceName="true"
90
91
  class="cn-features-board__tab-actions"
@@ -375,6 +376,8 @@ export default {
375
376
 
376
377
  /** Show the Features and Roadmap folder tabs. */
377
378
  tabs: { type: Boolean, default: true },
379
+ /** Show the Actions menu (suggest, buildiq, AI skills, support), in the header or the tab row; false draws none. */
380
+ actionsMenu: { type: Boolean, default: true },
378
381
  /** `chips`: pill buttons with a count. `segmented`: a segmented control without counts. */
379
382
  filter: {
380
383
  type: String,
@@ -36,7 +36,7 @@
36
36
  <NcButton v-if="showRun"
37
37
  :disabled="store.running || !store.flow.id"
38
38
  data-testid="flow-board-run"
39
- @click="$emit('run')">
39
+ @click="onRun">
40
40
  {{ t('nextcloud-vue', 'Run') }}
41
41
  </NcButton>
42
42
  <NcButton v-if="testSubjectReady"
@@ -109,7 +109,7 @@
109
109
  :key="group.app"
110
110
  class="cn-flow-board__palette-group">
111
111
  <h4 class="cn-flow-board__palette-app">
112
- {{ group.app }}
112
+ {{ group.name }}
113
113
  </h4>
114
114
  <ul class="cn-flow-board__palette-list">
115
115
  <li v-for="entry in group.entries" :key="entry.id">
@@ -135,7 +135,7 @@
135
135
  :toolbarActions="false"
136
136
  :addStep="!showPalette"
137
137
  @save="$emit('save')"
138
- @run="$emit('run')" />
138
+ @run="onRun" />
139
139
  <div v-if="store.inRunView"
140
140
  class="cn-flow-board__legend"
141
141
  data-testid="flow-board-legend">
@@ -183,6 +183,15 @@
183
183
  <h3 class="cn-flow-board__card-title" data-testid="flow-board-step-name">
184
184
  {{ stepName }}
185
185
  </h3>
186
+ <dl v-if="stepSettings.length > 0" class="cn-flow-board__settings">
187
+ <div v-for="setting in stepSettings"
188
+ :key="setting.key"
189
+ class="cn-flow-board__setting"
190
+ data-testid="flow-board-step-setting">
191
+ <dt>{{ setting.label }}</dt>
192
+ <dd>{{ setting.value }}</dd>
193
+ </div>
194
+ </dl>
186
195
  <div class="cn-flow-board__step-actions">
187
196
  <NcButton data-testid="flow-board-edit-step" @click="store.editingNodeId = store.selectedNodeId">
188
197
  {{ t('nextcloud-vue', 'Edit step') }}
@@ -212,7 +221,23 @@
212
221
  </NcButton>
213
222
  </div>
214
223
  </template>
215
- <CnFlowSidebar v-else embedded />
224
+ <template v-else-if="showRuns">
225
+ <NcButton variant="tertiary"
226
+ class="cn-flow-board__runs-back"
227
+ data-testid="flow-board-hide-runs"
228
+ @click="showRuns = false">
229
+ {{ t('nextcloud-vue', 'Back to the step settings') }}
230
+ </NcButton>
231
+ <CnFlowSidebar embedded />
232
+ </template>
233
+ <div v-else class="cn-flow-board__inspector-empty" data-testid="flow-board-inspector-empty">
234
+ <p>{{ t('nextcloud-vue', 'Select a step on the canvas to see its settings.') }}</p>
235
+ <NcButton variant="secondary"
236
+ data-testid="flow-board-show-runs"
237
+ @click="showRuns = true">
238
+ {{ t('nextcloud-vue', 'Show runs') }}
239
+ </NcButton>
240
+ </div>
216
241
  </aside>
217
242
  </div>
218
243
  </div>
@@ -232,6 +257,7 @@ import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
232
257
  import { FLOW_RUN_ACTIVE_STATUSES, useFlowStore } from '../../composables/useFlowStore.js'
233
258
  import { formatRelativeDate } from '../../utils/boardDate.js'
234
259
  import { flowOutcomeLevels, flowRunDurationMs, flowRunLogLines, flowRunName, formatFlowDuration } from '../../utils/flowRunLog.js'
260
+ import { appDisplayNames } from '../../utils/nextcloudApps.js'
235
261
  import { CnStatusBadge } from '../CnStatusBadge/index.js'
236
262
 
237
263
  /**
@@ -278,7 +304,7 @@ export default {
278
304
  },
279
305
 
280
306
  data() {
281
- return { runTab: 'step', testOpen: false, testRecord: null }
307
+ return { runTab: 'step', testOpen: false, testRecord: null, appNames: {}, showRuns: false }
282
308
  },
283
309
 
284
310
  computed: {
@@ -414,17 +440,25 @@ export default {
414
440
  return { tone: 'success', text: this.t('nextcloud-vue', 'The flow is valid.') }
415
441
  },
416
442
 
417
- /** @return {Array<object>} The palette grouped by owning app. */
443
+ /**
444
+ * The palette grouped by owning app, each group named by the app's
445
+ * display name (its id when Nextcloud names none), the app that owns
446
+ * this flow first.
447
+ *
448
+ * @return {Array<{app: string, name: string, entries: Array<object>}>} The groups.
449
+ */
418
450
  paletteGroups() {
419
451
  const groups = {}
420
452
  ;(this.store.nodeCatalog || []).forEach((entry) => {
421
453
  const app = String(entry?.app || String(entry?.id || '').split('.')[0] || '')
422
454
  if (!groups[app]) {
423
- groups[app] = { app, entries: [] }
455
+ groups[app] = { app, name: this.appNames[app] || app, entries: [] }
424
456
  }
425
457
  groups[app].entries.push(entry)
426
458
  })
427
- return Object.values(groups)
459
+ const list = Object.values(groups)
460
+ const host = list.findIndex((group) => group.app === this.app)
461
+ return host > 0 ? [list[host], ...list.slice(0, host), ...list.slice(host + 1)] : list
428
462
  },
429
463
 
430
464
  /** @return {object|null} The run on screen. */
@@ -477,6 +511,37 @@ export default {
477
511
  return entry ? (entry.displayName || entry.id) : String(node?.type || '')
478
512
  },
479
513
 
514
+ /**
515
+ * The selected step's settings as its owner declares them in the
516
+ * catalogue's `configForm`: label and value, a select's option label,
517
+ * Yes or No for a switch. Unset settings are left out.
518
+ *
519
+ * @return {Array<{key: string, label: string, value: string}>} The settings.
520
+ */
521
+ stepSettings() {
522
+ const node = this.store.selectedNode
523
+ const entry = node ? this.store.catalogEntry(node.type) : null
524
+ const config = (node && node.config) || {}
525
+ const form = (entry && Array.isArray(entry.configForm)) ? entry.configForm : []
526
+ return form
527
+ .filter((field) => field && field.key && config[field.key] !== undefined && config[field.key] !== null && config[field.key] !== '')
528
+ .map((field) => {
529
+ const raw = config[field.key]
530
+ const option = Array.isArray(field.options) ? field.options.find((o) => o && o.value === raw) : null
531
+ let value
532
+ if (option) {
533
+ value = String(option.label ?? option.value)
534
+ } else if (typeof raw === 'boolean') {
535
+ value = raw ? this.t('nextcloud-vue', 'Yes') : this.t('nextcloud-vue', 'No')
536
+ } else if (typeof raw === 'object') {
537
+ value = JSON.stringify(raw)
538
+ } else {
539
+ value = String(raw)
540
+ }
541
+ return { key: field.key, label: field.label || field.key, value }
542
+ })
543
+ },
544
+
480
545
  /** @return {string} The selected step's name. */
481
546
  stepName() {
482
547
  const node = this.store.selectedNode
@@ -507,7 +572,36 @@ export default {
507
572
  },
508
573
  },
509
574
 
575
+ watch: {
576
+ // A selected step takes the panel back from the run history.
577
+ 'store.selectedNodeId': function(id) {
578
+ if (id) {
579
+ this.showRuns = false
580
+ }
581
+ },
582
+ },
583
+
584
+ async created() {
585
+ // The palette names its groups by app display name; only the column
586
+ // palette draws them, so only it reads the names.
587
+ if (this.palette !== 'column') {
588
+ return
589
+ }
590
+ const { default: axios } = await import('@nextcloud/axios')
591
+ this.appNames = await appDisplayNames(axios)
592
+ },
593
+
510
594
  methods: {
595
+ /**
596
+ * Run the flow, with the run history in the panel to follow it.
597
+ *
598
+ * @return {void}
599
+ */
600
+ onRun() {
601
+ this.showRuns = true
602
+ this.$emit('run')
603
+ },
604
+
511
605
  /**
512
606
  * Run the saved flow once with the picked record as its subject, then
513
607
  * close the dialog; the run shows on the canvas as any run does.
@@ -822,6 +916,36 @@ export default {
822
916
  text-align: center;
823
917
  }
824
918
 
919
+ .cn-flow-board__settings {
920
+ display: flex;
921
+ flex-direction: column;
922
+ gap: 10px;
923
+ margin: 0;
924
+ }
925
+
926
+ .cn-flow-board__setting dt {
927
+ font-size: 13px;
928
+ color: var(--color-text-maxcontrast);
929
+ }
930
+
931
+ .cn-flow-board__setting dd {
932
+ margin: 2px 0 0;
933
+ font-weight: 600;
934
+ overflow-wrap: anywhere;
935
+ }
936
+
937
+ .cn-flow-board__inspector-empty {
938
+ display: flex;
939
+ flex-direction: column;
940
+ align-items: flex-start;
941
+ gap: 12px;
942
+ color: var(--color-text-maxcontrast);
943
+ }
944
+
945
+ .cn-flow-board__inspector-empty p {
946
+ margin: 0;
947
+ }
948
+
825
949
  .cn-flow-board__step-actions,
826
950
  .cn-flow-board__step-footer {
827
951
  display: flex;
@@ -390,6 +390,7 @@ export default {
390
390
  | `showViewToggle` | Boolean | `true` | Whether to show the Cards/Table view toggle |
391
391
  | `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set. |
392
392
  | `search` | Boolean | `true` | `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page (manifest: `config.search`). Wins over `inlineSearch`. |
393
+ | `appConfig` | Object | `{}` | The app config `@config.<key>` tokens resolve against (manifest: `config.appConfig`), as on the dashboard and detail pages, merged over a config an ancestor provides on `cnAppConfig`. A quick filter's `filter` and its `label` / `activeLabel` take the token: `"Residence @config.municipality"` reads "Residence Zuiddrecht" (board DqContacten); the label is translated first, and a key the config does not hold leaves the token as written. |
393
394
  | `showCountWithSearch` | Boolean | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls (only relevant with `inlineSearch`, which otherwise takes the counter's place). Fed from the manifest as `pages[].config.showCountWithSearch`. |
394
395
  | `searchInFiles` | Boolean | `false` | "Also search inside files" switch beside the search box (manifest `config.searchInFiles`). On, a search with a term sends `_content_search=true`; rows found through a file show "Found in {file}" (`@self.matchedFile`). State kept in the route as `contentSearch=1`. Emits `content-search`. |
395
396
  | `searchPlaceholder` | String | `''` | Placeholder for the inline search field (manifest `config.searchPlaceholder`). |