@conduction/nextcloud-vue 2.56.0-beta.35 → 2.56.0-beta.36

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 (37) hide show
  1. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  2. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
  3. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  4. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
  5. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  6. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  7. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
  8. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  9. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  10. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  11. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  12. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  13. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  15. package/dist/esm/index.js +1 -0
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/library-style.js +1 -1
  18. package/dist/esm/nextcloud-vue.css +120 -65
  19. package/dist/esm/utils/gridStack.js +40 -1
  20. package/dist/esm/utils/gridStack.js.map +1 -1
  21. package/dist/esm/utils/humaniseKey.js +41 -0
  22. package/dist/esm/utils/humaniseKey.js.map +1 -0
  23. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  24. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  25. package/dist/nextcloud-vue.cjs.js +546 -256
  26. package/dist/nextcloud-vue.cjs.js.map +2 -2
  27. package/dist/nextcloud-vue.css +120 -65
  28. package/package.json +1 -1
  29. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
  30. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
  31. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  32. package/src/css/kpi-card.css +13 -0
  33. package/src/css/look-board.css +19 -0
  34. package/src/index.js +2 -0
  35. package/src/utils/gridStack.js +39 -0
  36. package/src/utils/humaniseKey.js +38 -0
  37. package/src/utils/notificationRuleLabel.js +5 -23
@@ -339,16 +339,30 @@
339
339
  {{ stringifyCell(v) }}
340
340
  </span>
341
341
  </div>
342
- <!-- Single plain OBJECT → key: value definition list. -->
342
+ <!-- Single plain OBJECT → key: value definition list; a
343
+ nested plain object (e.g. a per-language map) gets
344
+ its own list one level down. -->
343
345
  <dl
344
346
  v-else-if="fieldValueKind(field) === 'object'"
345
347
  class="cn-object-data-widget__deflist">
346
348
  <template v-for="(pair, pi) in objectEntries(rawOf(field))" :key="pi">
347
349
  <dt>
348
- {{ pair[0] }}
350
+ {{ nestedLabel(field, [pair[0]]) }}
349
351
  </dt>
350
352
  <dd>
351
- {{ stringifyCell(pair[1]) }}
353
+ <dl
354
+ v-if="isPlainObject(pair[1])"
355
+ class="cn-object-data-widget__deflist cn-object-data-widget__deflist--nested">
356
+ <template v-for="(sub, si) in objectEntries(pair[1])" :key="si">
357
+ <dt>
358
+ {{ nestedLabel(field, [pair[0], sub[0]]) }}
359
+ </dt>
360
+ <dd>
361
+ <span class="cn-object-data-widget__deflist-text">{{ stringifyNested(sub[1]) }}</span>
362
+ </dd>
363
+ </template>
364
+ </dl>
365
+ <span v-else class="cn-object-data-widget__deflist-text">{{ stringifyNested(pair[1]) }}</span>
352
366
  </dd>
353
367
  </template>
354
368
  </dl>
@@ -413,6 +427,7 @@ import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
413
427
  import { normalizeLook } from '../../composables/useLook.js'
414
428
  import { useObjectStore } from '../../store/index.js'
415
429
  import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
430
+ import { humaniseKey } from '../../utils/humaniseKey.js'
416
431
  import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
417
432
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
418
433
  import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
@@ -423,6 +438,9 @@ import { CnIcon } from '../CnIcon/index.js'
423
438
  import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
424
439
  import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
425
440
 
