@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
@@ -24,8 +24,28 @@
24
24
  </div>
25
25
  <!-- Chips mode (default): the clickable tab strip. -->
26
26
  <div v-else
27
+ ref="chipsRoot"
27
28
  class="cn-quick-filter-bar"
28
- :class="{ 'cn-quick-filter-bar--inline': inline }">
29
+ :class="{ 'cn-quick-filter-bar--inline': inline, 'cn-quick-filter-bar--fit': fit }">
30
+ <!-- With `fit`: an invisible copy of every chip and the overflow chip,
31
+ measured to work out how many chips the bar's width holds. -->
32
+ <div v-if="fit"
33
+ ref="measure"
34
+ class="cn-quick-filter-bar__measure"
35
+ aria-hidden="true">
36
+ <span v-for="entry in entries"
37
+ :key="'m:' + entry.tab.label + ':' + entry.index"
38
+ class="cn-quick-filter-bar__tab"
39
+ :class="{ 'cn-quick-filter-bar__tab--active': isChipActive(entry.index) }"
40
+ data-measure="chip">
41
+ <span class="cn-quick-filter-bar__label">{{ tl(entry.tab.label) }}</span>
42
+ <span v-if="countOf(entry) !== null" class="cn-quick-filter-bar__count">{{ ' ' + countOf(entry) }}</span>
43
+ </span>
44
+ <span class="cn-quick-filter-bar__tab cn-quick-filter-bar__more" data-measure="more">
45
+ <span v-if="overflowLabel" class="cn-quick-filter-bar__label">{{ overflowLabel }}</span>
46
+ <DotsHorizontal :size="16" class="cn-quick-filter-bar__icon" />
47
+ </span>
48
+ </div>
29
49
  <!-- The tablist holds ONLY tabs: the overflow chip is a sibling, not a
30
50
  child, because a role="tablist" may not contain a menu button. -->
31
51
  <!-- Board look: the tabs are chips, pressed buttons in a group, not a
@@ -266,6 +286,19 @@ export default {
266
286
  type: [Object, Array],
267
287
  default: null,
268
288
  },
289
+
290
+ /**
291
+ * Chips mode only: show as many chips as the bar's own width holds and
292
+ * move the rest into the overflow chip, measured again whenever the
293
+ * width, the tabs or the counts change. The bar takes the room it is
294
+ * given (CnIndexPage's `quickFilterPlacement: "toolbar"` gives it what
295
+ * the Filter button and the view switch leave on their row). With
296
+ * `maxVisible` too, the smaller of the two wins.
297
+ */
298
+ fit: {
299
+ type: Boolean,
300
+ default: false,
301
+ },
269
302
  },
270
303
 
271
304
  emits: ['update:active-index', 'update:selected-indices'],
@@ -274,6 +307,8 @@ export default {
274
307
  return {
275
308
  /** Whether the overflow chip's panel is open. */
276
309
  moreOpen: false,
310
+ /** With `fit`: how many chips the width holds; null before the first measure. */
311
+ fitCount: null,
277
312
  }
278
313
  },
279
314
 
