@conduction/nextcloud-vue 2.83.0 → 2.84.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.
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +5 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +16 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +38 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +448 -296
- package/dist/esm/utils/buildManifest.js +37 -3
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1706 -454
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +448 -296
- package/l10n/en.json +16 -1
- package/l10n/nl.json +38 -1
- package/package.json +1 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +12 -0
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/schemas/app-manifest-v2.schema.json +23 -2
- package/src/utils/buildManifest.js +36 -2
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -196,6 +196,7 @@ function applyMenuRelocations(menu, relocations) {
|
|
|
196
196
|
if (!relocations || typeof relocations !== 'object') {
|
|
197
197
|
return menu
|
|
198
198
|
}
|
|
199
|
+
const heldEntries = captionsHoldingEntries(menu);
|
|
199
200
|
for (let pass = 0; pass < 5; pass++) {
|
|
200
201
|
const moves = [];
|
|
201
202
|
for (let i = menu.length - 1; i >= 0; i--) {
|
|
@@ -241,9 +242,42 @@ function applyMenuRelocations(menu, relocations) {
|
|
|
241
242
|
}
|
|
242
243
|
});
|
|
243
244
|
}
|
|
244
|
-
// Drop empty group shells left behind by relocations.
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// Drop empty group shells left behind by relocations. A caption (a
|
|
246
|
+
// section heading) has no route and no children and is kept, unless the
|
|
247
|
+
// relocations emptied it: every entry under it moved away.
|
|
248
|
+
const kept = menu.filter((m) => m.type === 'caption' || m.route || m.href || m.action
|
|
249
|
+
|| (Array.isArray(m.children) && m.children.length > 0));
|
|
250
|
+
const holdingNow = captionsHoldingEntries(kept);
|
|
251
|
+
return kept.filter((m) => m.type !== 'caption' || !heldEntries.has(m) || holdingNow.has(m))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The captions (`type: "caption"`) that have at least one entry under them:
|
|
256
|
+
* a non-caption entry of the same section before the next caption.
|
|
257
|
+
*
|
|
258
|
+
* @param {Array<object>} menu The top-level menu.
|
|
259
|
+
* @return {Set<object>} The captions holding an entry.
|
|
260
|
+
*/
|
|
261
|
+
function captionsHoldingEntries(menu) {
|
|
262
|
+
const holding = new Set();
|
|
263
|
+
menu.forEach((entry, index) => {
|
|
264
|
+
if (entry.type !== 'caption') {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
const section = entry.section ?? 'main';
|
|
268
|
+
for (let i = index + 1; i < menu.length; i++) {
|
|
269
|
+
const next = menu[i];
|
|
270
|
+
if ((next.section ?? 'main') !== section) {
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (next.type === 'caption') {
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
holding.add(entry);
|
|
277
|
+
break
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
return holding
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildManifest.js","sources":["../../../src/utils/buildManifest.js"],"sourcesContent":["/**\n * Build an app's effective manifest from its bundled base, its modular\n * `manifest.d/*.json` fragments (ADR-037), and its `menu-layout.json`.\n *\n * Every manifest-v2 app ran an identical ~200-line copy of this pipeline\n * inline in its `src/main.js`. Lifting it here makes the navigation contract\n * a single shared implementation: fragment merging, canonical relocations,\n * duplicate-entry removals, promotion of config entries into Nextcloud's\n * settings foldout (`section: \"settings\"`), and relocation of cross-app links\n * out of the navigation into the per-user settings modal's Integrations\n * section (`section: \"integrations\"`, ADR-110) all behave the same across the\n * fleet, and a new app adopts the whole IA model by passing its three inputs.\n *\n * The only app-local piece is collecting the fragments: `require.context` is\n * resolved by each app's own webpack build, so the caller gathers the fragment\n * objects and passes them in:\n *\n * const ctx = require.context('./manifest.d/', false, /\\.json$/)\n * const fragments = ctx.keys().sort().map((k) => ctx(k))\n * const manifest = buildManifest(bundledManifest, fragments, menuLayout)\n *\n * If the base (or a fragment) declares `pageTemplates[]` + `pageInstances[]`\n * (manifest-entity-scaffold-templating), the entity-scaffold expander runs as\n * the final step: each instantiation is materialised into a concrete page and\n * appended to `pages[]`, so the runtime renderer only ever sees concrete pages.\n * A manifest without templating is passed through unchanged (no-op fast path).\n *\n * @param {object} base The bundled base manifest (`src/manifest.json`).\n * @param {Array<object>} [fragments] Fragment objects (each may carry `pages`/`menu`).\n * @param {object} [menuLayout] `{ relocations?, removals?, settingsSection?, integrationsSection? }`.\n * @return {object} The merged manifest: `{ ...base, pages, menu }`.\n */\n\nimport { expandPageTemplates } from './expandPageTemplates.js'\n\nexport function buildManifest(base, fragments = [], menuLayout = {}) {\n\tconst merged = { ...base, pages: [...(base.pages || [])], menu: [] }\n\tmergeMenuItems(merged.menu, base.menu || [])\n\t// Fragments may also carry page-template instantiations/templates/sets;\n\t// collect them so a fragment-authored entity scaffold expands too.\n\tconst fragTemplates = []\n\tconst fragInstances = []\n\tfor (const frag of (Array.isArray(fragments) ? fragments : [])) {\n\t\tif (frag && Array.isArray(frag.pages)) {\n\t\t\tmergePages(merged.pages, frag.pages)\n\t\t}\n\t\tif (frag && Array.isArray(frag.menu)) {\n\t\t\tmergeMenuItems(merged.menu, frag.menu)\n\t\t}\n\t\tif (frag && Array.isArray(frag.pageTemplates)) {\n\t\t\tfragTemplates.push(...frag.pageTemplates)\n\t\t}\n\t\tif (frag && Array.isArray(frag.pageInstances)) {\n\t\t\tfragInstances.push(...frag.pageInstances)\n\t\t}\n\t\tif (frag && frag.sets && typeof frag.sets === 'object') {\n\t\t\tmerged.sets = { ...(merged.sets || {}), ...frag.sets }\n\t\t}\n\t}\n\tif (fragTemplates.length) {\n\t\tmerged.pageTemplates = [...(merged.pageTemplates || []), ...fragTemplates]\n\t}\n\tif (fragInstances.length) {\n\t\tmerged.pageInstances = [...(merged.pageInstances || []), ...fragInstances]\n\t}\n\tmerged.menu = applyMenuLayout(merged.menu, menuLayout)\n\n\t// Entity-scaffold expansion (runtime/boot path). No-op unless the manifest\n\t// declares pageTemplates + pageInstances. Runtime fallback semantics: a bad\n\t// instantiation is skipped + warned rather than blanking the whole app.\n\tif (Array.isArray(merged.pageTemplates) || Array.isArray(merged.pageInstances)) {\n\t\tconst result = expandPageTemplates(merged, { throwOnError: false })\n\t\tif (result.errors.length) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn('[buildManifest] page-template expansion reported errors; the offending instantiations were skipped.', result.errors)\n\t\t}\n\t\treturn result.manifest\n\t}\n\treturn merged\n}\n\n/**\n * Apply the canonical navigation layout (`relocations` → `removals` →\n * `settingsSection` → `integrationsSection`) to an already-merged menu.\n * Exposed separately for apps that assemble their menu through a different\n * path but still want the shared layout semantics.\n *\n * `integrationsSection` runs LAST so it wins over `settingsSection` if an id\n * is listed in both: an integration link is not a settings page of this app,\n * and the entry must end up in exactly one place. Running it last also means\n * an id lifted into the foldout by a stale `settingsSection` entry is still\n * reachable to be re-lifted, rather than silently staying in the nav.\n *\n * @param {Array<object>} menu The merged menu (mutated in place by the steps).\n * @param {object} [menuLayout] `{ relocations?, removals?, settingsSection?, integrationsSection? }`.\n * @return {Array<object>} The laid-out menu.\n */\nexport function applyMenuLayout(menu, menuLayout = {}) {\n\tlet out = applyMenuRelocations(menu, menuLayout.relocations)\n\tout = applyMenuRemovals(out, menuLayout.removals)\n\tout = applySettingsSection(out, menuLayout.settingsSection)\n\tout = applyIntegrationsSection(out, menuLayout.integrationsSection)\n\treturn out\n}\n\n/**\n * Merge an array of incoming menu items into a target array, keyed by `id`.\n * New ids are appended; existing ids are merged in place: the first\n * definition of each listed key wins (the base manifest loads first, so its\n * canonical group definitions take precedence), and `children` are unioned\n * recursively by the same rule. Fragments may therefore extend an existing\n * group by re-declaring only its `id` plus their own `children`.\n *\n * @param {Array<object>} target The accumulated menu (mutated in place).\n * @param {Array<object>} incoming Menu items from a fragment.\n * @return {void}\n */\nexport function mergeMenuItems(target, incoming) {\n\tincoming.forEach((item) => {\n\t\tconst existing = target.find((t) => t.id === item.id)\n\t\tif (!existing) {\n\t\t\t// Copy `children` ONLY when there is an array to copy. The previous\n\t\t\t// ternary's else-branch assigned `item.children` — i.e. `undefined`\n\t\t\t// — which is not the same as leaving the key off: `Object.keys()`\n\t\t\t// and JSON-Schema's `additionalProperties: false` both see a key\n\t\t\t// that is present. Every leaf entry therefore carried a `children`\n\t\t\t// property, and the BUILT manifest failed validation against the\n\t\t\t// same schema its source passes. Invisible in practice because\n\t\t\t// `JSON.stringify` drops undefined values, so nothing that logged\n\t\t\t// or persisted the manifest ever showed it.\n\t\t\tconst copy = { ...item }\n\t\t\tif (Array.isArray(item.children)) {\n\t\t\t\tcopy.children = [...item.children]\n\t\t\t} else {\n\t\t\t\tdelete copy.children\n\t\t\t}\n\t\t\ttarget.push(copy)\n\t\t\treturn\n\t\t}\n\t\tfor (const key of ['label', 'icon', 'route', 'order', 'section', 'featureFlag', 'permission', 'visibleIf', 'href', 'action']) {\n\t\t\tif (existing[key] === undefined && item[key] !== undefined) {\n\t\t\t\texisting[key] = item[key]\n\t\t\t}\n\t\t}\n\t\tif (Array.isArray(item.children) && item.children.length > 0) {\n\t\t\tif (!Array.isArray(existing.children)) {\n\t\t\t\texisting.children = []\n\t\t\t}\n\t\t\tmergeMenuItems(existing.children, item.children)\n\t\t}\n\t})\n}\n\n/**\n * Merge fragment pages onto the accumulated page list by `id` — a later\n * declaration REPLACES an earlier one wholesale, so a fragment can overlay a\n * base page (e.g. swap a schema-driven detail for a bespoke `type: \"custom\"`\n * component) and guarantee one route per page id.\n *\n * @param {Array<object>} target Accumulated pages (mutated in place).\n * @param {Array<object>} incoming Pages from a fragment.\n * @return {void}\n */\nexport function mergePages(target, incoming) {\n\tincoming.forEach((page) => {\n\t\tconst idx = target.findIndex((p) => p.id === page.id)\n\t\tif (idx === -1) {\n\t\t\ttarget.push(page)\n\t\t} else {\n\t\t\ttarget[idx] = page\n\t\t}\n\t})\n}\n\n/**\n * Re-home merged menu entries onto the canonical navigation layout declared\n * by `menu-layout.json#relocations` (`{ sourceId: targetGroupId }`).\n *\n * - A relocated GROUP dissolves: its children merge (by id) into the target\n * group and the now-empty shell is dropped.\n * - A relocated LEAF (top-level or child of any group) moves under the\n * target group.\n * - A child group relocated onto its own parent flattens into it.\n * - Unknown source ids are inert; a missing target keeps the entry at the\n * top level so nothing silently disappears.\n *\n * Runs in passes until stable (children freed by a dissolved group can\n * themselves be relocated on the next pass).\n *\n * @param {Array<object>} menu The merged menu (mutated in place).\n * @param {Record<string, string>|undefined} relocations Source-id → target-group-id map.\n * @return {Array<object>} The menu with relocations applied.\n */\nexport function applyMenuRelocations(menu, relocations) {\n\tif (!relocations || typeof relocations !== 'object') {\n\t\treturn menu\n\t}\n\tfor (let pass = 0; pass < 5; pass++) {\n\t\tconst moves = []\n\t\tfor (let i = menu.length - 1; i >= 0; i--) {\n\t\t\tconst node = menu[i]\n\t\t\tconst target = relocations[node.id]\n\t\t\tif (target && target !== node.id) {\n\t\t\t\tmenu.splice(i, 1)\n\t\t\t\tmoves.push({ node, target })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (!Array.isArray(node.children)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor (let j = node.children.length - 1; j >= 0; j--) {\n\t\t\t\tconst child = node.children[j]\n\t\t\t\tconst childTarget = relocations[child.id]\n\t\t\t\tif (!childTarget) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (childTarget === node.id && !Array.isArray(child.children)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnode.children.splice(j, 1)\n\t\t\t\tmoves.push({ node: child, target: childTarget })\n\t\t\t}\n\t\t}\n\t\tif (moves.length === 0) {\n\t\t\tbreak\n\t\t}\n\t\tmoves.forEach(({ node, target }) => {\n\t\t\tconst group = menu.find((m) => m.id === target)\n\t\t\tif (!group) {\n\t\t\t\tmenu.push(node)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!Array.isArray(group.children)) {\n\t\t\t\tgroup.children = []\n\t\t\t}\n\t\t\tif (Array.isArray(node.children)) {\n\t\t\t\tmergeMenuItems(group.children, node.children)\n\t\t\t} else {\n\t\t\t\tmergeMenuItems(group.children, [node])\n\t\t\t}\n\t\t})\n\t}\n\t// Drop empty group shells left behind by relocations.\n\treturn menu.filter((m) => m.route || m.href || m.action\n\t\t|| (Array.isArray(m.children) && m.children.length > 0))\n}\n\n/**\n * Remove individual menu entries by id after relocation — used to retire\n * duplicate navigation entries whose PAGE must stay routable (deep links and\n * e2e specs hit the route directly). Declared in `menu-layout.json#removals`.\n * Only leaf entries are removed; a group is dropped only when it is explicitly\n * removed OR left empty after its children were removed/relocated away. A\n * group carrying a `route`/`href`/`action` is itself clickable (a direct-link\n * top-level item, e.g. a \"Reporting & Compliance\" → cards-page link) and\n * survives even when all its children are gone.\n *\n * @param {Array<object>} menu The merged menu.\n * @param {Array<string>|undefined} removals Menu-entry ids to drop.\n * @return {Array<object>} The menu without the removed entries.\n */\nexport function applyMenuRemovals(menu, removals) {\n\tif (!Array.isArray(removals) || removals.length === 0) {\n\t\treturn menu\n\t}\n\tconst drop = new Set(removals)\n\tconst wasGroup = (n) => Array.isArray(n.children) && n.children.length > 0\n\tconst isClickable = (n) => n.route !== undefined || n.href !== undefined || n.action !== undefined\n\tconst prune = (nodes) => nodes.reduce((acc, n) => {\n\t\tif (drop.has(n.id) && !wasGroup(n)) {\n\t\t\treturn acc\n\t\t}\n\t\tif (Array.isArray(n.children)) {\n\t\t\tconst children = prune(n.children)\n\t\t\tconst hadChildren = wasGroup(n)\n\t\t\tif (children.length === 0 && hadChildren && !isClickable(n)) {\n\t\t\t\treturn acc\n\t\t\t}\n\t\t\tacc.push({ ...n, children })\n\t\t\treturn acc\n\t\t}\n\t\tacc.push(n)\n\t\treturn acc\n\t}, [])\n\treturn prune(menu)\n}\n\n/**\n * Lift the menu entries listed by id out of wherever they currently sit, tag\n * them with `section`, flatten them (neither destination renders nested\n * groups), and append them at the top level. Empty non-clickable groups left\n * behind are dropped; a clickable group is kept.\n *\n * Shared by `applySettingsSection` and `applyIntegrationsSection` so the two\n * destinations cannot drift in their lift semantics — the only difference\n * between them is the section tag, and therefore which surface renders them.\n *\n * @param {Array<object>} menu The merged + relocated + pruned menu.\n * @param {Array<string>|undefined} ids Entry ids to lift.\n * @param {string} section The `section` tag to stamp on each lifted entry.\n * @return {Array<object>} The menu with the listed entries lifted out.\n */\nfunction liftToSection(menu, ids, section) {\n\tif (!Array.isArray(ids) || ids.length === 0) {\n\t\treturn menu\n\t}\n\tconst want = new Set(ids)\n\tconst isClickable = (n) => n.route !== undefined || n.href !== undefined || n.action !== undefined\n\tconst lifted = []\n\tconst strip = (nodes) => nodes.reduce((acc, n) => {\n\t\tif (want.has(n.id)) {\n\t\t\tconst { children, ...leaf } = n\n\t\t\tlifted.push({ ...leaf, section })\n\t\t\treturn acc\n\t\t}\n\t\tif (Array.isArray(n.children)) {\n\t\t\tconst children = strip(n.children)\n\t\t\tif (children.length === 0 && n.children.length > 0 && !isClickable(n)) {\n\t\t\t\treturn acc\n\t\t\t}\n\t\t\tacc.push({ ...n, children })\n\t\t\treturn acc\n\t\t}\n\t\tacc.push(n)\n\t\treturn acc\n\t}, [])\n\tconst remaining = strip(menu)\n\treturn [...remaining, ...lifted]\n}\n\n/**\n * Promote the menu entries listed in `menu-layout.json#settingsSection` into\n * Nextcloud's settings foldout — the `NcAppNavigationSettings` gear at the\n * bottom-left of the navigation, OUTSIDE the scrollable list. CnAppNav renders\n * every TOP-LEVEL item carrying `section: \"settings\"` as a flat entry inside\n * that foldout (with an auto-prepended \"Personal settings\").\n *\n * @param {Array<object>} menu The merged + relocated + pruned menu.\n * @param {Array<string>|undefined} settingsIds Entry ids to move to the foldout.\n * @return {Array<object>} The menu with the settings entries lifted out.\n */\nexport function applySettingsSection(menu, settingsIds) {\n\treturn liftToSection(menu, settingsIds, 'settings')\n}\n\n/**\n * Move the menu entries listed in `menu-layout.json#integrationsSection` OUT of\n * the navigation entirely and into the Integrations section at the bottom of\n * the per-user settings modal (ADR-110).\n *\n * This is where a link that LEAVES this app for another one belongs. Such a\n * link is not a page of this app — it cannot be active, it has no counter, and\n * putting it in the nav makes another app's capability read as this app's\n * feature. The one deep link that stays in the navigation is Admin settings,\n * and CnAppNav auto-prepends that itself (ADR-079); no app declares it.\n *\n * The entry survives — it is relocated, never dropped — which is what keeps\n * this compatible with the ADR-044 Decision 5 no-functionality-loss invariant.\n *\n * @param {Array<object>} menu The merged + relocated + pruned menu.\n * @param {Array<string>|undefined} integrationIds Entry ids to move to Integrations.\n * @return {Array<object>} The menu with the integration entries lifted out.\n */\nexport function applyIntegrationsSection(menu, integrationIds) {\n\treturn liftToSection(menu, integrationIds, 'integrations')\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"buildManifest.js","sources":["../../../src/utils/buildManifest.js"],"sourcesContent":["/**\n * Build an app's effective manifest from its bundled base, its modular\n * `manifest.d/*.json` fragments (ADR-037), and its `menu-layout.json`.\n *\n * Every manifest-v2 app ran an identical ~200-line copy of this pipeline\n * inline in its `src/main.js`. Lifting it here makes the navigation contract\n * a single shared implementation: fragment merging, canonical relocations,\n * duplicate-entry removals, promotion of config entries into Nextcloud's\n * settings foldout (`section: \"settings\"`), and relocation of cross-app links\n * out of the navigation into the per-user settings modal's Integrations\n * section (`section: \"integrations\"`, ADR-110) all behave the same across the\n * fleet, and a new app adopts the whole IA model by passing its three inputs.\n *\n * The only app-local piece is collecting the fragments: `require.context` is\n * resolved by each app's own webpack build, so the caller gathers the fragment\n * objects and passes them in:\n *\n * const ctx = require.context('./manifest.d/', false, /\\.json$/)\n * const fragments = ctx.keys().sort().map((k) => ctx(k))\n * const manifest = buildManifest(bundledManifest, fragments, menuLayout)\n *\n * If the base (or a fragment) declares `pageTemplates[]` + `pageInstances[]`\n * (manifest-entity-scaffold-templating), the entity-scaffold expander runs as\n * the final step: each instantiation is materialised into a concrete page and\n * appended to `pages[]`, so the runtime renderer only ever sees concrete pages.\n * A manifest without templating is passed through unchanged (no-op fast path).\n *\n * @param {object} base The bundled base manifest (`src/manifest.json`).\n * @param {Array<object>} [fragments] Fragment objects (each may carry `pages`/`menu`).\n * @param {object} [menuLayout] `{ relocations?, removals?, settingsSection?, integrationsSection? }`.\n * @return {object} The merged manifest: `{ ...base, pages, menu }`.\n */\n\nimport { expandPageTemplates } from './expandPageTemplates.js'\n\nexport function buildManifest(base, fragments = [], menuLayout = {}) {\n\tconst merged = { ...base, pages: [...(base.pages || [])], menu: [] }\n\tmergeMenuItems(merged.menu, base.menu || [])\n\t// Fragments may also carry page-template instantiations/templates/sets;\n\t// collect them so a fragment-authored entity scaffold expands too.\n\tconst fragTemplates = []\n\tconst fragInstances = []\n\tfor (const frag of (Array.isArray(fragments) ? fragments : [])) {\n\t\tif (frag && Array.isArray(frag.pages)) {\n\t\t\tmergePages(merged.pages, frag.pages)\n\t\t}\n\t\tif (frag && Array.isArray(frag.menu)) {\n\t\t\tmergeMenuItems(merged.menu, frag.menu)\n\t\t}\n\t\tif (frag && Array.isArray(frag.pageTemplates)) {\n\t\t\tfragTemplates.push(...frag.pageTemplates)\n\t\t}\n\t\tif (frag && Array.isArray(frag.pageInstances)) {\n\t\t\tfragInstances.push(...frag.pageInstances)\n\t\t}\n\t\tif (frag && frag.sets && typeof frag.sets === 'object') {\n\t\t\tmerged.sets = { ...(merged.sets || {}), ...frag.sets }\n\t\t}\n\t}\n\tif (fragTemplates.length) {\n\t\tmerged.pageTemplates = [...(merged.pageTemplates || []), ...fragTemplates]\n\t}\n\tif (fragInstances.length) {\n\t\tmerged.pageInstances = [...(merged.pageInstances || []), ...fragInstances]\n\t}\n\tmerged.menu = applyMenuLayout(merged.menu, menuLayout)\n\n\t// Entity-scaffold expansion (runtime/boot path). No-op unless the manifest\n\t// declares pageTemplates + pageInstances. Runtime fallback semantics: a bad\n\t// instantiation is skipped + warned rather than blanking the whole app.\n\tif (Array.isArray(merged.pageTemplates) || Array.isArray(merged.pageInstances)) {\n\t\tconst result = expandPageTemplates(merged, { throwOnError: false })\n\t\tif (result.errors.length) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn('[buildManifest] page-template expansion reported errors; the offending instantiations were skipped.', result.errors)\n\t\t}\n\t\treturn result.manifest\n\t}\n\treturn merged\n}\n\n/**\n * Apply the canonical navigation layout (`relocations` → `removals` →\n * `settingsSection` → `integrationsSection`) to an already-merged menu.\n * Exposed separately for apps that assemble their menu through a different\n * path but still want the shared layout semantics.\n *\n * `integrationsSection` runs LAST so it wins over `settingsSection` if an id\n * is listed in both: an integration link is not a settings page of this app,\n * and the entry must end up in exactly one place. Running it last also means\n * an id lifted into the foldout by a stale `settingsSection` entry is still\n * reachable to be re-lifted, rather than silently staying in the nav.\n *\n * @param {Array<object>} menu The merged menu (mutated in place by the steps).\n * @param {object} [menuLayout] `{ relocations?, removals?, settingsSection?, integrationsSection? }`.\n * @return {Array<object>} The laid-out menu.\n */\nexport function applyMenuLayout(menu, menuLayout = {}) {\n\tlet out = applyMenuRelocations(menu, menuLayout.relocations)\n\tout = applyMenuRemovals(out, menuLayout.removals)\n\tout = applySettingsSection(out, menuLayout.settingsSection)\n\tout = applyIntegrationsSection(out, menuLayout.integrationsSection)\n\treturn out\n}\n\n/**\n * Merge an array of incoming menu items into a target array, keyed by `id`.\n * New ids are appended; existing ids are merged in place: the first\n * definition of each listed key wins (the base manifest loads first, so its\n * canonical group definitions take precedence), and `children` are unioned\n * recursively by the same rule. Fragments may therefore extend an existing\n * group by re-declaring only its `id` plus their own `children`.\n *\n * @param {Array<object>} target The accumulated menu (mutated in place).\n * @param {Array<object>} incoming Menu items from a fragment.\n * @return {void}\n */\nexport function mergeMenuItems(target, incoming) {\n\tincoming.forEach((item) => {\n\t\tconst existing = target.find((t) => t.id === item.id)\n\t\tif (!existing) {\n\t\t\t// Copy `children` ONLY when there is an array to copy. The previous\n\t\t\t// ternary's else-branch assigned `item.children` — i.e. `undefined`\n\t\t\t// — which is not the same as leaving the key off: `Object.keys()`\n\t\t\t// and JSON-Schema's `additionalProperties: false` both see a key\n\t\t\t// that is present. Every leaf entry therefore carried a `children`\n\t\t\t// property, and the BUILT manifest failed validation against the\n\t\t\t// same schema its source passes. Invisible in practice because\n\t\t\t// `JSON.stringify` drops undefined values, so nothing that logged\n\t\t\t// or persisted the manifest ever showed it.\n\t\t\tconst copy = { ...item }\n\t\t\tif (Array.isArray(item.children)) {\n\t\t\t\tcopy.children = [...item.children]\n\t\t\t} else {\n\t\t\t\tdelete copy.children\n\t\t\t}\n\t\t\ttarget.push(copy)\n\t\t\treturn\n\t\t}\n\t\tfor (const key of ['label', 'icon', 'route', 'order', 'section', 'featureFlag', 'permission', 'visibleIf', 'href', 'action']) {\n\t\t\tif (existing[key] === undefined && item[key] !== undefined) {\n\t\t\t\texisting[key] = item[key]\n\t\t\t}\n\t\t}\n\t\tif (Array.isArray(item.children) && item.children.length > 0) {\n\t\t\tif (!Array.isArray(existing.children)) {\n\t\t\t\texisting.children = []\n\t\t\t}\n\t\t\tmergeMenuItems(existing.children, item.children)\n\t\t}\n\t})\n}\n\n/**\n * Merge fragment pages onto the accumulated page list by `id` — a later\n * declaration REPLACES an earlier one wholesale, so a fragment can overlay a\n * base page (e.g. swap a schema-driven detail for a bespoke `type: \"custom\"`\n * component) and guarantee one route per page id.\n *\n * @param {Array<object>} target Accumulated pages (mutated in place).\n * @param {Array<object>} incoming Pages from a fragment.\n * @return {void}\n */\nexport function mergePages(target, incoming) {\n\tincoming.forEach((page) => {\n\t\tconst idx = target.findIndex((p) => p.id === page.id)\n\t\tif (idx === -1) {\n\t\t\ttarget.push(page)\n\t\t} else {\n\t\t\ttarget[idx] = page\n\t\t}\n\t})\n}\n\n/**\n * Re-home merged menu entries onto the canonical navigation layout declared\n * by `menu-layout.json#relocations` (`{ sourceId: targetGroupId }`).\n *\n * - A relocated GROUP dissolves: its children merge (by id) into the target\n * group and the now-empty shell is dropped.\n * - A relocated LEAF (top-level or child of any group) moves under the\n * target group.\n * - A child group relocated onto its own parent flattens into it.\n * - Unknown source ids are inert; a missing target keeps the entry at the\n * top level so nothing silently disappears.\n *\n * Runs in passes until stable (children freed by a dissolved group can\n * themselves be relocated on the next pass).\n *\n * @param {Array<object>} menu The merged menu (mutated in place).\n * @param {Record<string, string>|undefined} relocations Source-id → target-group-id map.\n * @return {Array<object>} The menu with relocations applied.\n */\nexport function applyMenuRelocations(menu, relocations) {\n\tif (!relocations || typeof relocations !== 'object') {\n\t\treturn menu\n\t}\n\tconst heldEntries = captionsHoldingEntries(menu)\n\tfor (let pass = 0; pass < 5; pass++) {\n\t\tconst moves = []\n\t\tfor (let i = menu.length - 1; i >= 0; i--) {\n\t\t\tconst node = menu[i]\n\t\t\tconst target = relocations[node.id]\n\t\t\tif (target && target !== node.id) {\n\t\t\t\tmenu.splice(i, 1)\n\t\t\t\tmoves.push({ node, target })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (!Array.isArray(node.children)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor (let j = node.children.length - 1; j >= 0; j--) {\n\t\t\t\tconst child = node.children[j]\n\t\t\t\tconst childTarget = relocations[child.id]\n\t\t\t\tif (!childTarget) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (childTarget === node.id && !Array.isArray(child.children)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnode.children.splice(j, 1)\n\t\t\t\tmoves.push({ node: child, target: childTarget })\n\t\t\t}\n\t\t}\n\t\tif (moves.length === 0) {\n\t\t\tbreak\n\t\t}\n\t\tmoves.forEach(({ node, target }) => {\n\t\t\tconst group = menu.find((m) => m.id === target)\n\t\t\tif (!group) {\n\t\t\t\tmenu.push(node)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!Array.isArray(group.children)) {\n\t\t\t\tgroup.children = []\n\t\t\t}\n\t\t\tif (Array.isArray(node.children)) {\n\t\t\t\tmergeMenuItems(group.children, node.children)\n\t\t\t} else {\n\t\t\t\tmergeMenuItems(group.children, [node])\n\t\t\t}\n\t\t})\n\t}\n\t// Drop empty group shells left behind by relocations. A caption (a\n\t// section heading) has no route and no children and is kept, unless the\n\t// relocations emptied it: every entry under it moved away.\n\tconst kept = menu.filter((m) => m.type === 'caption' || m.route || m.href || m.action\n\t\t|| (Array.isArray(m.children) && m.children.length > 0))\n\tconst holdingNow = captionsHoldingEntries(kept)\n\treturn kept.filter((m) => m.type !== 'caption' || !heldEntries.has(m) || holdingNow.has(m))\n}\n\n/**\n * The captions (`type: \"caption\"`) that have at least one entry under them:\n * a non-caption entry of the same section before the next caption.\n *\n * @param {Array<object>} menu The top-level menu.\n * @return {Set<object>} The captions holding an entry.\n */\nfunction captionsHoldingEntries(menu) {\n\tconst holding = new Set()\n\tmenu.forEach((entry, index) => {\n\t\tif (entry.type !== 'caption') {\n\t\t\treturn\n\t\t}\n\t\tconst section = entry.section ?? 'main'\n\t\tfor (let i = index + 1; i < menu.length; i++) {\n\t\t\tconst next = menu[i]\n\t\t\tif ((next.section ?? 'main') !== section) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (next.type === 'caption') {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tholding.add(entry)\n\t\t\tbreak\n\t\t}\n\t})\n\treturn holding\n}\n\n/**\n * Remove individual menu entries by id after relocation — used to retire\n * duplicate navigation entries whose PAGE must stay routable (deep links and\n * e2e specs hit the route directly). Declared in `menu-layout.json#removals`.\n * Only leaf entries are removed; a group is dropped only when it is explicitly\n * removed OR left empty after its children were removed/relocated away. A\n * group carrying a `route`/`href`/`action` is itself clickable (a direct-link\n * top-level item, e.g. a \"Reporting & Compliance\" → cards-page link) and\n * survives even when all its children are gone.\n *\n * @param {Array<object>} menu The merged menu.\n * @param {Array<string>|undefined} removals Menu-entry ids to drop.\n * @return {Array<object>} The menu without the removed entries.\n */\nexport function applyMenuRemovals(menu, removals) {\n\tif (!Array.isArray(removals) || removals.length === 0) {\n\t\treturn menu\n\t}\n\tconst drop = new Set(removals)\n\tconst wasGroup = (n) => Array.isArray(n.children) && n.children.length > 0\n\tconst isClickable = (n) => n.route !== undefined || n.href !== undefined || n.action !== undefined\n\tconst prune = (nodes) => nodes.reduce((acc, n) => {\n\t\tif (drop.has(n.id) && !wasGroup(n)) {\n\t\t\treturn acc\n\t\t}\n\t\tif (Array.isArray(n.children)) {\n\t\t\tconst children = prune(n.children)\n\t\t\tconst hadChildren = wasGroup(n)\n\t\t\tif (children.length === 0 && hadChildren && !isClickable(n)) {\n\t\t\t\treturn acc\n\t\t\t}\n\t\t\tacc.push({ ...n, children })\n\t\t\treturn acc\n\t\t}\n\t\tacc.push(n)\n\t\treturn acc\n\t}, [])\n\treturn prune(menu)\n}\n\n/**\n * Lift the menu entries listed by id out of wherever they currently sit, tag\n * them with `section`, flatten them (neither destination renders nested\n * groups), and append them at the top level. Empty non-clickable groups left\n * behind are dropped; a clickable group is kept.\n *\n * Shared by `applySettingsSection` and `applyIntegrationsSection` so the two\n * destinations cannot drift in their lift semantics — the only difference\n * between them is the section tag, and therefore which surface renders them.\n *\n * @param {Array<object>} menu The merged + relocated + pruned menu.\n * @param {Array<string>|undefined} ids Entry ids to lift.\n * @param {string} section The `section` tag to stamp on each lifted entry.\n * @return {Array<object>} The menu with the listed entries lifted out.\n */\nfunction liftToSection(menu, ids, section) {\n\tif (!Array.isArray(ids) || ids.length === 0) {\n\t\treturn menu\n\t}\n\tconst want = new Set(ids)\n\tconst isClickable = (n) => n.route !== undefined || n.href !== undefined || n.action !== undefined\n\tconst lifted = []\n\tconst strip = (nodes) => nodes.reduce((acc, n) => {\n\t\tif (want.has(n.id)) {\n\t\t\tconst { children, ...leaf } = n\n\t\t\tlifted.push({ ...leaf, section })\n\t\t\treturn acc\n\t\t}\n\t\tif (Array.isArray(n.children)) {\n\t\t\tconst children = strip(n.children)\n\t\t\tif (children.length === 0 && n.children.length > 0 && !isClickable(n)) {\n\t\t\t\treturn acc\n\t\t\t}\n\t\t\tacc.push({ ...n, children })\n\t\t\treturn acc\n\t\t}\n\t\tacc.push(n)\n\t\treturn acc\n\t}, [])\n\tconst remaining = strip(menu)\n\treturn [...remaining, ...lifted]\n}\n\n/**\n * Promote the menu entries listed in `menu-layout.json#settingsSection` into\n * Nextcloud's settings foldout — the `NcAppNavigationSettings` gear at the\n * bottom-left of the navigation, OUTSIDE the scrollable list. CnAppNav renders\n * every TOP-LEVEL item carrying `section: \"settings\"` as a flat entry inside\n * that foldout (with an auto-prepended \"Personal settings\").\n *\n * @param {Array<object>} menu The merged + relocated + pruned menu.\n * @param {Array<string>|undefined} settingsIds Entry ids to move to the foldout.\n * @return {Array<object>} The menu with the settings entries lifted out.\n */\nexport function applySettingsSection(menu, settingsIds) {\n\treturn liftToSection(menu, settingsIds, 'settings')\n}\n\n/**\n * Move the menu entries listed in `menu-layout.json#integrationsSection` OUT of\n * the navigation entirely and into the Integrations section at the bottom of\n * the per-user settings modal (ADR-110).\n *\n * This is where a link that LEAVES this app for another one belongs. Such a\n * link is not a page of this app — it cannot be active, it has no counter, and\n * putting it in the nav makes another app's capability read as this app's\n * feature. The one deep link that stays in the navigation is Admin settings,\n * and CnAppNav auto-prepends that itself (ADR-079); no app declares it.\n *\n * The entry survives — it is relocated, never dropped — which is what keeps\n * this compatible with the ADR-044 Decision 5 no-functionality-loss invariant.\n *\n * @param {Array<object>} menu The merged + relocated + pruned menu.\n * @param {Array<string>|undefined} integrationIds Entry ids to move to Integrations.\n * @return {Array<object>} The menu with the integration entries lifted out.\n */\nexport function applyIntegrationsSection(menu, integrationIds) {\n\treturn liftToSection(menu, integrationIds, 'integrations')\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { cnFetchJson } from './cnFetch.js';
|
|
2
|
+
import { isWorkspaceCondition, isLocallyDecidableVisibleWhen, evaluateVisibleWhenLocal } from './visibleWhen.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* detailWidgetVisibility: whether a widget on a detail page shows for the
|
|
9
|
+
* loaded record, through the shared `visibleWhen` grammar.
|
|
10
|
+
*
|
|
11
|
+
* A detail page can name records it reads beside the loaded one
|
|
12
|
+
* (`visibilityContext`, e.g. a case's case type). A condition reads such a
|
|
13
|
+
* record as `@context.<name>.<path>`, so a widget can be shown for some case types
|
|
14
|
+
* only while the configuration stays in the app manifest.
|
|
15
|
+
*
|
|
16
|
+
* @module utils/detailWidgetVisibility
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read a reference value as an id: a plain string or number, or an object
|
|
22
|
+
* carrying `id`, `uuid` or `@self.id` (an extended reference).
|
|
23
|
+
*
|
|
24
|
+
* @param {unknown} value The value of the reference field.
|
|
25
|
+
* @return {string} The id, or '' when there is none.
|
|
26
|
+
*/
|
|
27
|
+
function referenceId(value) {
|
|
28
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
29
|
+
return String(value)
|
|
30
|
+
}
|
|
31
|
+
if (value && typeof value === 'object') {
|
|
32
|
+
const id = value.id ?? value.uuid ?? (value['@self'] && value['@self'].id);
|
|
33
|
+
return id === undefined || id === null ? '' : String(id)
|
|
34
|
+
}
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The records a `visibilityContext` names for one loaded record.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} context `{ <name>: { field, register, schema } }`.
|
|
42
|
+
* @param {object|null} record The loaded record.
|
|
43
|
+
* @return {Array<{key: string, register: string, schema: string, id: string}>} One entry per usable name; `id` is '' when the record holds no reference.
|
|
44
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
45
|
+
*/
|
|
46
|
+
function visibilityContextRefs(context, record) {
|
|
47
|
+
if (!context || typeof context !== 'object' || !record || typeof record !== 'object') {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
return Object.entries(context)
|
|
51
|
+
.filter(([name, ref]) => name !== '' && ref && typeof ref === 'object'
|
|
52
|
+
&& typeof ref.field === 'string' && ref.field !== ''
|
|
53
|
+
&& typeof ref.register === 'string' && ref.register !== ''
|
|
54
|
+
&& typeof ref.schema === 'string' && ref.schema !== '')
|
|
55
|
+
.map(([name, ref]) => ({
|
|
56
|
+
key: name,
|
|
57
|
+
register: ref.register,
|
|
58
|
+
schema: ref.schema,
|
|
59
|
+
id: referenceId(ref.field.split('.').reduce((obj, part) => (obj === null || obj === undefined ? obj : obj[part]), record)),
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read the records `refs` names. A reference without an id, and a read that
|
|
65
|
+
* fails, resolve `null`: the record is absent, and a condition on it fails
|
|
66
|
+
* safe rather than throwing out of the page.
|
|
67
|
+
*
|
|
68
|
+
* @param {Array<{key: string, register: string, schema: string, id: string}>} refs From {@link visibilityContextRefs}.
|
|
69
|
+
* @param {Map<string, object|null>} [cache] Rows already read, by `register/schema/id`.
|
|
70
|
+
* @return {Promise<object>} `{ <name>: row | null }`.
|
|
71
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
72
|
+
*/
|
|
73
|
+
async function fetchVisibilityRows(refs, cache) {
|
|
74
|
+
const rows = {};
|
|
75
|
+
await Promise.all((refs || []).map(async (ref) => {
|
|
76
|
+
if (!ref.id) {
|
|
77
|
+
rows[ref.key] = null;
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const cacheKey = `${ref.register}/${ref.schema}/${ref.id}`;
|
|
81
|
+
if (cache && cache.has(cacheKey)) {
|
|
82
|
+
rows[ref.key] = cache.get(cacheKey);
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
let row;
|
|
86
|
+
try {
|
|
87
|
+
const body = await cnFetchJson(`/apps/openregister/api/objects/${encodeURIComponent(ref.register)}/${encodeURIComponent(ref.schema)}/${encodeURIComponent(ref.id)}`);
|
|
88
|
+
row = body && typeof body === 'object' ? body : null;
|
|
89
|
+
} catch {
|
|
90
|
+
row = null;
|
|
91
|
+
}
|
|
92
|
+
if (cache) {
|
|
93
|
+
cache.set(cacheKey, row);
|
|
94
|
+
}
|
|
95
|
+
rows[ref.key] = row;
|
|
96
|
+
}));
|
|
97
|
+
return rows
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The data a widget condition reads: the record, with the named records
|
|
102
|
+
* beside it under `@context.<name>`. Null while the record has not loaded.
|
|
103
|
+
*
|
|
104
|
+
* @param {object|null} record The loaded record.
|
|
105
|
+
* @param {object} [rows] `{ <name>: row | null }`.
|
|
106
|
+
* @return {object|null} The condition data.
|
|
107
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
108
|
+
*/
|
|
109
|
+
function widgetConditionData(record, rows) {
|
|
110
|
+
if (!record || typeof record !== 'object') {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
return { ...record, '@context': { ...(rows || {}) } }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether a widget's own `visibleWhen` lets it show on a detail page.
|
|
118
|
+
*
|
|
119
|
+
* No condition: shown, as before. A workspace condition: shown, a detail page
|
|
120
|
+
* has no workspace. While the data is not there (`data` null, or `pending`):
|
|
121
|
+
* hidden, so the widget never shows and then vanishes. A local condition is
|
|
122
|
+
* decided here; an endpoint or source condition takes its answer from
|
|
123
|
+
* `answers` (by widget id) and is hidden until it has one.
|
|
124
|
+
*
|
|
125
|
+
* @param {object|null} widget The widget definition.
|
|
126
|
+
* @param {object|null} data From {@link widgetConditionData}.
|
|
127
|
+
* @param {{pending?: boolean, answers?: object}} [state] Whether named records are still on their way, and the remote answers.
|
|
128
|
+
* @return {boolean} Whether the widget shows.
|
|
129
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
130
|
+
*/
|
|
131
|
+
function detailWidgetVisible(widget, data, state = {}) {
|
|
132
|
+
const cond = widget && typeof widget === 'object' ? widget.visibleWhen : null;
|
|
133
|
+
if (cond === null || cond === undefined) {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
if (typeof cond === 'object' && !Array.isArray(cond) && isWorkspaceCondition(cond)) {
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
if (!data || state.pending === true) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
if (isLocallyDecidableVisibleWhen(cond)) {
|
|
143
|
+
return evaluateVisibleWhenLocal(cond, data)
|
|
144
|
+
}
|
|
145
|
+
return Boolean(state.answers && state.answers[widget.id] === true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData };
|
|
149
|
+
//# sourceMappingURL=detailWidgetVisibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detailWidgetVisibility.js","sources":["../../../src/utils/detailWidgetVisibility.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * detailWidgetVisibility: whether a widget on a detail page shows for the\n * loaded record, through the shared `visibleWhen` grammar.\n *\n * A detail page can name records it reads beside the loaded one\n * (`visibilityContext`, e.g. a case's case type). A condition reads such a\n * record as `@context.<name>.<path>`, so a widget can be shown for some case types\n * only while the configuration stays in the app manifest.\n *\n * @module utils/detailWidgetVisibility\n */\n\nimport { cnFetchJson } from './cnFetch.js'\nimport { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen, isWorkspaceCondition } from './visibleWhen.js'\n\n/**\n * Read a reference value as an id: a plain string or number, or an object\n * carrying `id`, `uuid` or `@self.id` (an extended reference).\n *\n * @param {unknown} value The value of the reference field.\n * @return {string} The id, or '' when there is none.\n */\nfunction referenceId(value) {\n\tif (typeof value === 'string' || typeof value === 'number') {\n\t\treturn String(value)\n\t}\n\tif (value && typeof value === 'object') {\n\t\tconst id = value.id ?? value.uuid ?? (value['@self'] && value['@self'].id)\n\t\treturn id === undefined || id === null ? '' : String(id)\n\t}\n\treturn ''\n}\n\n/**\n * The records a `visibilityContext` names for one loaded record.\n *\n * @param {object} context `{ <name>: { field, register, schema } }`.\n * @param {object|null} record The loaded record.\n * @return {Array<{key: string, register: string, schema: string, id: string}>} One entry per usable name; `id` is '' when the record holds no reference.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n */\nexport function visibilityContextRefs(context, record) {\n\tif (!context || typeof context !== 'object' || !record || typeof record !== 'object') {\n\t\treturn []\n\t}\n\treturn Object.entries(context)\n\t\t.filter(([name, ref]) => name !== '' && ref && typeof ref === 'object'\n\t\t\t&& typeof ref.field === 'string' && ref.field !== ''\n\t\t\t&& typeof ref.register === 'string' && ref.register !== ''\n\t\t\t&& typeof ref.schema === 'string' && ref.schema !== '')\n\t\t.map(([name, ref]) => ({\n\t\t\tkey: name,\n\t\t\tregister: ref.register,\n\t\t\tschema: ref.schema,\n\t\t\tid: referenceId(ref.field.split('.').reduce((obj, part) => (obj === null || obj === undefined ? obj : obj[part]), record)),\n\t\t}))\n}\n\n/**\n * Read the records `refs` names. A reference without an id, and a read that\n * fails, resolve `null`: the record is absent, and a condition on it fails\n * safe rather than throwing out of the page.\n *\n * @param {Array<{key: string, register: string, schema: string, id: string}>} refs From {@link visibilityContextRefs}.\n * @param {Map<string, object|null>} [cache] Rows already read, by `register/schema/id`.\n * @return {Promise<object>} `{ <name>: row | null }`.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n */\nexport async function fetchVisibilityRows(refs, cache) {\n\tconst rows = {}\n\tawait Promise.all((refs || []).map(async (ref) => {\n\t\tif (!ref.id) {\n\t\t\trows[ref.key] = null\n\t\t\treturn\n\t\t}\n\t\tconst cacheKey = `${ref.register}/${ref.schema}/${ref.id}`\n\t\tif (cache && cache.has(cacheKey)) {\n\t\t\trows[ref.key] = cache.get(cacheKey)\n\t\t\treturn\n\t\t}\n\t\tlet row\n\t\ttry {\n\t\t\tconst body = await cnFetchJson(`/apps/openregister/api/objects/${encodeURIComponent(ref.register)}/${encodeURIComponent(ref.schema)}/${encodeURIComponent(ref.id)}`)\n\t\t\trow = body && typeof body === 'object' ? body : null\n\t\t} catch {\n\t\t\trow = null\n\t\t}\n\t\tif (cache) {\n\t\t\tcache.set(cacheKey, row)\n\t\t}\n\t\trows[ref.key] = row\n\t}))\n\treturn rows\n}\n\n/**\n * The data a widget condition reads: the record, with the named records\n * beside it under `@context.<name>`. Null while the record has not loaded.\n *\n * @param {object|null} record The loaded record.\n * @param {object} [rows] `{ <name>: row | null }`.\n * @return {object|null} The condition data.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n */\nexport function widgetConditionData(record, rows) {\n\tif (!record || typeof record !== 'object') {\n\t\treturn null\n\t}\n\treturn { ...record, '@context': { ...(rows || {}) } }\n}\n\n/**\n * Whether a widget's own `visibleWhen` lets it show on a detail page.\n *\n * No condition: shown, as before. A workspace condition: shown, a detail page\n * has no workspace. While the data is not there (`data` null, or `pending`):\n * hidden, so the widget never shows and then vanishes. A local condition is\n * decided here; an endpoint or source condition takes its answer from\n * `answers` (by widget id) and is hidden until it has one.\n *\n * @param {object|null} widget The widget definition.\n * @param {object|null} data From {@link widgetConditionData}.\n * @param {{pending?: boolean, answers?: object}} [state] Whether named records are still on their way, and the remote answers.\n * @return {boolean} Whether the widget shows.\n * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n */\nexport function detailWidgetVisible(widget, data, state = {}) {\n\tconst cond = widget && typeof widget === 'object' ? widget.visibleWhen : null\n\tif (cond === null || cond === undefined) {\n\t\treturn true\n\t}\n\tif (typeof cond === 'object' && !Array.isArray(cond) && isWorkspaceCondition(cond)) {\n\t\treturn true\n\t}\n\tif (!data || state.pending === true) {\n\t\treturn false\n\t}\n\tif (isLocallyDecidableVisibleWhen(cond)) {\n\t\treturn evaluateVisibleWhenLocal(cond, data)\n\t}\n\treturn Boolean(state.answers && state.answers[widget.id] === true)\n}\n"],"names":[],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,WAAW,CAAC,KAAK,EAAE;AAC5B,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7D,EAAE,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,EAAE;AACF,CAAC,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AACzC,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,EAAC;AAC5E,EAAE,OAAO,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;AAC1D,EAAE;AACF,CAAC,OAAO,EAAE;AACV,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE;AACvD,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AACvF,EAAE,OAAO,EAAE;AACX,EAAE;AACF,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;AAC/B,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,IAAI,KAAK,EAAE,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;AACxE,MAAM,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,IAAI,GAAG,CAAC,KAAK,KAAK,EAAE;AACvD,MAAM,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE;AAC7D,MAAM,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC;AAC1D,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM;AACzB,GAAG,GAAG,EAAE,IAAI;AACZ,GAAG,QAAQ,EAAE,GAAG,CAAC,QAAQ;AACzB,GAAG,MAAM,EAAE,GAAG,CAAC,MAAM;AACrB,GAAG,EAAE,EAAE,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,MAAM,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,GAAG,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAC7H,GAAG,CAAC,CAAC;AACL,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,eAAe,mBAAmB,CAAC,IAAI,EAAE,KAAK,EAAE;AACvD,CAAC,MAAM,IAAI,GAAG,GAAE;AAChB,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,GAAG,CAAC,OAAO,GAAG,KAAK;AACnD,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE;AACf,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAI;AACvB,GAAG,MAAM;AACT,GAAG;AACH,EAAE,MAAM,QAAQ,GAAG,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,EAAC;AAC5D,EAAE,IAAI,KAAK,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;AACpC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAC;AACtC,GAAG,MAAM;AACT,GAAG;AACH,EAAE,IAAI,IAAG;AACT,EAAE,IAAI;AACN,GAAG,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,CAAC,+BAA+B,EAAE,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAC;AACvK,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,GAAG,IAAI,GAAG,KAAI;AACvD,GAAG,CAAC,MAAM;AACV,GAAG,GAAG,GAAG,KAAI;AACb,GAAG;AACH,EAAE,IAAI,KAAK,EAAE;AACb,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAC;AAC3B,GAAG;AACH,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAG;AACrB,EAAE,CAAC,EAAC;AACJ,CAAC,OAAO,IAAI;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,mBAAmB,CAAC,MAAM,EAAE,IAAI,EAAE;AAClD,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC5C,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,OAAO,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE;AACtD,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,mBAAmB,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE;AAC9D,CAAC,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,GAAG,MAAM,CAAC,WAAW,GAAG,KAAI;AAC9E,CAAC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE;AAC1C,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAAE;AACrF,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE;AACtC,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC,IAAI,6BAA6B,CAAC,IAAI,CAAC,EAAE;AAC1C,EAAE,OAAO,wBAAwB,CAAC,IAAI,EAAE,IAAI,CAAC;AAC7C,EAAE;AACF,CAAC,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;AACnE;;;;"}
|
|
@@ -220,6 +220,35 @@ function metricLocale() {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A format spec with its literal `prefix` / `suffix` run through the host
|
|
225
|
+
* translate function. A `@`-token, an empty string, or a missing translate
|
|
226
|
+
* leaves the affix as written.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} format The `content.format` spec.
|
|
229
|
+
* @param {((text: string) => string)|null} translate The host translate function.
|
|
230
|
+
* @return {object} A copy of the spec (the same object when nothing changes).
|
|
231
|
+
* @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated
|
|
232
|
+
*/
|
|
233
|
+
function translateMetricAffixes(format, translate) {
|
|
234
|
+
const fmt = format || {};
|
|
235
|
+
if (typeof translate !== 'function') {
|
|
236
|
+
return fmt
|
|
237
|
+
}
|
|
238
|
+
let out = fmt;
|
|
239
|
+
for (const key of ['prefix', 'suffix']) {
|
|
240
|
+
const text = fmt[key];
|
|
241
|
+
if (typeof text === 'string' && text.trim() !== '' && text.charAt(0) !== '@') {
|
|
242
|
+
const translated = translate(text);
|
|
243
|
+
if (typeof translated === 'string' && translated !== text) {
|
|
244
|
+
out = out === fmt ? { ...fmt } : out;
|
|
245
|
+
out[key] = translated;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
223
252
|
/**
|
|
224
253
|
* Format a numeric value per a manifest `content.format` spec.
|
|
225
254
|
*
|
|
@@ -244,11 +273,18 @@ function metricLocale() {
|
|
|
244
273
|
* @param {unknown} value The raw value to format.
|
|
245
274
|
* @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).
|
|
246
275
|
* @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.
|
|
276
|
+
* A literal `prefix` / `suffix` (one that is not a `@config.` or `@object.`
|
|
277
|
+
* token) is run through `translate` when one is given, as every other
|
|
278
|
+
* manifest-authored string is, so an app's catalogue entry for " days"
|
|
279
|
+
* (" dagen") applies. A token is resolved, never translated.
|
|
280
|
+
*
|
|
247
281
|
* @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.
|
|
282
|
+
* @param {((text: string) => string)|null} [translate] The host app's translate function (`cnTranslate`).
|
|
248
283
|
* @return {string} The formatted display string.
|
|
249
284
|
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
285
|
+
* @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated
|
|
250
286
|
*/
|
|
251
|
-
function formatMetricValue(value, format, configCtx, object = null) {
|
|
287
|
+
function formatMetricValue(value, format, configCtx, object = null, translate = null) {
|
|
252
288
|
if (value === null || value === undefined) {
|
|
253
289
|
return '—'
|
|
254
290
|
}
|
|
@@ -257,7 +293,7 @@ function formatMetricValue(value, format, configCtx, object = null) {
|
|
|
257
293
|
return String(value)
|
|
258
294
|
}
|
|
259
295
|
|
|
260
|
-
const fmt = resolveConfigFormat(format, configCtx, object);
|
|
296
|
+
const fmt = resolveConfigFormat(translateMetricAffixes(format, translate), configCtx, object);
|
|
261
297
|
const locale = metricLocale();
|
|
262
298
|
// `duration-hours` and `decimal` default to ONE fraction digit; the
|
|
263
299
|
// other styles keep the pre-existing default of 0.
|
|
@@ -293,5 +329,5 @@ function formatMetricValue(value, format, configCtx, object = null) {
|
|
|
293
329
|
return `${fmt.prefix || ''}${body}${fmt.suffix || ''}`
|
|
294
330
|
}
|
|
295
331
|
|
|
296
|
-
export { formatMetricValue, formatMoney, metricLocale, normalizeMetricFormat, resolveConfigFormat, resolveFormatCurrency, safeCurrencyCode, unwrapAppConfig };
|
|
332
|
+
export { formatMetricValue, formatMoney, metricLocale, normalizeMetricFormat, resolveConfigFormat, resolveFormatCurrency, safeCurrencyCode, translateMetricAffixes, unwrapAppConfig };
|
|
297
333
|
//# sourceMappingURL=formatMetric.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatMetric.js","sources":["../../../src/utils/formatMetric.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { boardDateLocale } from './boardDate.js'\nimport { resolveFilterValue } from './resolveFilterTokens.js'\n\n/**\n * The format styles a stat or stats-block entry may name.\n *\n * @type {Array<string>}\n */\nexport const METRIC_FORMAT_STYLES = ['number', 'currency', 'percent', 'duration-hours', 'decimal']\n\n/**\n * Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.\n *\n * Dashboard / detail pages provide `cnAppConfig` either as a plain object or as\n * a Vue ref (`{ value: {...} }`, depending on how the page seeds it). Widgets\n * need the plain map to resolve `@config.<key>` tokens against it.\n *\n * @param {object} cnAppConfig The injected app-config value (object or ref).\n * @return {object} The plain config map (always an object; `{}` when absent).\n */\nexport function unwrapAppConfig(cnAppConfig) {\n\tconst c = cnAppConfig\n\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n}\n\n/**\n * Resolve `@config.<key>` tokens in a `content.format` spec's string fields.\n *\n * Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.\n * `currency: '@config.currency'`), or an `@object.<field>` token read off the\n * detail page's object when `object` is given. Tokens are resolved against\n * `configCtx`; a token that stays unresolved (the config key is unset, or the\n * config map has not been injected yet during a route transition) is DROPPED\n * so the downstream literal default applies instead of a raw `@config.…`\n * string leaking into `Intl.NumberFormat`.\n *\n * @param {object} format The raw `content.format` spec.\n * @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).\n * @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.\n * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = {\n\t\tconfig: (configCtx && typeof configCtx === 'object') ? configCtx : {},\n\t\tobject: (object && typeof object === 'object') ? object : null,\n\t}\n\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\tconst raw = fmt[key]\n\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\tcontinue\n\t\t}\n\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t}\n\treturn out\n}\n\n/**\n * Coerce a currency value to a safe ISO-4217-shaped code.\n *\n * `Intl.NumberFormat({ style: 'currency', currency })` throws a `RangeError`\n * when `currency` is not a three-letter code — which is exactly what happens\n * when an unresolved `@config.currency` token (or any bad value) reaches it.\n * This guard is the single choke point that keeps a malformed currency from\n * ever throwing: anything that is not three ASCII letters falls back to `EUR`.\n *\n * @param {unknown} currency The (possibly unresolved / invalid) currency value.\n * @return {string} A safe upper-case three-letter code (falls back to `EUR`).\n */\nexport function safeCurrencyCode(currency) {\n\treturn (typeof currency === 'string' && /^[A-Za-z]{3}$/.test(currency))\n\t\t? currency.toUpperCase()\n\t\t: 'EUR'\n}\n\n/**\n * Whether a value is a three-letter currency code.\n *\n * @param {unknown} value The candidate.\n * @return {boolean}\n */\nfunction isCurrencyCode(value) {\n\treturn typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)\n}\n\n/**\n * Read a dot-path off an object.\n *\n * @param {object} object The object.\n * @param {string} path The path, e.g. `currency` or `price.currency`.\n * @return {unknown} The value, or undefined.\n */\nfunction readPath(object, path) {\n\treturn String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)\n}\n\n/**\n * The currency a money value is shown in. In order:\n *\n * 1. `currencyField`: the field of the page's object that holds the currency\n * (a detail page; e.g. a contract's own `currency`).\n * 2. `currency`: a literal code, or a token (`@config.currency`,\n * `@object.currency`).\n * 3. The app's reporting currency, `currency` in the page's app config.\n * 4. `EUR`.\n *\n * A step whose value is not a three-letter code is skipped, so an object\n * without a currency falls back to the reporting currency instead of\n * throwing in `Intl.NumberFormat`.\n *\n * @param {object} format The format spec (`{ currency?, currencyField? }`).\n * @param {object} configCtx The page-level app-config map.\n * @param {object|null} [object] The detail page's object.\n * @return {string} An upper-case three-letter code.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function resolveFormatCurrency(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tif (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {\n\t\tconst own = readPath(object, fmt.currencyField)\n\t\tif (isCurrencyCode(own)) {\n\t\t\treturn own.toUpperCase()\n\t\t}\n\t}\n\tconst declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency\n\tif (isCurrencyCode(declared)) {\n\t\treturn declared.toUpperCase()\n\t}\n\tconst reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined\n\treturn safeCurrencyCode(reporting)\n}\n\n/**\n * A money amount in a currency, written for the reader's language (\"€ 1.234,50\").\n * The currency is the literal `currency` or the code in the row's\n * `currencyField` (a dot path); when neither is a three-letter code it is the\n * app's reporting currency (`currency` in the app config), then EUR. Empty and non-numeric values answer ''.\n *\n * @param {unknown} value The amount.\n * @param {{currency?: string, currencyField?: string, decimals?: number}} [format] The format spec.\n * @param {object|null} [row] The row `currencyField` reads from.\n * @param {object|null} [configCtx] The app config map; its `currency` (the reporting currency) is used when neither resolves.\n * @return {string} The text.\n * @spec openspec/changes/screens-currency-format/specs/currency-format/spec.md#requirement-a-money-value-reads-in-its-currency\n */\nexport function formatMoney(value, format = {}, row = null, configCtx = null) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn ''\n\t}\n\tconst fmt = format || {}\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : 2\n\tconst options = {\n\t\tstyle: 'currency',\n\t\tcurrency: resolveFormatCurrency(fmt, configCtx, row),\n\t\tminimumFractionDigits: decimals,\n\t\tmaximumFractionDigits: decimals,\n\t}\n\ttry {\n\t\treturn new Intl.NumberFormat(boardDateLocale(), options).format(num)\n\t} catch {\n\t\treturn new Intl.NumberFormat(undefined, options).format(num)\n\t}\n}\n\n/**\n * The format spec of a stat or stats-block entry in one shape.\n *\n * `format` may be a style name (`\"currency\"`) or an object (`{ style,\n * currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`\n * or `currencyField` on the entry itself fills the same keys, and either one\n * makes the style `currency` when none is named. Answers null when the entry\n * declares no format at all, so the caller keeps its own rendering.\n *\n * @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).\n * @return {object|null} The format spec, or null.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value\n */\nexport function normalizeMetricFormat(entry) {\n\tconst source = entry && typeof entry === 'object' ? entry : {}\n\tlet fmt = null\n\tif (typeof source.format === 'string' && source.format !== '') {\n\t\tfmt = { style: source.format }\n\t} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {\n\t\tfmt = { ...source.format }\n\t}\n\tfor (const key of ['currency', 'currencyField']) {\n\t\tif (typeof source[key] === 'string' && source[key] !== '') {\n\t\t\tfmt = fmt || {}\n\t\t\tif (fmt[key] === undefined) {\n\t\t\t\tfmt[key] = source[key]\n\t\t\t}\n\t\t}\n\t}\n\tif (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {\n\t\tfmt.style = 'currency'\n\t}\n\treturn fmt\n}\n\n/**\n * The user's locale for number formatting: the Nextcloud locale setting,\n * else the browser's.\n *\n * @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.\n */\nexport function metricLocale() {\n\ttry {\n\t\tconst tag = getCanonicalLocale()\n\t\t// A tag Intl does not accept would throw a RangeError on every value.\n\t\treturn tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Format a numeric value per a manifest `content.format` spec.\n *\n * Handles the `number` / `currency` / `percent` / `duration-hours` /\n * `decimal` styles, resolves `@config.<key>` tokens in `currency` / `prefix`\n * / `suffix` against `configCtx`, and guards the currency code so an\n * unresolved or invalid currency can never throw a `RangeError`. Returns\n * `'—'` for null/undefined and the raw string for non-numeric input,\n * matching the KPI widgets' prior behaviour.\n *\n * Style notes:\n * - `duration-hours` renders an hour count with an `h` suffix and ONE\n * fraction digit by default (`42.5h` — the pipelinq resolution-time KPI\n * contract); `decimals` overrides the fraction digits.\n * - `decimal` is a plain number with ONE fraction digit by default (the\n * fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.\n *\n * The currency follows `resolveFormatCurrency`: the object's own currency\n * (`currencyField`), then `currency`, then the app's reporting currency, then\n * EUR. Numbers are formatted in the user's locale (`metricLocale`).\n *\n * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.\n * @return {string} The formatted display string.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function formatMetricValue(value, format, configCtx, object = null) {\n\tif (value === null || value === undefined) {\n\t\treturn '—'\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\n\tconst fmt = resolveConfigFormat(format, configCtx, object)\n\tconst locale = metricLocale()\n\t// `duration-hours` and `decimal` default to ONE fraction digit; the\n\t// other styles keep the pre-existing default of 0.\n\tconst oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : (oneDigitDefault ? 1 : 0)\n\n\tlet body\n\tif (fmt.style === 'currency') {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: resolveFormatCurrency(format, configCtx, object),\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t} else if (fmt.style === 'percent') {\n\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + '%'\n\t} else if (fmt.style === 'duration-hours') {\n\t\t// An hour count (e.g. mean resolution time): `42.5h`.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + 'h'\n\t} else {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t}\n\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"formatMetric.js","sources":["../../../src/utils/formatMetric.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { boardDateLocale } from './boardDate.js'\nimport { resolveFilterValue } from './resolveFilterTokens.js'\n\n/**\n * The format styles a stat or stats-block entry may name.\n *\n * @type {Array<string>}\n */\nexport const METRIC_FORMAT_STYLES = ['number', 'currency', 'percent', 'duration-hours', 'decimal']\n\n/**\n * Unwrap a possibly-ref-wrapped `cnAppConfig` inject into a plain object.\n *\n * Dashboard / detail pages provide `cnAppConfig` either as a plain object or as\n * a Vue ref (`{ value: {...} }`, depending on how the page seeds it). Widgets\n * need the plain map to resolve `@config.<key>` tokens against it.\n *\n * @param {object} cnAppConfig The injected app-config value (object or ref).\n * @return {object} The plain config map (always an object; `{}` when absent).\n */\nexport function unwrapAppConfig(cnAppConfig) {\n\tconst c = cnAppConfig\n\tconst unwrapped = (c && typeof c === 'object' && 'value' in c) ? c.value : c\n\treturn (unwrapped && typeof unwrapped === 'object') ? unwrapped : {}\n}\n\n/**\n * Resolve `@config.<key>` tokens in a `content.format` spec's string fields.\n *\n * Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.\n * `currency: '@config.currency'`), or an `@object.<field>` token read off the\n * detail page's object when `object` is given. Tokens are resolved against\n * `configCtx`; a token that stays unresolved (the config key is unset, or the\n * config map has not been injected yet during a route transition) is DROPPED\n * so the downstream literal default applies instead of a raw `@config.…`\n * string leaking into `Intl.NumberFormat`.\n *\n * @param {object} format The raw `content.format` spec.\n * @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).\n * @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.\n * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = {\n\t\tconfig: (configCtx && typeof configCtx === 'object') ? configCtx : {},\n\t\tobject: (object && typeof object === 'object') ? object : null,\n\t}\n\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\tconst raw = fmt[key]\n\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\tcontinue\n\t\t}\n\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t}\n\treturn out\n}\n\n/**\n * Coerce a currency value to a safe ISO-4217-shaped code.\n *\n * `Intl.NumberFormat({ style: 'currency', currency })` throws a `RangeError`\n * when `currency` is not a three-letter code — which is exactly what happens\n * when an unresolved `@config.currency` token (or any bad value) reaches it.\n * This guard is the single choke point that keeps a malformed currency from\n * ever throwing: anything that is not three ASCII letters falls back to `EUR`.\n *\n * @param {unknown} currency The (possibly unresolved / invalid) currency value.\n * @return {string} A safe upper-case three-letter code (falls back to `EUR`).\n */\nexport function safeCurrencyCode(currency) {\n\treturn (typeof currency === 'string' && /^[A-Za-z]{3}$/.test(currency))\n\t\t? currency.toUpperCase()\n\t\t: 'EUR'\n}\n\n/**\n * Whether a value is a three-letter currency code.\n *\n * @param {unknown} value The candidate.\n * @return {boolean}\n */\nfunction isCurrencyCode(value) {\n\treturn typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)\n}\n\n/**\n * Read a dot-path off an object.\n *\n * @param {object} object The object.\n * @param {string} path The path, e.g. `currency` or `price.currency`.\n * @return {unknown} The value, or undefined.\n */\nfunction readPath(object, path) {\n\treturn String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)\n}\n\n/**\n * The currency a money value is shown in. In order:\n *\n * 1. `currencyField`: the field of the page's object that holds the currency\n * (a detail page; e.g. a contract's own `currency`).\n * 2. `currency`: a literal code, or a token (`@config.currency`,\n * `@object.currency`).\n * 3. The app's reporting currency, `currency` in the page's app config.\n * 4. `EUR`.\n *\n * A step whose value is not a three-letter code is skipped, so an object\n * without a currency falls back to the reporting currency instead of\n * throwing in `Intl.NumberFormat`.\n *\n * @param {object} format The format spec (`{ currency?, currencyField? }`).\n * @param {object} configCtx The page-level app-config map.\n * @param {object|null} [object] The detail page's object.\n * @return {string} An upper-case three-letter code.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n */\nexport function resolveFormatCurrency(format, configCtx, object = null) {\n\tconst fmt = format || {}\n\tif (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {\n\t\tconst own = readPath(object, fmt.currencyField)\n\t\tif (isCurrencyCode(own)) {\n\t\t\treturn own.toUpperCase()\n\t\t}\n\t}\n\tconst declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency\n\tif (isCurrencyCode(declared)) {\n\t\treturn declared.toUpperCase()\n\t}\n\tconst reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined\n\treturn safeCurrencyCode(reporting)\n}\n\n/**\n * A money amount in a currency, written for the reader's language (\"€ 1.234,50\").\n * The currency is the literal `currency` or the code in the row's\n * `currencyField` (a dot path); when neither is a three-letter code it is the\n * app's reporting currency (`currency` in the app config), then EUR. Empty and non-numeric values answer ''.\n *\n * @param {unknown} value The amount.\n * @param {{currency?: string, currencyField?: string, decimals?: number}} [format] The format spec.\n * @param {object|null} [row] The row `currencyField` reads from.\n * @param {object|null} [configCtx] The app config map; its `currency` (the reporting currency) is used when neither resolves.\n * @return {string} The text.\n * @spec openspec/changes/screens-currency-format/specs/currency-format/spec.md#requirement-a-money-value-reads-in-its-currency\n */\nexport function formatMoney(value, format = {}, row = null, configCtx = null) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn ''\n\t}\n\tconst fmt = format || {}\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : 2\n\tconst options = {\n\t\tstyle: 'currency',\n\t\tcurrency: resolveFormatCurrency(fmt, configCtx, row),\n\t\tminimumFractionDigits: decimals,\n\t\tmaximumFractionDigits: decimals,\n\t}\n\ttry {\n\t\treturn new Intl.NumberFormat(boardDateLocale(), options).format(num)\n\t} catch {\n\t\treturn new Intl.NumberFormat(undefined, options).format(num)\n\t}\n}\n\n/**\n * The format spec of a stat or stats-block entry in one shape.\n *\n * `format` may be a style name (`\"currency\"`) or an object (`{ style,\n * currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`\n * or `currencyField` on the entry itself fills the same keys, and either one\n * makes the style `currency` when none is named. Answers null when the entry\n * declares no format at all, so the caller keeps its own rendering.\n *\n * @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).\n * @return {object|null} The format spec, or null.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value\n */\nexport function normalizeMetricFormat(entry) {\n\tconst source = entry && typeof entry === 'object' ? entry : {}\n\tlet fmt = null\n\tif (typeof source.format === 'string' && source.format !== '') {\n\t\tfmt = { style: source.format }\n\t} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {\n\t\tfmt = { ...source.format }\n\t}\n\tfor (const key of ['currency', 'currencyField']) {\n\t\tif (typeof source[key] === 'string' && source[key] !== '') {\n\t\t\tfmt = fmt || {}\n\t\t\tif (fmt[key] === undefined) {\n\t\t\t\tfmt[key] = source[key]\n\t\t\t}\n\t\t}\n\t}\n\tif (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {\n\t\tfmt.style = 'currency'\n\t}\n\treturn fmt\n}\n\n/**\n * The user's locale for number formatting: the Nextcloud locale setting,\n * else the browser's.\n *\n * @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.\n */\nexport function metricLocale() {\n\ttry {\n\t\tconst tag = getCanonicalLocale()\n\t\t// A tag Intl does not accept would throw a RangeError on every value.\n\t\treturn tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * A format spec with its literal `prefix` / `suffix` run through the host\n * translate function. A `@`-token, an empty string, or a missing translate\n * leaves the affix as written.\n *\n * @param {object} format The `content.format` spec.\n * @param {((text: string) => string)|null} translate The host translate function.\n * @return {object} A copy of the spec (the same object when nothing changes).\n * @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated\n */\nexport function translateMetricAffixes(format, translate) {\n\tconst fmt = format || {}\n\tif (typeof translate !== 'function') {\n\t\treturn fmt\n\t}\n\tlet out = fmt\n\tfor (const key of ['prefix', 'suffix']) {\n\t\tconst text = fmt[key]\n\t\tif (typeof text === 'string' && text.trim() !== '' && text.charAt(0) !== '@') {\n\t\t\tconst translated = translate(text)\n\t\t\tif (typeof translated === 'string' && translated !== text) {\n\t\t\t\tout = out === fmt ? { ...fmt } : out\n\t\t\t\tout[key] = translated\n\t\t\t}\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * Format a numeric value per a manifest `content.format` spec.\n *\n * Handles the `number` / `currency` / `percent` / `duration-hours` /\n * `decimal` styles, resolves `@config.<key>` tokens in `currency` / `prefix`\n * / `suffix` against `configCtx`, and guards the currency code so an\n * unresolved or invalid currency can never throw a `RangeError`. Returns\n * `'—'` for null/undefined and the raw string for non-numeric input,\n * matching the KPI widgets' prior behaviour.\n *\n * Style notes:\n * - `duration-hours` renders an hour count with an `h` suffix and ONE\n * fraction digit by default (`42.5h` — the pipelinq resolution-time KPI\n * contract); `decimals` overrides the fraction digits.\n * - `decimal` is a plain number with ONE fraction digit by default (the\n * fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.\n *\n * The currency follows `resolveFormatCurrency`: the object's own currency\n * (`currencyField`), then `currency`, then the app's reporting currency, then\n * EUR. Numbers are formatted in the user's locale (`metricLocale`).\n *\n * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * A literal `prefix` / `suffix` (one that is not a `@config.` or `@object.`\n * token) is run through `translate` when one is given, as every other\n * manifest-authored string is, so an app's catalogue entry for \" days\"\n * (\" dagen\") applies. A token is resolved, never translated.\n *\n * @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.\n * @param {((text: string) => string)|null} [translate] The host app's translate function (`cnTranslate`).\n * @return {string} The formatted display string.\n * @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names\n * @spec openspec/changes/screens-metric-affix-translate/specs/dashboard-page/spec.md#requirement-a-metric-prefix-and-suffix-are-translated\n */\nexport function formatMetricValue(value, format, configCtx, object = null, translate = null) {\n\tif (value === null || value === undefined) {\n\t\treturn '—'\n\t}\n\tconst num = Number(value)\n\tif (!Number.isFinite(num)) {\n\t\treturn String(value)\n\t}\n\n\tconst fmt = resolveConfigFormat(translateMetricAffixes(format, translate), configCtx, object)\n\tconst locale = metricLocale()\n\t// `duration-hours` and `decimal` default to ONE fraction digit; the\n\t// other styles keep the pre-existing default of 0.\n\tconst oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal'\n\tconst decimals = Number.isFinite(fmt.decimals) ? fmt.decimals : (oneDigitDefault ? 1 : 0)\n\n\tlet body\n\tif (fmt.style === 'currency') {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: resolveFormatCurrency(format, configCtx, object),\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t} else if (fmt.style === 'percent') {\n\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + '%'\n\t} else if (fmt.style === 'duration-hours') {\n\t\t// An hour count (e.g. mean resolution time): `42.5h`.\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num) + 'h'\n\t} else {\n\t\tbody = new Intl.NumberFormat(locale, {\n\t\t\tminimumFractionDigits: decimals,\n\t\t\tmaximumFractionDigits: decimals,\n\t\t}).format(num)\n\t}\n\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -58,6 +58,7 @@ const SENTINEL_RESOLVERS = Object.freeze({
|
|
|
58
58
|
answer: 'resolveAssignValue (utils/formAssign.js)',
|
|
59
59
|
row: 'CnFieldListWidget (rowActions request)',
|
|
60
60
|
subject: 'CnBannerWidget (action route)',
|
|
61
|
+
context: 'detailWidgetVisible (utils/detailWidgetVisibility.js)',
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveManifestTokens.js","sources":["../../../src/utils/resolveManifestTokens.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Single documented resolver-dispatch entrypoint for the closed sentinel-token\n * vocabulary. Before this module the four resolvers were invoked ad-hoc by\n * callers who had to know which resolver owned which token. This entrypoint\n * routes a token to its owning resolver BY CONTEXT (see\n * {@link module:utils/sentinelTokens}) so the vocabulary cannot drift between\n * the schema, the runtime, and the gate.\n *\n * It does NOT re-implement resolution: it composes the existing resolver\n * functions verbatim —\n *\n * - route context → {@link resolveRouteSentinels} (reference-preserving walk +\n * per-page unresolved warning);\n * - filter / object / workspace / config(@config.<key>) contexts →\n * {@link resolveFilterValue} per leaf (identical behaviour to\n * resolveFilterTokens, which is that function applied over a filter map);\n * - config `@resolve:<key>` context → handled at manifest-LOAD time by\n * `resolveManifestSentinels` (async IAppConfig), so a render-time\n * subtree pass leaves it untouched — matching today's split;\n * - declarative `@self`/`@ref`/`@aggregate` → resolved server-side by\n * OpenRegister, so they pass through the client walk untouched;\n * - visibleWhen `@total` (manifest-form-logic) → resolved by\n * {@link module:utils/visibleWhen}'s own `evaluateVisibleWhen` /\n * `evaluateVisibleWhenLocal`, not by this generic subtree walk, so it\n * likewise passes through untouched.\n *\n * On top it adds the migration-window behaviour the vocabulary requires: a\n * one-time `console.warn` naming the canonical replacement whenever a\n * DEPRECATED token is encountered.\n *\n * @module utils/resolveManifestTokens\n */\n\nimport { isOptionalUnresolved, resolveFilterValue } from './resolveFilterTokens.js'\nimport resolveRouteSentinels from './resolveRouteSentinels.js'\nimport { contextOf, looksLikeSentinel, matchDeprecation, SENTINEL_CONTEXTS } from './sentinelTokens.js'\n\n/**\n * Dispatch table: canonical context → the resolver that owns it. Documentation\n * + programmatic lookup; the actual routing in {@link resolveManifestSubtree}\n * uses the same source-of-truth classifier ({@link contextOf}).\n *\n * @type {Readonly<Record<string, string>>}\n */\nexport const SENTINEL_RESOLVERS = Object.freeze({\n\tfilter: 'resolveFilterTokens',\n\tconfig: 'resolveManifestSentinels / resolveFilterTokens',\n\tobject: 'resolveFilterTokens',\n\tworkspace: 'resolveFilterTokens',\n\troute: 'resolveRouteSentinels',\n\tdeclarative: 'fetchAggregate (OpenRegister server-side)',\n\tvisibleWhen: 'evaluateVisibleWhen (utils/visibleWhen.js)',\n\tanswer: 'resolveAssignValue (utils/formAssign.js)',\n\trow: 'CnFieldListWidget (rowActions request)',\n\tsubject: 'CnBannerWidget (action route)',\n})\n\n/**\n * Process-wide set of deprecated tokens already warned about, so the same\n * deprecated token referenced from many pages warns once. Cleared by\n * {@link clearDeprecationWarnings} (test-only).\n *\n * @type {Set<string>}\n */\nconst _warnedDeprecations = new Set()\n\n/**\n * Reset the one-time deprecation-warning dedup set. Test-only.\n *\n * @return {void}\n */\nexport function clearDeprecationWarnings() {\n\t_warnedDeprecations.clear()\n}\n\n/**\n * Emit a one-time deprecation warning for `token` if it is on the deprecation\n * map. Idempotent per token for the process lifetime.\n *\n * @param {string} token The `@`-prefixed token.\n * @param {(...args: unknown[]) => void} warn console.warn override (tests).\n * @return {boolean} True when the token is deprecated (regardless of whether a\n * warning was emitted this call).\n */\nexport function warnIfDeprecated(token, warn) {\n\tconst dep = matchDeprecation(token)\n\tif (!dep) {\n\t\treturn false\n\t}\n\tif (!_warnedDeprecations.has(token)) {\n\t\t_warnedDeprecations.add(token)\n\t\tconst target = dep.replacement\n\t\t\t? `use '${dep.replacement}' instead`\n\t\t\t: 'it will be removed with no direct replacement'\n\t\twarn(`[resolveManifestTokens] Deprecated sentinel token '${token}' — ${target} `\n\t\t\t+ `(removal ${dep.removal}). ${dep.note}`)\n\t}\n\treturn true\n}\n\n/**\n * Resolve every sentinel token in a manifest subtree through the single\n * dispatch, routing each by context to its existing resolver.\n *\n * Behaviour is the composition of the existing resolvers, so this is a\n * drop-in for a caller that previously ran `resolveRouteSentinels` then a\n * `resolveFilterTokens`-style pass by hand.\n *\n * @param {unknown} value The subtree (typically a `pages[].config` block). Not mutated.\n * @param {object} [opts] Resolution inputs.\n * @param {object} [opts.params] vue-router params for `@route.<param>`.\n * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object}} [opts.ctx]\n * Context forwarded to {@link resolveFilterValue} for filter / object /\n * workspace / `@config.<key>` tokens.\n * @param {string} [opts.pageId] Page id for route-resolver warning dedup.\n * @param {(...args: unknown[]) => void} [opts.warn] console.warn override (tests).\n * @return {{value: unknown, unresolved: string[]}} The resolved subtree plus the\n * list of tokens that stayed unresolved (excluding OPTIONAL `?` tokens, which\n * are meant to be dropped, not waited on).\n */\nexport function resolveManifestSubtree(value, opts = {}) {\n\tconst params = opts.params && typeof opts.params === 'object' ? opts.params : {}\n\tconst ctx = opts.ctx && typeof opts.ctx === 'object' ? opts.ctx : {}\n\tconst pageId = opts.pageId || '<unknown>'\n\tconst warn = opts.warn ?? ((...args) => {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(...args)\n\t})\n\n\t// Pass 1: route context (reuses the reference-preserving route walker,\n\t// including its own per-page unresolved warning).\n\tconst afterRoute = resolveRouteSentinels(value, params, pageId)\n\n\t// Pass 2: filter / object / workspace / @config.<key> contexts, per leaf,\n\t// via the existing resolveFilterValue. Deprecated tokens warn once.\n\tconst unresolved = []\n\tconst walk = (node) => {\n\t\tif (typeof node === 'string') {\n\t\t\tif (!looksLikeSentinel(node)) {\n\t\t\t\treturn node\n\t\t\t}\n\t\t\twarnIfDeprecated(node, warn)\n\t\t\tconst resolved = resolveFilterValue(node, ctx)\n\t\t\tif (resolved === node && !isOptionalUnresolved(node)) {\n\t\t\t\t// Still a raw token: unresolved unless it is a load-time / server-side\n\t\t\t\t// context this render-pass deliberately leaves alone.\n\t\t\t\tconst c = contextOf(node)\n\t\t\t\tif (c !== 'config' && c !== 'declarative' && c !== 'visibleWhen') {\n\t\t\t\t\tunresolved.push(node)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn resolved\n\t\t}\n\t\tif (Array.isArray(node)) {\n\t\t\treturn node.map(walk)\n\t\t}\n\t\tif (node && typeof node === 'object') {\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(node)) {\n\t\t\t\tout[k] = walk(v)\n\t\t\t}\n\t\t\treturn out\n\t\t}\n\t\treturn node\n\t}\n\n\treturn { value: walk(afterRoute), unresolved }\n}\n\nexport { SENTINEL_CONTEXTS }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resolveManifestTokens.js","sources":["../../../src/utils/resolveManifestTokens.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Single documented resolver-dispatch entrypoint for the closed sentinel-token\n * vocabulary. Before this module the four resolvers were invoked ad-hoc by\n * callers who had to know which resolver owned which token. This entrypoint\n * routes a token to its owning resolver BY CONTEXT (see\n * {@link module:utils/sentinelTokens}) so the vocabulary cannot drift between\n * the schema, the runtime, and the gate.\n *\n * It does NOT re-implement resolution: it composes the existing resolver\n * functions verbatim —\n *\n * - route context → {@link resolveRouteSentinels} (reference-preserving walk +\n * per-page unresolved warning);\n * - filter / object / workspace / config(@config.<key>) contexts →\n * {@link resolveFilterValue} per leaf (identical behaviour to\n * resolveFilterTokens, which is that function applied over a filter map);\n * - config `@resolve:<key>` context → handled at manifest-LOAD time by\n * `resolveManifestSentinels` (async IAppConfig), so a render-time\n * subtree pass leaves it untouched — matching today's split;\n * - declarative `@self`/`@ref`/`@aggregate` → resolved server-side by\n * OpenRegister, so they pass through the client walk untouched;\n * - visibleWhen `@total` (manifest-form-logic) → resolved by\n * {@link module:utils/visibleWhen}'s own `evaluateVisibleWhen` /\n * `evaluateVisibleWhenLocal`, not by this generic subtree walk, so it\n * likewise passes through untouched.\n *\n * On top it adds the migration-window behaviour the vocabulary requires: a\n * one-time `console.warn` naming the canonical replacement whenever a\n * DEPRECATED token is encountered.\n *\n * @module utils/resolveManifestTokens\n */\n\nimport { isOptionalUnresolved, resolveFilterValue } from './resolveFilterTokens.js'\nimport resolveRouteSentinels from './resolveRouteSentinels.js'\nimport { contextOf, looksLikeSentinel, matchDeprecation, SENTINEL_CONTEXTS } from './sentinelTokens.js'\n\n/**\n * Dispatch table: canonical context → the resolver that owns it. Documentation\n * + programmatic lookup; the actual routing in {@link resolveManifestSubtree}\n * uses the same source-of-truth classifier ({@link contextOf}).\n *\n * @type {Readonly<Record<string, string>>}\n */\nexport const SENTINEL_RESOLVERS = Object.freeze({\n\tfilter: 'resolveFilterTokens',\n\tconfig: 'resolveManifestSentinels / resolveFilterTokens',\n\tobject: 'resolveFilterTokens',\n\tworkspace: 'resolveFilterTokens',\n\troute: 'resolveRouteSentinels',\n\tdeclarative: 'fetchAggregate (OpenRegister server-side)',\n\tvisibleWhen: 'evaluateVisibleWhen (utils/visibleWhen.js)',\n\tanswer: 'resolveAssignValue (utils/formAssign.js)',\n\trow: 'CnFieldListWidget (rowActions request)',\n\tsubject: 'CnBannerWidget (action route)',\n\tcontext: 'detailWidgetVisible (utils/detailWidgetVisibility.js)',\n})\n\n/**\n * Process-wide set of deprecated tokens already warned about, so the same\n * deprecated token referenced from many pages warns once. Cleared by\n * {@link clearDeprecationWarnings} (test-only).\n *\n * @type {Set<string>}\n */\nconst _warnedDeprecations = new Set()\n\n/**\n * Reset the one-time deprecation-warning dedup set. Test-only.\n *\n * @return {void}\n */\nexport function clearDeprecationWarnings() {\n\t_warnedDeprecations.clear()\n}\n\n/**\n * Emit a one-time deprecation warning for `token` if it is on the deprecation\n * map. Idempotent per token for the process lifetime.\n *\n * @param {string} token The `@`-prefixed token.\n * @param {(...args: unknown[]) => void} warn console.warn override (tests).\n * @return {boolean} True when the token is deprecated (regardless of whether a\n * warning was emitted this call).\n */\nexport function warnIfDeprecated(token, warn) {\n\tconst dep = matchDeprecation(token)\n\tif (!dep) {\n\t\treturn false\n\t}\n\tif (!_warnedDeprecations.has(token)) {\n\t\t_warnedDeprecations.add(token)\n\t\tconst target = dep.replacement\n\t\t\t? `use '${dep.replacement}' instead`\n\t\t\t: 'it will be removed with no direct replacement'\n\t\twarn(`[resolveManifestTokens] Deprecated sentinel token '${token}' — ${target} `\n\t\t\t+ `(removal ${dep.removal}). ${dep.note}`)\n\t}\n\treturn true\n}\n\n/**\n * Resolve every sentinel token in a manifest subtree through the single\n * dispatch, routing each by context to its existing resolver.\n *\n * Behaviour is the composition of the existing resolvers, so this is a\n * drop-in for a caller that previously ran `resolveRouteSentinels` then a\n * `resolveFilterTokens`-style pass by hand.\n *\n * @param {unknown} value The subtree (typically a `pages[].config` block). Not mutated.\n * @param {object} [opts] Resolution inputs.\n * @param {object} [opts.params] vue-router params for `@route.<param>`.\n * @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object}} [opts.ctx]\n * Context forwarded to {@link resolveFilterValue} for filter / object /\n * workspace / `@config.<key>` tokens.\n * @param {string} [opts.pageId] Page id for route-resolver warning dedup.\n * @param {(...args: unknown[]) => void} [opts.warn] console.warn override (tests).\n * @return {{value: unknown, unresolved: string[]}} The resolved subtree plus the\n * list of tokens that stayed unresolved (excluding OPTIONAL `?` tokens, which\n * are meant to be dropped, not waited on).\n */\nexport function resolveManifestSubtree(value, opts = {}) {\n\tconst params = opts.params && typeof opts.params === 'object' ? opts.params : {}\n\tconst ctx = opts.ctx && typeof opts.ctx === 'object' ? opts.ctx : {}\n\tconst pageId = opts.pageId || '<unknown>'\n\tconst warn = opts.warn ?? ((...args) => {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(...args)\n\t})\n\n\t// Pass 1: route context (reuses the reference-preserving route walker,\n\t// including its own per-page unresolved warning).\n\tconst afterRoute = resolveRouteSentinels(value, params, pageId)\n\n\t// Pass 2: filter / object / workspace / @config.<key> contexts, per leaf,\n\t// via the existing resolveFilterValue. Deprecated tokens warn once.\n\tconst unresolved = []\n\tconst walk = (node) => {\n\t\tif (typeof node === 'string') {\n\t\t\tif (!looksLikeSentinel(node)) {\n\t\t\t\treturn node\n\t\t\t}\n\t\t\twarnIfDeprecated(node, warn)\n\t\t\tconst resolved = resolveFilterValue(node, ctx)\n\t\t\tif (resolved === node && !isOptionalUnresolved(node)) {\n\t\t\t\t// Still a raw token: unresolved unless it is a load-time / server-side\n\t\t\t\t// context this render-pass deliberately leaves alone.\n\t\t\t\tconst c = contextOf(node)\n\t\t\t\tif (c !== 'config' && c !== 'declarative' && c !== 'visibleWhen') {\n\t\t\t\t\tunresolved.push(node)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn resolved\n\t\t}\n\t\tif (Array.isArray(node)) {\n\t\t\treturn node.map(walk)\n\t\t}\n\t\tif (node && typeof node === 'object') {\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(node)) {\n\t\t\t\tout[k] = walk(v)\n\t\t\t}\n\t\t\treturn out\n\t\t}\n\t\treturn node\n\t}\n\n\treturn { value: walk(afterRoute), unresolved }\n}\n\nexport { SENTINEL_CONTEXTS }\n"],"names":[],"mappings":"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"}
|
|
@@ -89,6 +89,9 @@ const SENTINEL_TOKEN_PATTERNS = Object.freeze({
|
|
|
89
89
|
// A field of the record a start-here attention card names (`@subject.<field>`, dot path),
|
|
90
90
|
// read by the banner widget's action routes.
|
|
91
91
|
subject: '^@subject\\.[A-Za-z][A-Za-z0-9_.]*$',
|
|
92
|
+
// A record a detail page reads beside the loaded one (`visibilityContext`), read by a
|
|
93
|
+
// grid widget's or a tab's `visibleWhen` as `@context.<name>.<field>` (dot path).
|
|
94
|
+
context: '^@context\\.[A-Za-z][A-Za-z0-9_.]*$',
|
|
92
95
|
// Deprecated-but-not-yet-removed tokens. Kept in the schema union so
|
|
93
96
|
// deployed manifests still validate during the migration window; the gate
|
|
94
97
|
// downgrades them to a WARN (see SENTINEL_DEPRECATIONS).
|
|
@@ -112,6 +115,7 @@ const SENTINEL_CONTEXTS = Object.freeze([
|
|
|
112
115
|
'answer',
|
|
113
116
|
'row',
|
|
114
117
|
'subject',
|
|
118
|
+
'context',
|
|
115
119
|
]);
|
|
116
120
|
|
|
117
121
|
/**
|
|
@@ -172,6 +176,11 @@ const SENTINEL_VOCABULARY = Object.freeze({
|
|
|
172
176
|
description: 'A field of the record an attention card names (the first row of its ordered `visibleWhen` source query), read when the card builds an action\'s route. Dot paths are allowed (`@subject.owner.id`).',
|
|
173
177
|
members: ['@subject.<field>'],
|
|
174
178
|
},
|
|
179
|
+
context: {
|
|
180
|
+
resolver: 'CnDetailPage visibilityContext (detailWidgetVisibility), read by a grid widget\'s or a tab\'s visibleWhen',
|
|
181
|
+
description: 'A record the detail page reads beside the loaded one, named in `config.visibilityContext` (a case\'s case type). A `visibleWhen` field reads it as `@context.<name>.<field>`, dot paths allowed (`@context.caseType.category`).',
|
|
182
|
+
members: ['@context.<name>.<field>'],
|
|
183
|
+
},
|
|
175
184
|
});
|
|
176
185
|
|
|
177
186
|
/**
|