@conduction/nextcloud-vue 2.101.0 → 2.102.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 (74) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +22 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.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 +29 -4
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -0
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +70 -4
  16. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -4
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnIndexPage/useNamedSource.js +26 -2
  20. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  21. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
  22. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
  23. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
  26. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
  27. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  28. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
  29. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  30. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  31. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
  32. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  33. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  34. package/dist/esm/composables/indexSources.js +9 -0
  35. package/dist/esm/composables/indexSources.js.map +1 -1
  36. package/dist/esm/l10n/en.json.js +5 -1
  37. package/dist/esm/l10n/en.json.js.map +1 -1
  38. package/dist/esm/l10n/nl.json.js +5 -1
  39. package/dist/esm/l10n/nl.json.js.map +1 -1
  40. package/dist/esm/library-style.js +1 -1
  41. package/dist/esm/nextcloud-vue.css +232 -187
  42. package/dist/esm/utils/builtInFormatters.js +24 -4
  43. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  44. package/dist/esm/utils/rowClassRules.js +43 -0
  45. package/dist/esm/utils/rowClassRules.js.map +1 -0
  46. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  47. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  48. package/dist/esm/utils/visibleWhen.js +19 -2
  49. package/dist/esm/utils/visibleWhen.js.map +1 -1
  50. package/dist/nextcloud-vue.cjs.js +1464 -1053
  51. package/dist/nextcloud-vue.cjs.js.map +2 -2
  52. package/dist/nextcloud-vue.css +232 -187
  53. package/l10n/en.json +5 -1
  54. package/l10n/nl.json +5 -1
  55. package/package.json +1 -1
  56. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  57. package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
  58. package/src/components/CnChartWidget/CnChartWidget.md +3 -2
  59. package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
  60. package/src/components/CnDashboardPage/CnDashboardPage.vue +21 -1
  61. package/src/components/CnIndexPage/CnIndexPage.md +5 -1
  62. package/src/components/CnIndexPage/CnIndexPage.vue +74 -6
  63. package/src/components/CnIndexPage/useNamedSource.js +26 -2
  64. package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
  65. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
  66. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
  67. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
  68. package/src/composables/indexSources.js +9 -0
  69. package/src/css/look-board-index.css +10 -0
  70. package/src/schemas/app-manifest-v2.schema.json +55 -3
  71. package/src/utils/builtInFormatters.js +24 -4
  72. package/src/utils/rowClassRules.js +39 -0
  73. package/src/utils/validateManifestV2.compiled.js +3 -3
  74. package/src/utils/visibleWhen.js +19 -2
package/l10n/en.json CHANGED
@@ -1720,7 +1720,11 @@
1720
1720
  "Made {date}": "Made {date}",
1721
1721
  "expires {date}": "expires {date}",
1722
1722
  "Loading …": "Loading …",
1723
- "Reorder {item}: drag, or press the up and down arrow keys": "Reorder {item}: drag, or press the up and down arrow keys"
1723
+ "Reorder {item}: drag, or press the up and down arrow keys": "Reorder {item}: drag, or press the up and down arrow keys",
1724
+ "No contacts linked": "No contacts linked",
1725
+ "No relations yet": "No relations yet",
1726
+ "Period": "Period",
1727
+ "On": "On"
1724
1728
  },