441
+ // Widgets whose value the inline text fallback would corrupt.
442
+ const STRUCTURED_WIDGETS = ['json', 'code', 'file', 'sub-objects', 'child-records']
443
+
426
444
  /**
427
445
  * CnObjectDataWidget — Schema-driven editable data grid widget.
428
446
  *
@@ -1363,6 +1381,59 @@ export default {
1363
1381
  return (raw && typeof raw === 'object') ? Object.entries(raw) : []
1364
1382
  },
1365
1383
 
1384
+ /**
1385
+ * Whether a value is a plain object (not an array, not null).
1386
+ *
1387
+ * @param {unknown} v The value.
1388
+ * @return {boolean} True for a plain object.
1389
+ */
1390
+ isPlainObject(v) {
1391
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
1392
+ },
1393
+
1394
+ /**
1395
+ * Label for a key inside an object value: the sub-schema's `title`
1396
+ * (translated) when the schema describes it, else the key, made
1397
+ * readable when it is identifier-shaped (`shortDescription` → "Short
1398
+ * description"; `nl` stays `nl`).
1399
+ *
1400
+ * @param {object} field Resolved field definition.
1401
+ * @param {string[]} path Keys from the field's value down to this entry.
1402
+ * @return {string} The label.
1403
+ * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key
1404
+ */
1405
+ nestedLabel(field, path) {
1406
+ let prop = ((this.schema && this.schema.properties) || {})[field.key]
1407
+ for (const key of path) {
1408
+ if (!this.isPlainObject(prop)) {
1409
+ prop = null
1410
+ break
1411
+ }
1412
+ prop = (prop.properties && prop.properties[key])
1413
+ || (this.isPlainObject(prop.additionalProperties) ? prop.additionalProperties : null)
1414
+ }
1415
+ if (prop && typeof prop.title === 'string' && prop.title) {
1416
+ return this.cnTranslate(prop.title)
1417
+ }
1418
+ const key = String(path[path.length - 1])
1419
+ return /[A-Z_-]/.test(key) ? humaniseKey(key) : key
1420
+ },
1421
+
1422
+ /**
1423
+ * Stringify a value inside a definition list: a list of scalars reads
1424
+ * as a comma-separated line, anything else as `stringifyCell`.
1425
+ *
1426
+ * @param {unknown} v The value.
1427
+ * @return {string} The display string.
1428
+ * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key
1429
+ */
1430
+ stringifyNested(v) {
1431
+ if (Array.isArray(v) && v.length && v.every((item) => item === null || typeof item !== 'object')) {
1432
+ return v.map((item) => this.stringifyCell(item)).join(', ')
1433
+ }
1434
+ return this.stringifyCell(v)
1435
+ },
1436
+
1366
1437
  /**
1367
1438
  * Stringify a scalar cell value; a nested object/array collapses to
1368
1439
  * compact JSON (never "[object Object]").
@@ -1690,15 +1761,53 @@ export default {
1690
1761
  }))
1691
1762
  },
1692
1763
 
1764
+ /**
1765
+ * Whether a field's value cannot be edited inline: a structured widget
1766
+ * (json, code, file, sub-objects, child-records), or a plain-object /
1767
+ * list-of-objects value that is not a relation. Scalar widgets without
1768
+ * a dedicated editor (duration, language, …) keep the text fallback.
1769
+ *
1770
+ * @param {object} field Resolved field definition.
1771
+ * @return {boolean} True when only the form dialog can edit it.
1772
+ * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-structured-value-is-never-edited-inline
1773
+ */
1774
+ hasNoInlineEditor(field) {
1775
+ if (STRUCTURED_WIDGETS.includes(field.widget)) {
1776
+ return true
1777
+ }
1778
+ const prop = ((this.schema && this.schema.properties) || {})[field.key]
1779
+ if (this.isRelationField(prop)) {
1780
+ return false
1781
+ }
1782
+ // A list of objects by schema, also while empty (it resolves to `tags`).
1783
+ if (prop && prop.type === 'array' && prop.items && prop.items.type === 'object') {
1784
+ return true
1785
+ }
1786
+ // Classify the raw value itself: fieldValueKind reports a formatter
1787
+ // field as 'scalar' whatever it holds.
1788
+ const raw = this.rawOf(field)
1789
+ if (Array.isArray(raw)) {
1790
+ return raw.some((v) => this.isPlainObject(v))
1791
+ }
1792
+ return this.isPlainObject(raw)
1793
+ },
1794
+
1693
1795
  /**
1694
1796
  * Check if a field is editable.
1695
1797
  *
1696
1798
  * @param {object} field - Resolved field definition from resolvedFields
1799
+ * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-structured-value-is-never-edited-inline
1697
1800
  */
