@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.
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +120 -65
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +546 -256
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +120 -65
- package/package.json +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board.css +19 -0
- package/src/index.js +2 -0
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- 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
|
-
|
|
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`.
|
package/src/css/kpi-card.css
CHANGED
|
@@ -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;
|
package/src/css/look-board.css
CHANGED
|
@@ -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
|
package/src/utils/gridStack.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
/**
|