@@ -303,10 +338,24 @@ export default {
303
338
  * @return {Array<{tab: object, index: number}>}
304
339
  */
305
340
  visibleEntries() {
306
- if (!this.maxVisible || this.tabs.length <= this.maxVisible) {
341
+ const limit = this.effectiveMax
342
+ if (!limit || this.tabs.length <= limit) {
307
343
  return this.entries
308
344
  }
309
- return this.entries.slice(0, this.maxVisible)
345
+ return this.entries.slice(0, limit)
346
+ },
347
+
348
+ /**
349
+ * How many chips render: `maxVisible`, and with `fit` no more than the
350
+ * width holds; 0 for no limit.
351
+ *
352
+ * @return {number}
353
+ */
354
+ effectiveMax() {
355
+ if (!this.fit || this.fitCount === null) {
356
+ return this.maxVisible
357
+ }
358
+ return this.maxVisible ? Math.min(this.maxVisible, this.fitCount) : this.fitCount
310
359
  },
311
360
 
312
361
  /**
@@ -316,10 +365,11 @@ export default {
316
365
  * @return {Array<{tab: object, index: number}>}
317
366
  */
318
367
  overflowEntries() {
319
- if (!this.maxVisible || this.tabs.length <= this.maxVisible) {
368
+ const limit = this.effectiveMax
369
+ if (!limit || this.tabs.length <= limit) {
320
370
  return []
321
371
  }
322
- return this.entries.slice(this.maxVisible)
372
+ return this.entries.slice(limit)
323
373
  },
324
374
 
325
375
  /**
@@ -399,7 +449,79 @@ export default {
399
449
  },
400
450
  },
401
451
 
452
+ watch: {
453
+ tabs() {
454
+ this.scheduleFit()
455
+ },
456
+
457
+ counts: {
458
+ deep: true,
459
+ handler() {
460
+ this.scheduleFit()
461
+ },
462
+ },
463
+ },
464
+
465
+ mounted() {
466
+ if (this.fit && typeof ResizeObserver === 'function' && this.$refs.chipsRoot) {
467
+ this.fitObserver = new ResizeObserver(() => this.measureFit())
468
+ this.fitObserver.observe(this.$refs.chipsRoot)
469
+ }
470
+ this.scheduleFit()
471
+ },
472
+
473
+ beforeUnmount() {
474
+ if (this.fitObserver) {
475
+ this.fitObserver.disconnect()
476
+ this.fitObserver = null
477
+ }
478
+ },
479
+
402
480
  methods: {
481
+ /** Measure the fit after the next render. */
482
+ scheduleFit() {
483
+ if (this.fit) {
484
+ this.$nextTick(() => this.measureFit())
485
+ }
486
+ },
487
+
488
+ /**
489
+ * Work out how many chips the bar's width holds, from the invisible
490
+ * copy: every chip that fits, and when not all do, room left for the
491
+ * overflow chip. At least one chip always shows.
492
+ */
493
+ measureFit() {
494
+ const box = this.$refs.measure
495
+ const root = this.$refs.chipsRoot
496
+ if (!this.fit || !box || !root) {
497
+ return
498
+ }
499
+ const available = root.clientWidth
500
+ const chips = [...box.querySelectorAll('[data-measure="chip"]')].map((el) => el.offsetWidth)
501
+ const more = box.querySelector('[data-measure="more"]')
502
+ const moreWidth = more ? more.offsetWidth : 0
503
+ const gap = parseFloat(getComputedStyle(box).columnGap) || 8
504
+ if (available <= 0 || chips.length === 0) {
505
+ this.fitCount = null
506
+ return
507
+ }
508
+ const total = chips.reduce((sum, width) => sum + width, 0) + gap * (chips.length - 1)
509
+ if (total <= available) {
510
+ this.fitCount = null
511
+ return
512
+ }
513
+ let used = moreWidth
514
+ let count = 0
515
+ for (const width of chips) {
516
+ if (used + gap + width > available) {
517
+ break
518
+ }
519
+ used += gap + width
520
+ count++
521
+ }
522
+ this.fitCount = Math.max(1, count)
523
+ },
524
+
403
525
  /**
404
526
  * A manifest string through the host's label lookup (`cnTranslate`), so a key
405
527
  * such as "Mine" reads in the user's language.
@@ -531,6 +653,38 @@ export default {
531
653
  /* Inline variant — bare strip for embedding inside another bar (e.g. the
532
654
  CnActionsBar #filters slot), so it reads as a control beside the view toggle
533
655
  rather than a separate bordered row. */
656
+ /* With `fit`: one line, as wide as it is given; the chips it cannot hold
657
+ are in the overflow chip. The measuring copy takes no room and is never
658
+ seen or read. */
659
+ .cn-quick-filter-bar--fit {
660
+ position: relative;
661
+ flex-wrap: nowrap;
662
+ min-width: 0;
663
+ }
664
+
665
+ .cn-quick-filter-bar--fit .cn-quick-filter-bar__tabs {
666
+ flex-wrap: nowrap;
667
+ min-width: 0;
668
+ }
669
+
670
+ .cn-quick-filter-bar__measure {
671
+ position: absolute;
672
+ inset-block-start: 0;
673
+ inset-inline-start: 0;
674
+ display: flex;
675
+ gap: 8px;
676
+ width: 0;
677
+ height: 0;
678
+ overflow: hidden;
679
+ visibility: hidden;
680
+ pointer-events: none;
681
+ white-space: nowrap;
682
+ }
683
+
684
+ .cn-quick-filter-bar__measure > * {
685
+ flex: none;
686
+ }
687
+
534
688
  .cn-quick-filter-bar--inline {
535
689
  padding: 0;
536
690
  border-bottom: none;
@@ -88,8 +88,8 @@ import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatch
88
88
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
89
89
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
90
90
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
91
+ import { compileRowClass } from '../../utils/rowClassRules.js'
91
92
  import { fieldsFromSchema } from '../../utils/schema.js'
92
- import { compareVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'
93
93
  import { CnIcon } from '../CnIcon/index.js'
94
94
  import { CnRowActions } from '../CnRowActions/index.js'
95
95
  import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
@@ -762,28 +762,7 @@ export default {
762
762
  * @return {?(row: object) => string} The row-class function, or null.
763
763
  */
764
764
  compiledRowClass() {
765
- const rc = this.rowClass
766
- if (typeof rc === 'function') {
767
- return rc
768
- }
769
- if (!Array.isArray(rc) || rc.length === 0) {
770
- return null
771
- }
772
- const rules = rc
773
- return (row) => {
774
- const classes = []
775
- for (const rule of rules) {
776
- if (!rule || !rule.class) {
777
- continue
778
- }
779
- const when = rule.when || {}
780
- const actual = readVisibleWhenPath(row, when.field)
781
- if (compareVisibleWhen(actual, when.op || 'eq', when.value)) {
782
- classes.push(rule.class)
783
- }
784
- }
785
- return classes.join(' ')
786
- }
765
+ return compileRowClass(this.rowClass)
787
766
  },
788
767
 
789
768
  /**
@@ -530,6 +530,15 @@ function tasksSource() {
530
530
  */
531
531
  loading: () => !!store.loading,
532
532
 
533
+ // The inbox pages: the page sends `limit` and `offset` and draws the
534
+ // pager from the datastore total.
535
+ pages: true,
536
+
537
+ /**
538
+ * @return {number} The datastore total, independent of the page size.
539
+ */
540
+ total: () => Number(store.total) || 0,
541
+
533
542
  /**
534
543
  * Count the inbox for a config without touching the rows on screen,
535
544
  * for a quick-filter tab with `showCount`.
@@ -128,6 +128,16 @@
128
128
  gap: 8px;
129
129
  }
130
130
 
131
+ /* `quickFilterPlacement: "toolbar"` (DqTeamwachtrij): the chips take the room
132
+ the Filter button and the view switch leave on row 1, so the three stay on
133
+ one row in the column beside a side panel; the chips that do not fit move
134
+ into the overflow chip (CnQuickFilterBar `fit`) instead of pushing the
135
+ buttons onto a row of their own. */
136
+ .cn-look-board.cn-index-page--quick-filters-toolbar .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-quick-filter-bar {
137
+ flex: 1 1 0;
138
+ min-width: 0;
139
+ }
140
+
131
141
  /* Row 2: the search field and the active filters on a line of their own. */
132
142
  .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--search {
133
143
  display: flex;
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.124.0",
6
+ "version": "2.125.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -2221,7 +2221,8 @@
2221
2221
  },
2222
2222
  "emptyLabel": { "type": "string", "description": "Empty-state message rendered instead of the chart when the resolved series contain no data points." },
2223
2223
  "barMax": { "type": "number", "exclusiveMinimum": 0, "description": "chartKind 'bar-list': the value a full track stands for (100 for percentages). Omitted = the largest value." },
2224
- "footnote": { "type": "string", "description": "chartKind 'bar-list' or 'matrix': a line under the rows, through the host's translate." },
2224
+ "footnote": { "type": "string", "description": "A line under the chart, through the host's translate: under the rows of a bar-list or matrix, and under the plot of a line, area or bar chart." },
2225
+ "emptyCellLabel": { "type": "string", "description": "chartKind 'matrix': the text of a cell without a value (\"geen\"), through the host's translate. Empty leaves the cell blank. Passed through to CnChartWidget." },
2225
2226
  "highlight": {
2226
2227
  "type": "object",
2227
2228
  "additionalProperties": false,
@@ -3738,7 +3739,58 @@
3738
3739
  "enum": ["flows", "flowRuns", "tasks", "endpoint", "search", "users", "organisations"],
3739
3740
  "description": "Name a registered NON-OBJECT entity collection for a type='index' page, instead of register+schema. Deliberately NOT called `source`: that key is already taken on page config and is polymorphic (a URL string in some manifests, an object with `params` in others), so reusing it would give one key two meanings and break six deployed shillinq pages. A flow definition is deliberately not an OpenRegister object, so an index had nothing to bind to and such lists became bespoke type='custom' pages. With a source the index loads the list itself. Takes precedence over register/schema; non-empty `objects` still wins over both. Registered sources live in src/composables/indexSources.js. `tasks` lists the viewer's task inbox from OpenRegister's flow-tasks read (cn-tasks-entity-source). `organisations` lists the OpenRegister organisations the viewer belongs to. On a type='detail' page a source that reads one record (today `organisations`) loads the record whose id is the route's objectId, instead of register+schema, and feeds the header, pills, widgets and @objectId / @object.* tokens like a fetched object (organisations-entity-source)."
3740
3741
  },
3741
- "sourceConfig": { "type": "object", "additionalProperties": true, "description": "type='index' with `entitySource`: what the named source loads with. `endpoint`: `url` (an app path, required) and `rowsPath` (default `results`); its other keys go along as query parameters. `search`: `kinds` (`{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }` or `{ id, label, files: true }`), `term` and `termQuery` (default `q`). `users`: `defaultGroup`, `functionField`, `rolesField`. `flows` / `tasks`: `app`, `scope`. Maps to CnIndexPage `sourceConfig`.", "properties": { "url": { "type": "string", "minLength": 1 }, "rowsPath": { "type": "string", "minLength": 1 }, "kinds": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true, "properties": { "id": { "type": "string", "minLength": 1 }, "label": { "type": "string" }, "register": { "type": "string" }, "schema": { "type": "string" }, "files": { "type": "boolean" } } } }, "term": { "type": "string" }, "termQuery": { "type": "string", "minLength": 1 }, "defaultGroup": { "type": "string", "minLength": 1 } } },
3742
+ "bulkBandAlways": { "type": "boolean", "default": false, "description": "type='index', board look: draw the bulk band (its actions and hint) before a selection too, its buttons disabled until rows are selected (DqZaken, DqWerkbord). Maps to CnIndexPage `bulkBandAlways`." },
3743
+ "quickFilterPlacement": { "type": "string", "enum": ["", "toolbar"], "default": "", "description": "type='index': where the quick-filter chips sit. `toolbar` keeps them on one row with the Filter button and the view switch beside a side panel, the chips that do not fit behind the overflow chip (DqTeamwachtrij). Maps to CnIndexPage `quickFilterPlacement`." },
3744
+ "rowClass": {
3745
+ "type": "array",
3746
+ "description": "type='index': per-row CSS class rules, compiled into the table's rowClass function as on the object-table widget. Each rule { when: { field, op?, value }, class } adds `class` to a row whose `field` (a dot-path) compares to `value` with `op` (default eq; the visibleWhen operators and tokens such as `@today`, with two dates compared as moments), e.g. an overdue task `{ when: { field: \"dueAt\", op: \"lt\", value: \"@today\" }, class: \"cn-row--overdue\" }`. Maps to CnIndexPage `rowClass`.",
3747
+ "items": {
3748
+ "type": "object",
3749
+ "additionalProperties": false,
3750
+ "required": [
3751
+ "when",
3752
+ "class"
3753
+ ],
3754
+ "properties": {
3755
+ "when": {
3756
+ "type": "object",
3757
+ "additionalProperties": false,
3758
+ "required": [
3759
+ "field",
3760
+ "value"
3761
+ ],
3762
+ "description": "The predicate evaluated against the row (shared visibleWhen grammar, LOCAL form).",
3763
+ "properties": {
3764
+ "field": {
3765
+ "type": "string",
3766
+ "description": "Dot-path into the row."
3767
+ },
3768
+ "op": {
3769
+ "type": "string",
3770
+ "enum": [
3771
+ "eq",
3772
+ "neq",
3773
+ "gt",
3774
+ "gte",
3775
+ "lt",
3776
+ "lte"
3777
+ ],
3778
+ "default": "eq",
3779
+ "description": "Comparison operator (default eq)."
3780
+ },
3781
+ "value": {
3782
+ "description": "Literal right-hand side of the comparison."
3783
+ }
3784
+ }
3785
+ },
3786
+ "class": {
3787
+ "type": "string",
3788
+ "description": "CSS class added to the row when `when` holds."
3789
+ }
3790
+ }
3791
+ }
3792
+ },
3793
+ "sourceConfig": { "type": "object", "additionalProperties": true, "description": "type='index' with `entitySource`: what the named source loads with. `endpoint`: `url` (an app path, required) and `rowsPath` (default `results`); its other keys go along as query parameters. `search`: `kinds` (`{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }` or `{ id, label, files: true }`), `term` and `termQuery` (default `q`). `users`: `defaultGroup`, `functionField`, `rolesField`. `flows` / `tasks`: `app`, `scope`. Maps to CnIndexPage `sourceConfig`.", "properties": { "url": { "type": "string", "minLength": 1 }, "rowsPath": { "type": "string", "minLength": 1 }, "kinds": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true, "properties": { "id": { "type": "string", "minLength": 1 }, "label": { "type": "string" }, "register": { "type": "string" }, "schema": { "type": "string" }, "files": { "type": "boolean" } } } }, "term": { "type": "string" }, "termQuery": { "type": "string", "minLength": 1 }, "defaultGroup": { "type": "string", "minLength": 1 }, "limit": { "type": "integer", "minimum": 1, "description": "A paging source: rows per page (20 by default); the pager moves through the pages." } } },
3742
3794
  "app": {
3743
3795
  "type": "string",
3744
3796
  "description": "Scopes a named `entitySource` to one app (e.g. entitySource='flows' with app='dossiq' lists that app's flows)."
@@ -379,25 +379,45 @@ const CONNECTION_STATUS_LABELS = {
379
379
  error: 'Error',
380
380
  }
381
381
 
382
+ /**
383
+ * The short labels `formatterOptions.short` draws (board DqInstellingen
384
+ * "Aan / Uit / Fout"): a connection that works in some form is on, one that
385
+ * does not run is off, and a failing one is an error.
386
+ */
387
+ const CONNECTION_STATUS_SHORT_LABELS = {
388
+ configured: 'On',
389
+ limited: 'On',
390
+ simulated: 'On',
391
+ unconfigured: 'Off',
392
+ disabled: 'Off',
393
+ unavailable: 'Off',
394
+ error: 'Error',
395
+ }
396
+
382
397
  /**
383
398
  * Connection status formatter (`connectionStatus` registry key) for the
384
399
  * fleet connection registry. Renders one of the seven stored status values
385
400
  * as its translated label through the library's own slug (`nextcloud-vue`).
401
+ * With `formatterOptions.short` it renders the board's three short labels
402
+ * instead: On (configured, limited, simulated), Off (not configured,
403
+ * switched off, not available) and Error.
386
404
  *
387
405
  * An unknown value passes through unchanged, so a status added later still
388
406
  * shows as its raw value instead of an empty cell. Null and undefined render
389
407
  * as `''`. Never throws.
390
408
  *
391
409
  * @param {unknown} value The stored status value.
410
+ * @param {object} [row] The row (unused).
411
+ * @param {object} [property] The column's property (unused).
412
+ * @param {{short?: boolean}} [options] The column's `formatterOptions`.
392
413
  * @return {string} The translated label, or the value itself when it is not one of the seven.
393
414
  */
394
- export function formatConnectionStatus(value) {
415
+ export function formatConnectionStatus(value, row, property, options) {
395
416
  if (value === null || value === undefined) {
396
417
  return ''
397
418
  }
398
- const label = Object.hasOwn(CONNECTION_STATUS_LABELS, value)
399
- ? CONNECTION_STATUS_LABELS[value]
400
- : null
419
+ const labels = options && options.short === true ? CONNECTION_STATUS_SHORT_LABELS : CONNECTION_STATUS_LABELS
420
+ const label = Object.hasOwn(labels, value) ? labels[value] : null
401
421
  return label ? t('nextcloud-vue', label) : String(value)
402
422
  }
403
423
 
@@ -0,0 +1,39 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+ import { compareVisibleWhen, readVisibleWhenPath } from './visibleWhen.js'
6
+
7
+ /**
8
+ * Compile a row-class declaration into the `(row) => string` CnDataTable
9
+ * takes. A function passes straight through. An array of rules
10
+ * `[{ when: { field, op?, value }, class }]` adds each rule's `class` to a row
11
+ * whose `field` (a dot-path) compares to `value` with `op` (`eq` by default;
12
+ * the shared `visibleWhen` operators, `@today` and the other tokens included),
13
+ * every matching class space-joined: an overdue row can be marked from the
14
+ * manifest (`{ when: { field: "dueAt", op: "lt", value: "@today" }, class:
15
+ * "cn-row--overdue" }`). Anything else, or an empty list, is null.
16
+ *
17
+ * @param {Function|Array<{when: {field: string, op?: string, value: unknown}, class: string}>|null} rowClass The declaration.
18
+ * @return {?(row: object) => string} The row-class function, or null.
19
+ */
20
+ export function compileRowClass(rowClass) {
21
+ if (typeof rowClass === 'function') {
22
+ return rowClass
23
+ }
24
+ if (!Array.isArray(rowClass) || rowClass.length === 0) {
25
+ return null
26
+ }
27
+ const rules = rowClass.filter((rule) => rule && typeof rule.class === 'string' && rule.class !== '')
28
+ return (row) => {
29
+ const classes = []
30
+ for (const rule of rules) {
31
+ const when = rule.when || {}
32
+ const actual = readVisibleWhenPath(row, when.field)
33
+ if (compareVisibleWhen(actual, when.op || 'eq', when.value)) {
34
+ classes.push(rule.class)
35
+ }
36
+ }
37
+ return classes.join(' ')
38
+ }
39
+ }