1725
1729
  "plurals": {
1726
1730
  "{count} selected": [
package/l10n/nl.json CHANGED
@@ -1933,7 +1933,11 @@
1933
1933
  "Expires today": "Verloopt vandaag",
1934
1934
  "Made {date}": "Gemaakt {date}",
1935
1935
  "expires {date}": "verloopt {date}",
1936
- "Reorder {item}: drag, or press the up and down arrow keys": "{item} verplaatsen: slepen, of de pijltoetsen omhoog en omlaag"
1936
+ "Reorder {item}: drag, or press the up and down arrow keys": "{item} verplaatsen: slepen, of de pijltoetsen omhoog en omlaag",
1937
+ "No contacts linked": "Geen contacten gekoppeld",
1938
+ "No relations yet": "Nog geen relaties",
1939
+ "Period": "Periode",
1940
+ "On": "Aan"
1937
1941
  },
1938
1942
  "plurals": {
1939
1943
  "{count} selected": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.101.0",
3
+ "version": "2.102.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": {
@@ -217,6 +217,7 @@ Controlling the inline action button count — `inlineActionCount` sets how many
217
217
  | `showBuildiqButton` | Boolean | `true` | Whether the buildiq square renders in the bar (the index page takes it into its header under the board look) |
218
218
  | `bulkNoun` | String | `''` | The plural in the bulk band lead, "With the selected <noun>" (default "items") |
219
219
  | `bulkHint` | String | `''` | The 13px hint after the bulk band's buttons |
220
+ | `bulkBandAlways` | Boolean | `false` | Board look: draw the bulk band before anything is selected too, its buttons disabled until there is a selection (DqZaken, DqWerkbord). Only when the band has an action to offer. |
220
221
 
221
222
 
222
223
  `showBuildiqButton` is documented in the prop table of the component reference (board look, screens-chrome-parity).
@@ -507,12 +507,15 @@
507
507
 
508
508
  <!--
509
509
  Board look: the bulk band is its own row between the toolbar and the
510
- table card, outside the toolbar element, and only while rows are
511
- selected. The count stays announced by the live region above.
510
+ table card, outside the toolbar element, while rows are selected; with
511
+ `bulkBandAlways` also before, its buttons disabled until there is a
512
+ selection (DqZaken, DqWerkbord). The count stays announced by the live
513
+ region above.
512
514
  -->
513
515
  <div
514
- v-if="isBoardLayout && selectable && selectedIds.length > 0"
516
+ v-if="isBoardLayout && selectable && (selectedIds.length > 0 || (bulkBandAlways && bulkBandHasActions))"
515
517
  class="cn-actions-bar__band"
518
+ :class="{ 'cn-actions-bar__band--idle': selectedIds.length === 0 }"
516
519
  role="region"
517
520
  :aria-label="t('nextcloud-vue', 'Actions for the selection')"
518
521
  data-testid="cn-bulk-band">
@@ -520,12 +523,14 @@
520
523
  <NcButton
521
524
  v-if="showMassCopy"
522
525
  variant="secondary"
526
+ :disabled="selectedIds.length === 0"
523
527
  @click="$emit('show-copy')">
524
528
  {{ t('nextcloud-vue', 'Copy selected') }}
525
529
  </NcButton>
526
530
  <NcButton
527
531
  v-if="showMassDelete"
528
532
  variant="secondary"
533
+ :disabled="selectedIds.length === 0"
529
534
  @click="$emit('show-delete')">
530
535
  {{ t('nextcloud-vue', 'Delete selected') }}
531
536
  </NcButton>
@@ -533,7 +538,7 @@
533
538
  v-for="entry in bulkActions"
534
539
  :key="entry.id"
535
540
  variant="secondary"
536
- :disabled="entry.disabled === true"
541
+ :disabled="entry.disabled === true || selectedIds.length === 0"
537
542
  :data-testid="`cn-bulk-action-${entry.id}`"
538
543
  :lang="bulkLang(entry)"
539
544
  @click="$emit('bulk-action', { id: entry.id, action: entry.id, selectedIds, count: selectedIds.length })">
@@ -719,6 +724,16 @@ export default {
719
724
  default: '',
720
725
  },
721
726
 
727
+ /**
728
+ * Board look: draw the bulk band also before anything is selected, its
729
+ * buttons disabled until there is a selection, as DqZaken and DqWerkbord
730
+ * draw it. Only when the band has an action to offer.
731
+ */
732
+ bulkBandAlways: {
733
+ type: Boolean,
734
+ default: false,
735
+ },
736
+
722
737
  /** The 13px hint after the board bulk band's buttons (`config.bulkHint`). */
723
738
  bulkHint: {
724
739
  type: String,
@@ -1117,6 +1132,18 @@ export default {
1117
1132
  return t('nextcloud-vue', '{count} active', { count: this.filterCount })
1118
1133
  },
1119
1134
 
1135
+ /**
1136
+ * Whether the bulk band has an action to offer: copy, delete, a
1137
+ * declared bulk action or the host's own buttons.
1138
+ *
1139
+ * @return {boolean}
1140
+ */
1141
+ bulkBandHasActions() {
1142
+ return this.showMassCopy || this.showMassDelete
1143
+ || (Array.isArray(this.bulkActions) && this.bulkActions.length > 0)
1144
+ || Boolean(this.$slots['selection-actions'])
1145
+ },
1146
+
1120
1147
  /**
1121
1148
  * The bold lead of the board bulk band.
1122
1149
  *
@@ -18,7 +18,7 @@ When `CnDashboardPage` resolves a widget definition with `type: "chart"`, it mou
18
18
  }
19
19
  ```
20
20
 
21
- Supported `props` keys forwarded by the dispatcher: `chartKind` (→ `type`), `series`, `categories`, `labels`, `options`, `colors`, `toolbar`, `legend`, `height`, `width`, `unavailableLabel`, plus the display passthrough `horizontal`, `legendPosition`, `valueAxisBaseline`, `valueFormat`, `colorMap`, `emptyLabel`, `views`, `endpointSource`, `barMax`, `footnote`, `highlight`, `cellSource`. The `dataSource` field resolves declaratively: `aggregate: 'count'` / raw `graphql` via GraphQL, `bucket` / `groupBy` / the OBJECT-form `aggregate` (Wave 3: group-by + topN/Other + labelResolve) via OpenRegister's REST aggregation facets, with an optional sibling `drilldown: { route, filterParam }` that navigates on segment/bar click with the raw category key in the query.
21
+ Supported `props` keys forwarded by the dispatcher: `chartKind` (→ `type`), `series`, `categories`, `labels`, `options`, `colors`, `toolbar`, `legend`, `height`, `width`, `unavailableLabel`, plus the display passthrough `horizontal`, `legendPosition`, `valueAxisBaseline`, `valueFormat`, `colorMap`, `emptyLabel`, `views`, `endpointSource`, `barMax`, `footnote`, `highlight`, `cellSource`, `emptyCellLabel`. The `dataSource` field resolves declaratively: `aggregate: 'count'` / raw `graphql` via GraphQL, `bucket` / `groupBy` / the OBJECT-form `aggregate` (Wave 3: group-by + topN/Other + labelResolve) via OpenRegister's REST aggregation facets, with an optional sibling `drilldown: { route, filterParam }` that navigates on segment/bar click with the raw category key in the query.
22
22
 
23
23
  Area chart — time series data:
24
24
 
@@ -107,7 +107,8 @@ Fallback slot — shown when ApexCharts is not available:
107
107
  | `horizontal` | Boolean | `false` | Render `type: "bar"` charts horizontally (row bars); an explicit `options.plotOptions.bar.horizontal` still wins |
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
- | `footnote` | String | `''` | `bar-list` / `matrix`: a line under the rows, through the host's translate |
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
+ | `emptyCellLabel` | String | `''` | `matrix`: the text of a cell without a value ("geen"), through the host's translate. Empty leaves the cell blank |
111
112
  | `highlight` | Object | `null` | `matrix`: the cell to outline, `{ row, column, columnKeys? }`, tokens such as `@object.behaviour` resolved |
112
113
  | `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) |
113
114
  | `valueAxisBaseline` | String | `'auto'` | How the value axis picks its baseline, so a small difference cannot read as a dramatic one. `'auto'` anchors magnitude marks (bar/area) at zero and stops line charts zooming past a quarter of the data's magnitude; `'zero'` always anchors at zero; `'fit'` restores plain ApexCharts autoscaling — use it for series that live far from zero (a percentage hovering 95–99), where a zero baseline flattens the signal. Pie-family charts, negative series, empty data and stacked charts (`options.chart.stacked` — where the visible height is the per-category sum, not the largest datapoint) always autoscale; an explicit `options.yaxis.min`/`max` still wins. The ceiling rounds up to a nice number (7 → 8, 62 → 80). |
@@ -126,6 +126,12 @@
126
126
  </p>
127
127
  </slot>
128
128
  </div>
129
+ <p
130
+ v-if="footnote && chartComponent && !showEmptyState && type !== 'bar-list' && type !== 'matrix'"
131
+ class="cn-chart-widget__footnote"
132
+ data-testid="cn-chart-widget-footnote">
133
+ {{ hostTranslate(footnote) }}
134
+ </p>
129
135
  </div>
130
136
  </template>
131
137
 
@@ -368,8 +374,9 @@ export default {
368
374
  },
369
375
 
370
376
  /**
371
- * A line under a `bar-list` or `matrix` ("Klik een balk om de zaken
372
- * te zien"), through the host's translate.
377
+ * A line under the chart ("Klik een balk om de zaken te zien"),
378
+ * through the host's translate: under the rows of a `bar-list` or
379
+ * `matrix`, and under the plot of an ApexCharts type.
373
380
  *
374
381
  * @spec openspec/changes/chart-bar-list-and-matrix/specs/chart-bar-list-and-matrix/spec.md#requirement-a-chart-can-draw-a-bar-list
375
382
  */
@@ -605,6 +612,17 @@ export default {
605
612
  default: '',
606
613
  },
607
614
 
615
+ /**
616
+ * `matrix`: the text of a cell without a value ("geen"), through the
617
+ * host's translate. Empty (the default) leaves the cell blank.
618
+ *
619
+ * @spec openspec/changes/chart-bar-list-and-matrix/specs/chart-bar-list-and-matrix/spec.md#requirement-a-chart-can-draw-a-matrix
620
+ */
621
+ emptyCellLabel: {
622
+ type: String,
623
+ default: '',
624
+ },
625
+
608
626
  /**
609
627
  * Manifest dataSource block. When set, `series` /
610
628
  * `categories` / `labels` are resolved from the GraphQL
@@ -1442,7 +1460,7 @@ export default {
1442
1460
  const value = Number(raw)
1443
1461
  return Number.isFinite(value) && raw !== null && raw !== undefined
1444
1462
  ? { text: this.formatPlain(value), level: levelOf(value), color: '', current: isCurrent }
1445
- : { text: '', level: 0, color: '', current: isCurrent }
1463
+ : { text: this.emptyCellLabel ? this.hostTranslate(this.emptyCellLabel) : '', level: 0, color: '', current: isCurrent }
1446
1464
  }),
1447
1465
  })),
1448
1466
  }
@@ -2388,6 +2406,8 @@ export default {
2388
2406
  * Map an endpoint payload whose `rowsPath` names an array of rows:
2389
2407
  * each row is a series (name at `rowLabelPath`, values at
2390
2408
  * `rowValuesPath`), the categories are at `labelsPath`.
2409
+ * A missing value stays null; a `matrix` keeps a text value ("none")
2410
+ * as text, other types read it as 0.
2391
2411
  *
2392
2412
  * @param {unknown} payload The plucked payload.
2393
2413
  * @param {object} es The `endpointSource` block.
@@ -2398,12 +2418,23 @@ export default {
2398
2418
  const rows = getByPath(payload, es.rowsPath)
2399
2419
  const rawLabels = es.labelsPath ? getByPath(payload, es.labelsPath) : undefined
2400
2420
  const labels = Array.isArray(rawLabels) ? rawLabels.map((v) => ((v === null || v === undefined) ? '' : String(v))) : []
2421
+ const keepText = this.type === 'matrix'
2422
+ const cellOf = (v) => {
2423
+ if (v === null || v === undefined) {
2424
+ return null
2425
+ }
2426
+ const num = typeof v === 'string' && v.trim() === '' ? NaN : Number(v)
2427
+ if (Number.isFinite(num)) {
2428
+ return num
2429
+ }
2430
+ return keepText && typeof v === 'string' ? v : 0
2431
+ }
2401
2432
  const series = (Array.isArray(rows) ? rows : []).map((row) => {
2402
2433
  const name = getByPath(row, es.rowLabelPath || 'label')
2403
2434
  const values = getByPath(row, es.rowValuesPath || 'values')
2404
2435
  return {
2405
2436
  name: (name === null || name === undefined) ? '' : String(name),
2406
- data: Array.isArray(values) ? values.map((v) => (v === null || v === undefined ? null : Number(v) || 0)) : [],
2437
+ data: Array.isArray(values) ? values.map(cellOf) : [],
2407
2438
  }
2408
2439
  })
2409
2440
  return { series, categories: labels, labels }
@@ -2690,6 +2721,13 @@ export default {
2690
2721
  margin: 0;
2691
2722
  }
2692
2723
 
2724
+ /* Under an ApexCharts plot the footnote is a sibling of the canvas, so it keeps
2725
+ its own line height while the canvas takes the rest of a fitted column. */
2726
+ .cn-chart-widget > .cn-chart-widget__footnote {
2727
+ flex: 0 0 auto;
2728
+ margin-top: 8px;
2729
+ }
2730
+
2693
2731
  .cn-chart-widget__matrix {
2694
2732
  display: grid;
2695
2733
  gap: 4px;
@@ -111,7 +111,7 @@
111
111
  <Pencil v-if="!isEditing" :size="20" />
112
112
  <Check v-else :size="20" />
113
113
  </template>
114
- {{ isEditing ? doneLabel : editLabel }}
114
+ {{ isEditing ? shownDoneLabel : shownEditLabel }}
115
115
  </NcButton>
116
116
  <!-- With `userLayout`, edit mode offers a way back to the
117
117
  manifest: the page's own grid and the chosen view return
@@ -1105,6 +1105,7 @@ const CHART_PROP_KEYS = [
1105
1105
  'footnote',
1106
1106
  'highlight',
1107
1107
  'cellSource',
1108
+ 'emptyCellLabel',
1108
1109
  ]
1109
1110
 
1110
1111
  /**
@@ -2400,6 +2401,25 @@ export default {
2400
2401
  return (this.pageFilters || []).filter((pf) => pf && pf.key && !inDescription.includes(pf))
2401
2402
  },
2402
2403
 
2404
+ /**
2405
+ * The edit button's label through the app's translate: a manifest's
2406
+ * `editLabel` ("Edit layout", DqDashboard) reads in the user's language.
2407
+ *
2408
+ * @return {string}
2409
+ */
2410
+ shownEditLabel() {
2411
+ return this.effectiveTranslate(this.editLabel)
2412
+ },
2413
+
2414
+ /**
2415
+ * The done button's label through the app's translate.
2416
+ *
2417
+ * @return {string}
2418
+ */
2419
+ shownDoneLabel() {
2420
+ return this.effectiveTranslate(this.doneLabel)
2421
+ },
2422
+
2403
2423
  /**
2404
2424
  * The preset list passed to the picker. Falls back to the
2405
2425
  * `DEFAULT_DATE_RANGE_PRESETS` constant when the consumer
@@ -362,7 +362,7 @@ export default {
362
362
  | `lensReasonTexts` | Object \| null | `null` | App wording for an unavailable lens, keyed `<lens>.<reason>` or `<reason>`. |
363
363
  | `loadingText` | String | `'Loading…'` | Accessible label for the loading spinner (NcLoadingIcon aria-label) |
364
364
  | `rowIcon` | String \| Function | `null` | Optional leading icon for every table row — a static MDI name or `(row) => iconName`. Forwarded to CnDataTable; fed from the manifest as `pages[].config.rowIcon`. Unset = no icon column. |
365
- | `rowClass` | Function | `null` | Callback returning CSS class(es) for a row |
365
+ | `rowClass` | Function \| Array | `null` | Callback returning CSS class(es) for a row, or (manifest `config.rowClass`) rules `[{ when: { field, op?, value }, class }]` compiled into one: each adds its class to a row whose field compares to the value (`visibleWhen` operators; two dates compare as moments, `@today` included). |
366
366
  | `rowIcon` | String \| Function | `null` | Optional leading row icon forwarded to CnDataTable — a static MDI icon name or `(row) => iconName`. Fed from the manifest as `pages[].config.rowIcon`; unset = no icon column |
367
367
  | `inlineActionCount` | Number | `2` | How many row actions to show inline (rest go in overflow menu) |
368
368
  | `showMassImport` | Boolean | `true` | Whether to show the mass Import action |
@@ -414,6 +414,7 @@ export default {
414
414
  | `quickFilterMode` | String | `'chips'` | How the quick filters render: `'chips'` (pill strip) or `'dropdown'` (a single `NcSelect`). Fed from `pages[].config.quickFilterMode`. |
415
415
  | `quickFilterMultiple` | Boolean | `false` | Allow several quick filters active at once; selected tabs' filters are OR-ed into the fetch (same field → `field[]=` array). Fed from `pages[].config.quickFilterMultiple`. |
416
416
  | `quickFilterMaxVisible` | Number | `0` | Chips mode only — how many quick-filter pills render inline before the rest move behind one more chip — a `⋯` pill that opens a panel of the hidden lenses. `0` renders every tab, which wraps the actions bar onto a second line once a page declares more than a handful. The visible set is the first *n* entries of `quickFilters`. Fed from `pages[].config.quickFilterMaxVisible`. |
417
+ | `quickFilterPlacement` | String | `''` | Where the quick-filter chips sit. `'toolbar'` keeps them on one row with the Filter button and the view switch, in whatever width that row has beside a side panel (DqTeamwachtrij): the chips take the room the buttons leave, and those that do not fit move behind the overflow chip. Fed from `pages[].config.quickFilterPlacement`. |
417
418
  | `cardComponent` | String | `''` | Optional name of a consumer-provided card component (registered in the v2 `registry` — any kind carrying a `component` — or in the legacy `customComponents` map on `CnAppRoot`) to render in place of the default `CnObjectCard` when the page is in card-grid view mode. Resolution priority: `#card` scoped slot → `cardComponent` registry entry → default `CnObjectCard`. Unknown names log `console.warn` once and fall back to the default. |
418
419
  | `customComponents` | Object | `null` | Optional explicit `customComponents` registry. Overrides the registry injected from `CnAppRoot` via `cnCustomComponents`. Mostly used by unit tests; production consumers register components on `CnAppRoot` instead. |
419
420
  | `subscribe` | Boolean | `true` | Self-fetch mode only. When `register` + `schema` are set (and no `objects` prop), the page subscribes to the collection's `or-collection-{register}-{schema}` live-update scope on mount and refetches on an update event; released on unmount. Set `false` (manifest: `config.subscribe: false`) for static / read-once views. No-op in consumer-managed mode or on stores without live-updates support. |
@@ -1136,3 +1137,6 @@ columns are right.
1136
1137
  ## Board look
1137
1138
 
1138
1139
  With `look: "board"` (the page's `look` prop, `config.look`, or the app's) the index page takes the DqZaken setup: a header with the count line from `countText` (`{shown}`, `{total}`), the buttons Download, Actions, the buildiq square and the primary one in that order, a two-row toolbar (saved-view chips, Filter with a count and the view switch; then search and active filter chips), a bulk band between toolbar and table (`bulkHint`), the table as a white card with one menu button per row, and a footer inside the card (`footerNote`). `cardFields` names the facts of each card in the cards view. Without the look nothing changes.
1140
+
1141
+ `bulkBandAlways` (`config.bulkBandAlways`) draws the board bulk band before a selection too, its buttons
1142
+ disabled until rows are selected (DqZaken, DqWerkbord).
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cn-index-page"
3
- :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel }]"
3
+ :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--quick-filters-toolbar': quickFilterPlacement === 'toolbar' }]"
4
4
  data-testid="cn-index-page"
5
5
  @keydown="onListKeydown">
6
6
  <!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
@@ -163,6 +163,7 @@
163
163
  :showBuildiqButton="!buildiqInHeader"
164
164
  :bulkNoun="boardBulkNoun"
165
165
  :bulkHint="bulkHint ? cnTranslate(bulkHint) : bulkHint"
166
+ :bulkBandAlways="bulkBandAlways"
166
167
  :pagination="effectivePagination"
167
168
  :objectCount="effectiveObjects.length"
168
169
  :selectable="selectable"
@@ -369,6 +370,7 @@
369
370
  :tabs="tabStripEntries"
370
371
  :mode="quickFilterMode"
371
372
  :maxVisible="quickFilterMaxVisible"
373
+ :fit="quickFilterPlacement === 'toolbar'"
372
374
  :multiple="quickFilterMultiple"
373
375
  :activeIndex="activeQuickFilterIndex"
374
376
  :selectedIndices="selectedQuickFilterIndices"
@@ -755,7 +757,7 @@
755
757
  :excludeColumns="excludeColumns"
756
758
  :includeColumns="includeColumns"
757
759
  :columnOverrides="columnOverrides"
758
- :rowClass="rowClass"
760
+ :rowClass="compiledRowClass"
759
761
  :rowTitle="rowTitle"
760
762
  :showUnread="showUnreadRows"
761
763
  :filterable="tableHeaderFilters"
@@ -1292,6 +1294,7 @@ import { resolveFilterMap } from '../../utils/routeFilters.js'
1292
1294
  import { availableRowActions, DEFAULT_ROW_ACTION_FIELD, refusalReasonFor, undeclaredRowActions, withoutViewWhenRowOpensDetail } from '../../utils/rowActionAvailability.js'
1293
1295
  import { isRowActionVisible, rowActionPayload } from '../../utils/rowActionItem.js'
1294
1296
  import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
1297
+ import { compileRowClass } from '../../utils/rowClassRules.js'
1295
1298
  import { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'
1296
1299
  import { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, savedViewScope, viewMatchesScope } from '../../utils/savedViewHelpers.js'
1297
1300
  import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
@@ -1938,6 +1941,17 @@ export default {
1938
1941
  default: '',
1939
1942
  },
1940
1943
 
1944
+ /**
1945
+ * Board look: draw the bulk band before anything is selected too, its
1946
+ * buttons disabled until there is a selection (DqZaken, DqWerkbord).
1947
+ * Manifest key `config.bulkBandAlways`. Only when the band has an
1948
+ * action to offer.
1949
+ */
1950
+ bulkBandAlways: {
1951
+ type: Boolean,
1952
+ default: false,
1953
+ },
1954
+
1941
1955
  /**
1942
1956
  * Board look: the 13px hint after the bulk band's buttons ("See what
1943
1957
  * changes first, then run it"). Manifest key `config.bulkHint`.
@@ -2119,6 +2133,21 @@ export default {
2119
2133
  default: null,
2120
2134
  },
2121
2135
 
2136
+ /**
2137
+ * Where the quick-filter chips sit (manifest `config.quickFilterPlacement`).
2138
+ * `''` (the default) as before. `'toolbar'` keeps them on one row with
2139
+ * the Filter button and the view switch, in whatever width that row has
2140
+ * beside a side panel (DqTeamwachtrij): the chips take the room the
2141
+ * buttons leave, and those that do not fit move into the overflow chip.
2142
+ *
2143
+ * @type {''|'toolbar'}
2144
+ */
2145
+ quickFilterPlacement: {
2146
+ type: String,
2147
+ default: '',
2148
+ validator: (v) => ['', 'toolbar'].includes(v),
2149
+ },
2150
+
2122
2151
  /**
2123
2152
  * How the quick filters render: `'chips'` (pill strip, default) or
2124
2153
  * `'dropdown'` (a single `NcSelect`). Sourced from the manifest as
@@ -2695,9 +2724,18 @@ export default {
2695
2724
  default: 'Loading…',
2696
2725
  },
2697
2726
 
2698
- /** Function returning CSS class(es) for a row */
2727
+ /**
2728
+ * CSS class(es) for a row: a function `(row) => string`, or (manifest
2729
+ * `config.rowClass`) rules `[{ when: { field, op?, value }, class }]`,
2730
+ * each adding its class to a row whose field compares to the value
2731
+ * (the `visibleWhen` operators, `@today` included). DqTaken marks an
2732
+ * overdue task: `{ when: { field: "dueAt", op: "lt", value: "@today" },
2733
+ * class: "cn-row--overdue" }`.
2734
+ *
2735
+ * @type {((row: object) => string)|Array<{when: {field: string, op?: string, value: unknown}, class: string}>|null}
2736
+ */
2699
2737
  rowClass: {
2700
- type: Function,
2738
+ type: [Function, Array],
2701
2739
  default: null,
2702
2740
  },
2703
2741
 
@@ -3779,6 +3817,10 @@ export default {
3779
3817
  //
3780
3818
  // Seeded from the route query so a shared link lands filtered, the
3781
3819
  // same way `_order` and a deep-link filter already do for self-fetch.
3820
+ // The page a paging named source shows: page 1 at the source config's
3821
+ // `limit` (20 by default), moved by the pager.
3822
+ const namedPageSize = Number(props.sourceConfig && props.sourceConfig.limit) > 0 ? Number(props.sourceConfig.limit) : 20
3823
+ const namedPaging = ref({ page: 1, limit: namedPageSize })
3782
3824
  const namedActiveFilters = ref(namedFiltersFromRoute(getCurrentInstance(), props))
3783
3825
 
3784
3826
  const {
@@ -3787,7 +3829,7 @@ export default {
3787
3829
  namedRows,
3788
3830
  namedLoading,
3789
3831
  namedQuickFilters,
3790
- } = useNamedSource(props, { activeQuickFilterIndex, activeFilters: namedActiveFilters })
3832
+ } = useNamedSource(props, { activeQuickFilterIndex, activeFilters: namedActiveFilters, paging: namedPaging })
3791
3833
 
3792
3834
  return {
3793
3835
  isBoard: isBoardLook,
@@ -3799,6 +3841,7 @@ export default {
3799
3841
  namedLoading,
3800
3842
  namedQuickFilters,
3801
3843
  namedActiveFilters,
3844
+ namedPaging,
3802
3845
  contextMenuOpen,
3803
3846
  contextMenuRow,
3804
3847
  openContextMenu,
@@ -4761,6 +4804,9 @@ export default {
4761
4804
  const loaded = this.effectivePagination || {}
4762
4805
  return Number(loaded.page || 1) < Number(loaded.pages || 1)
4763
4806
  }
4807
+ if (this.isNamedSource && this.namedSource && this.namedSource.pages === true) {
4808
+ return Number(this.effectivePagination?.pages || 1) > 1
4809
+ }
4764
4810
  return Number(this.pagination?.pages || 1) > 1
4765
4811
  },
4766
4812
 
@@ -5800,7 +5846,13 @@ export default {
5800
5846
  // A named source that reports its own total (the search source)
5801
5847
  // gives the count line its number.
5802
5848
  if (this.isNamedSource && this.namedSource && typeof this.namedSource.total === 'function') {
5803
- return { ...(this.pagination || {}), total: this.namedSource.total() }
5849
+ const total = this.namedSource.total()
5850
+ // A source with `pages: true` gets the pager the self-fetched lists draw.
5851
+ if (this.namedSource.pages === true) {
5852
+ const { page, limit } = this.namedPaging
5853
+ return { page, limit, total, pages: Math.max(1, Math.ceil(total / limit)) }
5854
+ }
5855
+ return { ...(this.pagination || {}), total }
5804
5856
  }
5805
5857
  return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
5806
5858
  },
@@ -6053,6 +6105,16 @@ export default {
6053
6105
  return (this.effectiveSchema && this.effectiveSchema.slug) || ''
6054
6106
  },
6055
6107
 
6108
+ /**
6109
+ * The row-class function the table takes: a function as given, or the
6110
+ * manifest's rules compiled.
6111
+ *
6112
+ * @return {?(row: object) => string}
6113
+ */
6114
+ compiledRowClass() {
6115
+ return compileRowClass(this.rowClass)
6116
+ },
6117
+
6056
6118
  /** @return {number} How many leading columns the person pinned (0 while a saved view's columns are applied). */
6057
6119
  pinnedColumnCount() {
6058
6120
  if (!this.personalColumns || Array.isArray(this.appliedViewColumns) || !this.personalLayout) {
@@ -8777,6 +8839,9 @@ export default {
8777
8839
  if (this.isSelfFetchMode && typeof this.list.onPageChange === 'function') {
8778
8840
  this.list.onPageChange(page)
8779
8841
  }
8842
+ if (this.isNamedSource && this.namedSource && this.namedSource.pages === true) {
8843
+ this.namedPaging = { ...this.namedPaging, page }
8844
+ }
8780
8845
  this.$emit('page-changed', page)
8781
8846
  },
8782
8847
 
@@ -8792,6 +8857,9 @@ export default {
8792
8857
  if (this.isSelfFetchMode && typeof this.list.onPageSizeChange === 'function') {
8793
8858
  this.list.onPageSizeChange(size)
8794
8859
  }
8860
+ if (this.isNamedSource && this.namedSource && this.namedSource.pages === true) {
8861
+ this.namedPaging = { page: 1, limit: size }
8862
+ }
8795
8863
  this.$emit('page-size-changed', size)
8796
8864
  },
8797
8865
 
@@ -105,6 +105,9 @@ export function useNamedSource(props, options = {}) {
105
105
  const tabs = manifestTabs || sourceTabs
106
106
  const activeIndex = options.activeQuickFilterIndex || null
107
107
  const activeFilters = options.activeFilters || null
108
+ // A source that pages (`pages: true`) gets the page as `limit` and
109
+ // `offset`, from the pager the page draws for it.
110
+ const paging = source.pages === true ? (options.paging || null) : null
108
111
 
109
112
  // Seed the default tab before the watcher below registers: the mount load
110
113
  // already carries this tab's filter, so the seed must not fire a second
@@ -165,6 +168,7 @@ export function useNamedSource(props, options = {}) {
165
168
  activeFilters ? activeFilters.value : null,
166
169
  props.entitySource,
167
170
  ),
171
+ ...(paging ? { limit: paging.value.limit, offset: (paging.value.page - 1) * paging.value.limit } : {}),
168
172
  },
169
173
  tokenCtx(),
170
174
  )
@@ -182,15 +186,35 @@ export function useNamedSource(props, options = {}) {
182
186
 
183
187
  // Registered AFTER the seed above, so only a person switching tabs
184
188
  // reloads. Without tabs there is nothing to watch.
189
+ /**
190
+ * Reload for a new query: on a paging source back to page 1, which
191
+ * reloads through the paging watcher below.
192
+ *
193
+ * @return {void}
194
+ */
195
+ function reloadFromFirstPage() {
196
+ if (paging && paging.value.page !== 1) {
197
+ paging.value = { ...paging.value, page: 1 }
198
+ return
199
+ }
200
+ loadActive()
201
+ }
202
+
185
203
  if (tabs && activeIndex) {
186
- watch(activeIndex, loadActive)
204
+ watch(activeIndex, reloadFromFirstPage)
187
205
  }
188
206
 
189
207
  // A sidebar choice is a new QUERY, not a new view of the page already
190
208
  // fetched, so it reloads. `deep` because the map is rewritten key by key
191
209
  // and a shallow watch misses a second value added to the same field.
192
210
  if (activeFilters) {
193
- watch(activeFilters, loadActive, { deep: true })
211
+ watch(activeFilters, reloadFromFirstPage, { deep: true })
212
+ }
213
+
214
+ // A new page, or a new page size, is a new request. A new tab or filter
215
+ // first returns to page 1, which reloads through here.
216
+ if (paging) {
217
+ watch(paging, loadActive, { deep: true })
194
218
  }
195
219
 
196
220
  // No manual invalidation tick here on purpose. The adapters read from a
@@ -239,6 +239,11 @@ export default {
239
239
  CnMenuItemIcon,
240
240
  },
241
241
 
242
+ inject: {
243
+ /** Host translate function provided by CnAppRoot, for the item labels. */
244
+ cnTranslate: { default: () => (key) => key },
245
+ },
246
+
242
247
  props: {
243
248
  /**
244
249
  * Persisted widget content `{items, style, orientation, showIcons,
@@ -288,7 +293,26 @@ export default {
288
293
  */
289
294
  items() {
290
295
  const arr = this.content?.items
291
- return Array.isArray(arr) ? arr : []
296
+ if (!Array.isArray(arr)) {
297
+ return []
298
+ }
299
+ // Labels are manifest strings: they go through the app's translate,
300
+ // children included, so "Case types" reads "Zaaktypen" (DqCatalogus).
301
+ const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
302
+ const translated = (list) => list.map((item) => {
303
+ if (!item || typeof item !== 'object') {
304
+ return item
305
+ }
306
+ const out = { ...item }
307
+ if (typeof item.label === 'string' && item.label !== '') {
308
+ out.label = translate(item.label)
309
+ }
310
+ if (Array.isArray(item.children)) {
311
+ out.children = translated(item.children)
312
+ }
313
+ return out
314
+ })
315
+ return translated(arr)
292
316
  },
293
317
 
294
318
  /**
@@ -59,6 +59,7 @@ In `manifest.json` (the usual entry-point — CnIndexPage mounts the bar for you
59
59
  - `selectLabel` (String, default `'Filter'`) — accessible label / placeholder for the dropdown control.
60
60
  - `placeholder` (String) — placeholder text for the dropdown (falls back to `selectLabel`).
61
61
  - `maxVisible` (Number, default `0`) — chips mode only: how many pills render inline before the rest move behind one more chip, a `⋯` pill that opens a small panel of the hidden lenses (also as chips). `0` renders every tab. The visible set is the first `maxVisible` entries of `tabs`, so their declared order decides what stays in the strip; an active hidden tab moves its label and fill onto that chip. Manifest: `config.quickFilterMaxVisible`.
62
+ - `fit` (Boolean, default `false`) — chips mode only: show as many chips as the bar's own width holds and move the rest behind the overflow chip, measured again when the width, the tabs or the counts change. The bar takes the room its parent gives it; CnIndexPage's `quickFilterPlacement: "toolbar"` gives it what the Filter button and the view switch leave on their row. With `maxVisible` too, the smaller of the two wins.
62
63
  - `counts` (Object or Array, default `null`): how many records each tab matches, keyed by tab index. A tab with a number shows it after its label. CnIndexPage fills this for tabs that set `showCount`.
63
64
 
64
65
  ### Events