1698
1801
  isEditable(field) {
1699
1802
  if (!this.editable) {
1700
1803
  return false
1701
1804
  }
1805
+ // No inline editor exists for structured values: the text fallback
1806
+ // would commit "[object Object]". Those edit through the form dialog,
1807
+ // unless the consumer supplies its own `field-<key>` editor slot.
1808
+ if (!this.$slots['field-' + field.key] && this.hasNoInlineEditor(field)) {
1809
+ return false
1810
+ }
1702
1811
  // Per-field override takes priority
1703
1812
  const override = this.resolvedOverrides[field.key]
1704
1813
  if (override && typeof override.editable === 'boolean') {
@@ -2439,6 +2548,17 @@ export default {
2439
2548
  overflow-wrap: anywhere;
2440
2549
  }
2441
2550
 
2551
+ /* A long description keeps its own paragraphs. Set on an inline span with no
2552
+ surrounding template whitespace, so only the value's own line breaks show. */
2553
+ .cn-object-data-widget__deflist-text {
2554
+ white-space: pre-wrap;
2555
+ }
2556
+
2557
+ .cn-object-data-widget__deflist--nested {
2558
+ padding-inline-start: 8px;
2559
+ border-inline-start: 2px solid var(--color-border);
2560
+ }
2561
+
2442
2562
  /* Responsive: collapse to single column on narrow widths */
2443
2563
  @media (max-width: 600px) {
2444
2564
  .cn-object-data-widget__grid {
@@ -89,7 +89,8 @@ const DEFAULT_LIMIT = 100
89
89
  *
90
90
  * It renders the five working days by default (`days: 7` adds the weekend),
91
91
  * highlights today, marks items that are late, and says so when a day has
92
- * nothing. On a narrow screen the strip scrolls sideways.
92
+ * nothing. On a narrow screen the strip scrolls sideways; under the board
93
+ * look (`.cn-look-board`) the days wrap into rows instead.
93
94
  *
94
95
  * Items come from an OpenRegister source (one request for the whole week) or
95
96
  * from static `items`. Resolved by its registry type key `week-strip`.
@@ -444,6 +444,19 @@ a.cn-kpi-card--stacked:focus-visible {
444
444
  color: var(--color-main-text);
445
445
  }
446
446
 
447
+ /* A board tile whose label wraps grows into its cell instead of clipping its
448
+ own last line. dossiq's "Afgehandeld deze maand" at 1100 wide held 129px of
449
+ content in a 114px tile and lost its caption's bottom. The tile still fills
450
+ a cell it fits (`min-height: 100%`), so every tile that fits keeps its
451
+ size; one that does not takes the height of its content, and the card-fit
452
+ cell's `safe center` keeps its top in view.
453
+ @spec openspec/changes/board-stat-tile-grows/specs/stat-tile-board-look/spec.md#requirement-a-board-stat-tile-shows-all-its-lines */
454
+ .cn-kpi-card--board.cn-kpi-card--stacked {
455
+ flex: 0 0 auto;
456
+ height: auto;
457
+ min-height: 100%;
458
+ }
459
+
447
460
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-kpi-card__caption-line,
448
461
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-stats-block__count-label {
449
462
  font-size: 13px;
@@ -714,3 +714,22 @@
714
714
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__chevron {
715
715
  display: none;
716
716
  }
717
+
718
+ /* ---- Week strip (DqMijnWerk "Termijnen deze week") ---------------------- */
719
+ /* The days wrap inside the card instead of scrolling sideways: a row holds
720
+ as many days of at least 120px as the card fits, and the rest go to the
721
+ next row (dossiq at 1100 wide held 680px of days in a 494px card and
722
+ scrolled; now three days and two). 120px keeps a case title's words whole;
723
+ five 88px days fitted one row but broke "Bezwaar" mid-word. A card wide
724
+ enough for the week draws it as before, one row of equal columns. The
725
+ day's head wraps its "today" badge under the label when the day is
726
+ narrow.
727
+ @spec openspec/changes/board-week-strip-fits/specs/week-strip-board-look/spec.md#requirement-a-board-week-strip-fits-its-card */
728
+ .cn-look-board .cn-week-strip:not(.cn-look-nextcloud *) .cn-week-strip__days {
729
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 120px), 1fr));
730
+ min-width: 0;
731
+ }
732
+
733
+ .cn-look-board .cn-week-strip:not(.cn-look-nextcloud *) .cn-week-strip__head {
734
+ flex-wrap: wrap;
735
+ }
package/src/index.js CHANGED
@@ -542,6 +542,8 @@ export { currentTheme, useCurrentTheme } from './composables/useCurrentTheme.js'
542
542
  export { ROADMAP_LABEL_BLOCKLIST } from './utils/roadmapLabelBlocklist.js'
543
543
  // The words a person reads for a notification rule key (never the raw key).
544
544
  export { humaniseRuleKey, notificationRuleLabel } from './utils/notificationRuleLabel.js'
545
+ // An identifier-shaped key made readable (`shortDescription` → "Short description").
546
+ export { humaniseKey } from './utils/humaniseKey.js'
545
547
 
546
548
  // Shared field validators. These are UX affordances that run in the browser —
547
549
  // they do NOT replace server-side validation, which stays authoritative at the
@@ -53,3 +53,42 @@ export function readGridGeometry(items) {
53
53
  }
54
54
  return map
55
55
  }
56
+
57
+ /**
58
+ * Where the items under a growing cell go so they keep their order: every item
59
+ * the growth would overlap moves down by the growth, and so does every item
60
+ * that move would overlap in turn. Items beside the cell stay put.
61
+ *
62
+ * GridStack resolves the same collision itself, but not in order: when
63
+ * pipelinq's size-to-content banner grew from two rows to three, the
64
+ * full-width card under the stat tiles landed below the lists.
65
+ *
66
+ * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
67
+ * @param {Array<{x: number, y: number, w: number, h: number}>} nodes The grid's items (the growing one among them).
68
+ * @param {{x: number, y: number, w: number, h: number}} node The growing item.
69
+ * @param {number} h Its new height, in grid rows.
70
+ * @return {Array<{node: object, y: number}>} The items to move and their new row, bottom-most first.
71
+ */
72
+ export function planGrowth(nodes, node, h) {
73
+ const delta = h - node.h
74
+ if (!(delta > 0)) {
75
+ return []
76
+ }
77
+ const moved = new Map()
78
+ const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
79
+ const queue = [{ x: node.x, y: node.y, w: node.w, h }]
80
+ while (queue.length > 0) {
81
+ const rect = queue.shift()
82
+ for (const other of nodes) {
83
+ if (other === node || moved.has(other) || !overlaps(rect, other)) {
84
+ continue
85
+ }
86
+ const next = { x: other.x, y: other.y + delta, w: other.w, h: other.h }
87
+ moved.set(other, next.y)
88
+ queue.push(next)
89
+ }
90
+ }
91
+ return [...moved.entries()]
92
+ .map(([item, y]) => ({ node: item, y }))
93
+ .sort((a, b) => b.y - a.y)
94
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Make an identifier-shaped key readable for a person. Pure: no Vue.
6
+ */
7
+
8
+ /**
9
+ * Split camelCase, snake_case and kebab-case into words, keep acronyms, and
10
+ * capitalise only the first word.
11
+ *
12
+ * `caseAssigned` → "Case assigned", `object_created` → "Object created",
13
+ * `newBRPRecord` → "New BRP record".
14
+ *
15
+ * @param {string} key The key.
16
+ * @return {string} A readable sentence-case label, or the key when it has no words.
17
+ * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key
18
+ */
19
+ export function humaniseKey(key) {
20
+ const words = String(key || '')
21
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
22
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
23
+ .split(/[\s_\-.]+/)
24
+ .filter(Boolean)
25
+ if (words.length === 0) {
26
+ return String(key || '')
27
+ }
28
+ return words
29
+ .map((word, index) => {
30
+ const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)
31
+ if (isAcronym) {
32
+ return word
33
+ }
34
+ const lower = word.toLowerCase()
35
+ return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
36
+ })
37
+ .join(' ')
38
+ }
@@ -28,6 +28,8 @@
28
28
  * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
29
29
  */
30
30
 
31
+ import { humaniseKey } from './humaniseKey.js'
32
+
31
33
  /**
32
34
  * Pick the string for the current language out of a string or per-locale map.
33
35
  *
@@ -54,34 +56,14 @@ export function resolveLocalised(value, language = 'en') {
54
56
  }
55
57
 
56
58
  /**
57
- * Make a rule key readable: split camelCase, snake_case and kebab-case into
58
- * words, keep acronyms, and capitalise only the first word.
59
- *
60
- * `caseAssigned` → "Case assigned", `object_created` → "Object created",
61
- * `newBRPRecord` → "New BRP record".
59
+ * Make a rule key readable (`caseAssigned` → "Case assigned"); see `humaniseKey`.
62
60
  *
63
61
  * @param {string} key The rule key.
64
62
  * @return {string} A readable sentence-case label, or the key when it has no words.
63
+ * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md#requirement-a-notification-rule-reads-as-a-label
65
64
  */
66
65
  export function humaniseRuleKey(key) {
67
- const words = String(key || '')
68
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
69
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
70
- .split(/[\s_\-.]+/)
71
- .filter(Boolean)
72
- if (words.length === 0) {
73
- return String(key || '')
74
- }
75
- return words
76
- .map((word, index) => {
77
- const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)
78
- if (isAcronym) {
79
- return word
80
- }
81
- const lower = word.toLowerCase()
82
- return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
83
- })
84
- .join(' ')
66
+ return humaniseKey(key)
85
67
  }
86
68
 
87
69
  /**