@conduction/nextcloud-vue 2.67.0 → 2.69.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/CnContextMenu/CnContextMenu.vue2.js +37 -26
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js +3 -3
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +21 -35
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +17 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +58 -16
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +87 -18
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +6 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +58 -48
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/defaultActions.js +6 -7
- package/dist/esm/components/CnIndexPage/defaultActions.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +33 -34
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js +3 -3
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +8 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +2 -2
- 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.vue2.js +70 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +23 -3
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +3 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useContextMenu.js +4 -2
- package/dist/esm/composables/useContextMenu.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +278 -11
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/store/plugins/dashboardLayouts.js +35 -1
- package/dist/esm/store/plugins/dashboardLayouts.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +137 -16
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/resolveRowActions.js +130 -0
- package/dist/esm/utils/resolveRowActions.js.map +1 -0
- package/dist/esm/utils/rowActionItem.js +104 -0
- package/dist/esm/utils/rowActionItem.js.map +1 -0
- package/dist/esm/utils/timelineEvents.js +44 -1
- package/dist/esm/utils/timelineEvents.js.map +1 -1
- package/dist/esm/utils/validateManifest.js +59 -3
- package/dist/esm/utils/validateManifest.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1251 -232
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +2 -2
- package/src/components/CnContextMenu/CnContextMenu.vue +40 -29
- package/src/components/CnDashboardPage/CnDashboardPage.md +2 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +34 -35
- package/src/components/CnDetailPage/CnDetailPage.md +6 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +67 -5
- package/src/components/CnFormDialog/CnFormDialog.vue +92 -18
- package/src/components/CnIndexPage/CnIndexPage.md +21 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +61 -51
- package/src/components/CnIndexPage/defaultActions.js +6 -7
- package/src/components/CnRowActions/CnRowActions.vue +36 -37
- package/src/components/CnSetupWizard/CnSetupWizard.vue +8 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +2 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +82 -3
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +3 -2
- package/src/composables/useContextMenu.js +4 -2
- package/src/mixins/pageViews.js +277 -10
- package/src/schemas/app-manifest-v2.schema.json +73 -9
- package/src/store/plugins/dashboardLayouts.js +34 -0
- package/src/utils/formatMetric.js +143 -15
- package/src/utils/resolveRowActions.js +127 -0
- package/src/utils/rowActionItem.js +100 -0
- package/src/utils/timelineEvents.js +43 -0
- package/src/utils/validateManifest.js +60 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pageViews.js","sources":["../../../src/mixins/pageViews.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Page views: a few views of one page, each its own widget grid, behind one\n * segmented control (\"My work | My team\").\n *\n * A page that mixes this in accepts `views` (each `{ id, label, icon?,\n * widgets, layout, emptyText? }`), `defaultView` and `viewsLabel`. It keeps\n * the chosen view, writes it to the address (`?view=<id>`) and to the\n * user's preferences (the server, mirrored in the browser), and provides `cnPageViews` so a greeting header widget on\n * the page can draw the switch instead of the page.\n *\n * Which view opens: the address, then the stored view, then `defaultView`,\n * then the first view. An id that names no view is ignored at every step.\n *\n * Used by CnDashboardPage and CnDetailPage. Each renders the chosen view's\n * `layout` with its own grid, so a view looks exactly like the page's own\n * grid.\n *\n * @mixin pageViews\n * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { readUserPreference, USER_PREFERENCE_STORAGE_PREFIX, writeUserPreference } from '../composables/useUserPreferences.js'\n\n/** The query parameter that carries the chosen view. */\nexport const PAGE_VIEW_QUERY_KEY = 'view'\n\n/** Prefix of the user-preference key; the page id follows it. */\nexport const PAGE_VIEW_PREFERENCE_PREFIX = 'cn_page_view:'\n\nlet regionSeq = 0\n\n/**\n * Read the browser mirror of a user preference synchronously, so the stored\n * view is on screen from the first frame. Errors read as nothing stored.\n *\n * @param {string} appId The app id.\n * @param {string} key The preference key.\n * @return {string|null} The stored id, or null.\n * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n */\nfunction readStoredViewMirror(appId, key) {\n\ttry {\n\t\tif (typeof localStorage === 'undefined') {\n\t\t\treturn null\n\t\t}\n\t\tconst raw = localStorage.getItem(`${USER_PREFERENCE_STORAGE_PREFIX}${appId}:${key}`)\n\t\tif (raw === null || raw === undefined) {\n\t\t\treturn null\n\t\t}\n\t\tconst value = JSON.parse(raw)\n\t\treturn typeof value === 'string' && value !== '' ? value : null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nexport const pageViews = {\n\tinject: {\n\t\t/** The host app id, provided by CnAppRoot; keys the stored view. */\n\t\tcnPageViewsAppId: { from: 'cnAppId', default: '' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Views of this page, each its own widget grid behind one segmented\n\t\t * control. Each is `{ id, label, icon?, widgets, layout, emptyText? }`:\n\t\t * `widgets` and `layout` have the same shape as the page's own. The\n\t\t * chosen view renders in a region below the page's own grid. Empty\n\t\t * (the default) draws no switch and no region.\n\t\t *\n\t\t * @type {Array<{id: string, label: string, icon?: string, widgets?: Array<object>, layout?: Array<object>, emptyText?: string}>}\n\t\t */\n\t\tviews: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The id of the view chosen when neither the address nor the browser\n\t\t * store names one. Empty chooses the first view.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdefaultView: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch (through the host translate\n\t\t * function). Empty reads \"View\".\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tviewsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\tregionSeq += 1\n\t\treturn {\n\t\t\t/** The view the user chose in this session, or the stored one. */\n\t\t\tselectedViewId: null,\n\t\t\t/** Whether the user chose a view in this session (a late server read then leaves it alone). */\n\t\t\tviewChosen: false,\n\t\t\t/** Id of the view region, the target of the options' `aria-controls`. */\n\t\t\tviewRegionId: `cn-page-view-region-${regionSeq}`,\n\t\t}\n\t},\n\n\t/**\n\t * Provide the views to descendants, so a greeting header widget in the\n\t * page's grid can draw the switch. Getters keep it reactive.\n\t *\n\t * @return {object} The provided `cnPageViews`.\n\t */\n\tprovide() {\n\t\tconst vm = this\n\t\treturn {\n\t\t\tcnPageViews: {\n\t\t\t\tget views() { return vm.normalizedViews },\n\t\t\t\tget activeId() { return vm.activeViewId },\n\t\t\t\tget regionId() { return vm.viewRegionId },\n\t\t\t\tselect: (id) => vm.selectView(id),\n\t\t\t},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The declared views in one shape, labels translated. Entries without\n\t\t * an id or a label are dropped, as are repeated ids.\n\t\t *\n\t\t * @return {Array<{id: string, label: string, icon: string, widgets: Array<object>, layout: Array<object>, emptyText: string}>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n\t\t */\n\t\tnormalizedViews() {\n\t\t\tconst translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\tconst seen = new Set()\n\t\t\treturn (Array.isArray(this.views) ? this.views : [])\n\t\t\t\t.filter((view) => {\n\t\t\t\t\tif (!view || typeof view !== 'object' || view.id === undefined || view.id === null || view.id === '') {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof view.label !== 'string' || view.label === '' || seen.has(String(view.id))) {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tseen.add(String(view.id))\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t\t.map((view) => ({\n\t\t\t\t\tid: String(view.id),\n\t\t\t\t\tlabel: translate(view.label),\n\t\t\t\t\ticon: typeof view.icon === 'string' ? view.icon : '',\n\t\t\t\t\twidgets: Array.isArray(view.widgets) ? view.widgets : [],\n\t\t\t\t\tlayout: Array.isArray(view.layout) ? view.layout : [],\n\t\t\t\t\temptyText: typeof view.emptyText === 'string' && view.emptyText !== '' ? translate(view.emptyText) : '',\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether this page has views.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasViews() {\n\t\t\treturn this.normalizedViews.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The chosen view's id: the address, then this session's or the\n\t\t * stored choice, then `defaultView`, then the first view.\n\t\t *\n\t\t * @return {string|null} The id, or null without views.\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tactiveViewId() {\n\t\t\tconst views = this.normalizedViews\n\t\t\tif (views.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = (id) => id !== null && id !== undefined && views.some((view) => view.id === String(id))\n\t\t\tconst fromAddress = this.$route && this.$route.query ? this.$route.query[PAGE_VIEW_QUERY_KEY] : undefined\n\t\t\tif (known(fromAddress)) {\n\t\t\t\treturn String(fromAddress)\n\t\t\t}\n\t\t\tif (known(this.selectedViewId)) {\n\t\t\t\treturn String(this.selectedViewId)\n\t\t\t}\n\t\t\tif (known(this.defaultView)) {\n\t\t\t\treturn this.defaultView\n\t\t\t}\n\t\t\treturn views[0].id\n\t\t},\n\n\t\t/**\n\t\t * The chosen view, or null without views.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveView() {\n\t\t\treturn this.normalizedViews.find((view) => view.id === this.activeViewId) || null\n\t\t},\n\n\t\t/**\n\t\t * The switch's options, for CnSegmentedControl.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewSwitchOptions() {\n\t\t\treturn this.normalizedViews.map((view) => ({ value: view.id, label: view.label }))\n\t\t},\n\n\t\t/**\n\t\t * The switch's accessible name: `viewsLabel` translated, else \"View\".\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t */\n\t\tviewSwitchLabel() {\n\t\t\tconst translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\treturn this.viewsLabel ? translate(this.viewsLabel) : t('nextcloud-vue', 'Views')\n\t\t},\n\n\t\t/**\n\t\t * Whether a greeting header in the page's own widgets draws the\n\t\t * switch (an option with a `view`), so the page draws none.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views\n\t\t */\n\t\tviewSwitchInHeaderWidget() {\n\t\t\tconst widgets = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn widgets.some((def) => {\n\t\t\t\tconst options = def && def.content && def.content.views && def.content.views.options\n\t\t\t\treturn Array.isArray(options) && options.some((option) => option && typeof option.view === 'string' && option.view !== '')\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the page draws the switch itself.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsPageViewSwitch() {\n\t\t\treturn this.hasViews && !this.viewSwitchInHeaderWidget\n\t\t},\n\n\t\t/**\n\t\t * The user-preference key for this page's chosen view.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewPreferenceKey() {\n\t\t\t// The page id, else the route name: a detail page's fallback id is\n\t\t\t// its record's title, which would remember a view per record.\n\t\t\tconst id = this.pageId || (this.$route && this.$route.name) || this.resolvedPageId || 'page'\n\t\t\treturn `${PAGE_VIEW_PREFERENCE_PREFIX}${id}`\n\t\t},\n\n\t\t/**\n\t\t * The app id the stored view is kept under: the page's `appId` prop\n\t\t * when it has one, else the app root's.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewPreferenceAppId() {\n\t\t\tconst own = typeof this.appId === 'string' ? this.appId : ''\n\t\t\treturn own || (typeof this.cnPageViewsAppId === 'string' ? this.cnPageViewsAppId : '')\n\t\t},\n\n\t\t/**\n\t\t * The message of the chosen view's empty state.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewEmptyText() {\n\t\t\tif (this.activeView && this.activeView.emptyText) {\n\t\t\t\treturn this.activeView.emptyText\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'This view has no widgets yet.')\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadStoredView()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the stored view: the browser mirror at once, then the user\n\t\t * preference on the server, which wins unless the user already chose.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tasync loadStoredView() {\n\t\t\tif (!this.hasViews) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst appId = this.viewPreferenceAppId\n\t\t\tconst key = this.viewPreferenceKey\n\t\t\tthis.selectedViewId = readStoredViewMirror(appId, key)\n\t\t\tif (!appId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stored = await readUserPreference(appId, key, null)\n\t\t\tif (!this.viewChosen && typeof stored === 'string' && stored !== '') {\n\t\t\t\tthis.selectedViewId = stored\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Choose a view: keep it, store it, and write it to the address\n\t\t * (replacing the history entry, keeping the other query keys).\n\t\t *\n\t\t * @param {string} id The view id.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tselectView(id) {\n\t\t\tconst view = this.normalizedViews.find((candidate) => candidate.id === String(id))\n\t\t\tif (!view) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.selectedViewId = view.id\n\t\t\tthis.viewChosen = true\n\t\t\twriteUserPreference(this.viewPreferenceAppId, this.viewPreferenceKey, view.id)\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst query = this.$route.query || {}\n\t\t\tif (query[PAGE_VIEW_QUERY_KEY] === view.id) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = this.$router.replace({ query: { ...query, [PAGE_VIEW_QUERY_KEY]: view.id } })\n\t\t\tif (result && typeof result.catch === 'function') {\n\t\t\t\tresult.catch(() => {})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Look a widget definition up in the views, for a layout item of the\n\t\t * chosen view.\n\t\t *\n\t\t * @param {string} widgetId The widget id.\n\t\t * @return {object|null} The definition, or null.\n\t\t */\n\t\tfindViewWidget(widgetId) {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn view.widgets.find((def) => def && def.id === widgetId) || null\n\t\t},\n\n\t\t/**\n\t\t * Write a drag or resize in the chosen view's grid back onto its\n\t\t * layout items in place, as the page does for its own layout, so the\n\t\t * in-app manifest editor sees it.\n\t\t *\n\t\t * @param {Array<object>} updated The layout from the grid.\n\t\t * @return {void}\n\t\t */\n\t\tonViewLayoutChange(updated) {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view || !Array.isArray(updated)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor (const u of updated) {\n\t\t\t\tconst item = view.layout.find((l) => String(l.id) === String(u.id))\n\t\t\t\tif (!item) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor (const key of ['gridX', 'gridY', 'gridWidth', 'gridHeight']) {\n\t\t\t\t\tif (u[key] !== undefined) {\n\t\t\t\t\t\titem[key] = u[key]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * Emitted when a widget in a view's grid is dragged or resized.\n\t\t\t *\n\t\t\t * @event view-layout-change\n\t\t\t * @type {{view: string, layout: Array<object>}}\n\t\t\t */\n\t\t\tthis.$emit('view-layout-change', { view: view.id, layout: view.layout })\n\t\t},\n\t},\n}\n"],"names":["translate","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pageViews.js","sources":["../../../src/mixins/pageViews.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Page views: a few views of one page, each its own widget grid, behind one\n * segmented control (\"My work | My team\").\n *\n * A page that mixes this in accepts `views` (each `{ id, label, icon?,\n * widgets, layout, emptyText? }`), `defaultView` and `viewsLabel`. It keeps\n * the chosen view, writes it to the address (`?view=<id>`) and to the\n * user's preferences (the server, mirrored in the browser), and provides `cnPageViews` so a greeting header widget on\n * the page can draw the switch instead of the page.\n *\n * Which view opens: the address, then the stored view, then `defaultView`,\n * then the first view. An id that names no view is ignored at every step.\n *\n * Used by CnDashboardPage and CnDetailPage. Each renders the chosen view's\n * `layout` with its own grid, so a view looks exactly like the page's own\n * grid.\n *\n * With `userLayout` on, a user arranges each view's grid for themselves, as\n * they arrange the dashboard's own grid. The arrangement is stored per view,\n * per page and per user, through the same user-preference record the\n * dashboard uses (the server, mirrored in the browser). The manifest stays\n * the base: it decides which widgets a view has, the user decides where they\n * sit, and a reset returns the view to the manifest.\n *\n * @mixin pageViews\n * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { readUserPreference, USER_PREFERENCE_STORAGE_PREFIX, writeUserPreference } from '../composables/useUserPreferences.js'\nimport { dashboardLayoutKey, LAYOUT_GEOMETRY_FIELDS, mergeUserLayout, resolveUserLayoutApi } from '../store/plugins/dashboardLayouts.js'\n\n/** The query parameter that carries the chosen view. */\nexport const PAGE_VIEW_QUERY_KEY = 'view'\n\n/** Prefix of the user-preference key; the page id follows it. */\nexport const PAGE_VIEW_PREFERENCE_PREFIX = 'cn_page_view:'\n\n/**\n * The page id one view's user layout is stored under: the page's own id,\n * then `.view.`, then the view id. Spelled once, because a read and a write\n * that disagree about the key read as an arrangement that never persists.\n *\n * @param {string} pageKey The page's id.\n * @param {string} viewId The view id.\n * @return {string} The page id handed to the layout store.\n * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n */\nexport function pageViewLayoutId(pageKey, viewId) {\n\treturn `${String(pageKey || 'page')}.view.${String(viewId)}`\n}\n\nlet regionSeq = 0\n\n/**\n * Read the browser mirror of a user preference synchronously, so a stored\n * value is on screen from the first frame. Errors read as nothing stored.\n *\n * @param {string} appId The app id.\n * @param {string} key The preference key.\n * @return {object|string|number|boolean|Array|null} The stored value, or null.\n * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n */\nfunction readPreferenceMirror(appId, key) {\n\ttry {\n\t\tif (typeof localStorage === 'undefined') {\n\t\t\treturn null\n\t\t}\n\t\tconst raw = localStorage.getItem(`${USER_PREFERENCE_STORAGE_PREFIX}${appId}:${key}`)\n\t\tif (raw === null || raw === undefined) {\n\t\t\treturn null\n\t\t}\n\t\treturn JSON.parse(raw)\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Read the browser mirror of the stored view synchronously, so the stored\n * view is on screen from the first frame.\n *\n * @param {string} appId The app id.\n * @param {string} key The preference key.\n * @return {string|null} The stored id, or null.\n * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n */\nfunction readStoredViewMirror(appId, key) {\n\tconst value = readPreferenceMirror(appId, key)\n\treturn typeof value === 'string' && value !== '' ? value : null\n}\n\n/**\n * Whether a stored layout record holds an arrangement.\n *\n * @param {object|Array<object>|null} record The record, or its items.\n * @return {boolean}\n */\nfunction holdsArrangement(record) {\n\tconst items = Array.isArray(record) ? record : (record && Array.isArray(record.items) ? record.items : [])\n\treturn items.length > 0\n}\n\nexport const pageViews = {\n\tinject: {\n\t\t/** The host app id, provided by CnAppRoot; keys the stored view. */\n\t\tcnPageViewsAppId: { from: 'cnAppId', default: '' },\n\t},\n\n\temits: ['view-layout-change', 'view-layout-reset'],\n\n\tprops: {\n\t\t/**\n\t\t * Views of this page, each its own widget grid behind one segmented\n\t\t * control. Each is `{ id, label, icon?, widgets, layout, emptyText? }`:\n\t\t * `widgets` and `layout` have the same shape as the page's own. The\n\t\t * chosen view renders in a region below the page's own grid. Empty\n\t\t * (the default) draws no switch and no region.\n\t\t *\n\t\t * @type {Array<{id: string, label: string, icon?: string, widgets?: Array<object>, layout?: Array<object>, emptyText?: string}>}\n\t\t */\n\t\tviews: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The id of the view chosen when neither the address nor the browser\n\t\t * store names one. Empty chooses the first view.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdefaultView: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the view switch (through the host translate\n\t\t * function). Empty reads \"View\".\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tviewsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\tregionSeq += 1\n\t\treturn {\n\t\t\t/** The view the user chose in this session, or the stored one. */\n\t\t\tselectedViewId: null,\n\t\t\t/** Whether the user chose a view in this session (a late server read then leaves it alone). */\n\t\t\tviewChosen: false,\n\t\t\t/** Id of the view region, the target of the options' `aria-controls`. */\n\t\t\tviewRegionId: `cn-page-view-region-${regionSeq}`,\n\t\t\t/**\n\t\t\t * This user's arrangement of each view, by view id, when the page\n\t\t\t * keeps a user layout. A separate copy, never the view's `layout`:\n\t\t\t * that array is the manifest, and writing a user's drag into it\n\t\t\t * would rearrange the view for everybody.\n\t\t\t *\n\t\t\t * @type {Record<string, Array<object>>}\n\t\t\t */\n\t\t\tviewUserLayouts: {},\n\t\t\t/** The views whose arrangement changed since the last save, by id. */\n\t\t\tviewUserLayoutsDirty: {},\n\t\t}\n\t},\n\n\t/**\n\t * Provide the views to descendants, so a greeting header widget in the\n\t * page's grid can draw the switch. Getters keep it reactive.\n\t *\n\t * @return {object} The provided `cnPageViews`.\n\t */\n\tprovide() {\n\t\tconst vm = this\n\t\treturn {\n\t\t\tcnPageViews: {\n\t\t\t\tget views() { return vm.normalizedViews },\n\t\t\t\tget activeId() { return vm.activeViewId },\n\t\t\t\tget regionId() { return vm.viewRegionId },\n\t\t\t\tselect: (id) => vm.selectView(id),\n\t\t\t},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The declared views in one shape, labels translated. Entries without\n\t\t * an id or a label are dropped, as are repeated ids.\n\t\t *\n\t\t * @return {Array<{id: string, label: string, icon: string, widgets: Array<object>, layout: Array<object>, emptyText: string}>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n\t\t */\n\t\tnormalizedViews() {\n\t\t\tconst translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\tconst seen = new Set()\n\t\t\treturn (Array.isArray(this.views) ? this.views : [])\n\t\t\t\t.filter((view) => {\n\t\t\t\t\tif (!view || typeof view !== 'object' || view.id === undefined || view.id === null || view.id === '') {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tif (typeof view.label !== 'string' || view.label === '' || seen.has(String(view.id))) {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tseen.add(String(view.id))\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t\t.map((view) => ({\n\t\t\t\t\tid: String(view.id),\n\t\t\t\t\tlabel: translate(view.label),\n\t\t\t\t\ticon: typeof view.icon === 'string' ? view.icon : '',\n\t\t\t\t\twidgets: Array.isArray(view.widgets) ? view.widgets : [],\n\t\t\t\t\tlayout: Array.isArray(view.layout) ? view.layout : [],\n\t\t\t\t\temptyText: typeof view.emptyText === 'string' && view.emptyText !== '' ? translate(view.emptyText) : '',\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether this page has views.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasViews() {\n\t\t\treturn this.normalizedViews.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The chosen view's id: the address, then this session's or the\n\t\t * stored choice, then `defaultView`, then the first view.\n\t\t *\n\t\t * @return {string|null} The id, or null without views.\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tactiveViewId() {\n\t\t\tconst views = this.normalizedViews\n\t\t\tif (views.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = (id) => id !== null && id !== undefined && views.some((view) => view.id === String(id))\n\t\t\tconst fromAddress = this.$route && this.$route.query ? this.$route.query[PAGE_VIEW_QUERY_KEY] : undefined\n\t\t\tif (known(fromAddress)) {\n\t\t\t\treturn String(fromAddress)\n\t\t\t}\n\t\t\tif (known(this.selectedViewId)) {\n\t\t\t\treturn String(this.selectedViewId)\n\t\t\t}\n\t\t\tif (known(this.defaultView)) {\n\t\t\t\treturn this.defaultView\n\t\t\t}\n\t\t\treturn views[0].id\n\t\t},\n\n\t\t/**\n\t\t * The chosen view, or null without views.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveView() {\n\t\t\treturn this.normalizedViews.find((view) => view.id === this.activeViewId) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a user arranges the views for themselves (`userLayout`).\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\tviewUserLayoutOn() {\n\t\t\treturn this.userLayout === true && this.hasViews\n\t\t},\n\n\t\t/**\n\t\t * The chosen view's layout: this user's arrangement when they have\n\t\t * one, the manifest's otherwise.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\tactiveViewLayout() {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst arranged = this.viewUserLayoutOn ? this.viewUserLayouts[view.id] : null\n\t\t\treturn Array.isArray(arranged) ? arranged : view.layout\n\t\t},\n\n\t\t/**\n\t\t * The switch's options, for CnSegmentedControl.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\tviewSwitchOptions() {\n\t\t\treturn this.normalizedViews.map((view) => ({ value: view.id, label: view.label }))\n\t\t},\n\n\t\t/**\n\t\t * The switch's accessible name: `viewsLabel` translated, else \"View\".\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible\n\t\t */\n\t\tviewSwitchLabel() {\n\t\t\tconst translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\treturn this.viewsLabel ? translate(this.viewsLabel) : t('nextcloud-vue', 'Views')\n\t\t},\n\n\t\t/**\n\t\t * Whether a greeting header in the page's own widgets draws the\n\t\t * switch (an option with a `view`), so the page draws none.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views\n\t\t */\n\t\tviewSwitchInHeaderWidget() {\n\t\t\tconst widgets = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn widgets.some((def) => {\n\t\t\t\tconst options = def && def.content && def.content.views && def.content.views.options\n\t\t\t\treturn Array.isArray(options) && options.some((option) => option && typeof option.view === 'string' && option.view !== '')\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the page draws the switch itself.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsPageViewSwitch() {\n\t\t\treturn this.hasViews && !this.viewSwitchInHeaderWidget\n\t\t},\n\n\t\t/**\n\t\t * The user-preference key for this page's chosen view.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewPreferenceKey() {\n\t\t\t// The page id, else the route name: a detail page's fallback id is\n\t\t\t// its record's title, which would remember a view per record.\n\t\t\tconst id = this.pageId || (this.$route && this.$route.name) || this.resolvedPageId || 'page'\n\t\t\treturn `${PAGE_VIEW_PREFERENCE_PREFIX}${id}`\n\t\t},\n\n\t\t/**\n\t\t * The app id the stored view is kept under: the page's `appId` prop\n\t\t * when it has one, else the app root's.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewPreferenceAppId() {\n\t\t\tconst own = typeof this.appId === 'string' ? this.appId : ''\n\t\t\treturn own || (typeof this.cnPageViewsAppId === 'string' ? this.cnPageViewsAppId : '')\n\t\t},\n\n\t\t/**\n\t\t * The message of the chosen view's empty state.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewEmptyText() {\n\t\t\tif (this.activeView && this.activeView.emptyText) {\n\t\t\t\treturn this.activeView.emptyText\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'This view has no widgets yet.')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/**\n\t\t * Read the user's arrangement of a view the first time it is shown.\n\t\t *\n\t\t * @param {string|null} id The chosen view id.\n\t\t * @return {void}\n\t\t */\n\t\tactiveViewId(id) {\n\t\t\tthis.loadViewUserLayout(id)\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadStoredView()\n\t\tthis.loadViewUserLayout(this.activeViewId)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the stored view: the browser mirror at once, then the user\n\t\t * preference on the server, which wins unless the user already chose.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tasync loadStoredView() {\n\t\t\tif (!this.hasViews) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst appId = this.viewPreferenceAppId\n\t\t\tconst key = this.viewPreferenceKey\n\t\t\tthis.selectedViewId = readStoredViewMirror(appId, key)\n\t\t\tif (!appId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst stored = await readUserPreference(appId, key, null)\n\t\t\tif (!this.viewChosen && typeof stored === 'string' && stored !== '') {\n\t\t\t\tthis.selectedViewId = stored\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Choose a view: keep it, store it, and write it to the address\n\t\t * (replacing the history entry, keeping the other query keys).\n\t\t *\n\t\t * @param {string} id The view id.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered\n\t\t */\n\t\tselectView(id) {\n\t\t\tconst view = this.normalizedViews.find((candidate) => candidate.id === String(id))\n\t\t\tif (!view) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.selectedViewId = view.id\n\t\t\tthis.viewChosen = true\n\t\t\twriteUserPreference(this.viewPreferenceAppId, this.viewPreferenceKey, view.id)\n\t\t\tif (!this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst query = this.$route.query || {}\n\t\t\tif (query[PAGE_VIEW_QUERY_KEY] === view.id) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = this.$router.replace({ query: { ...query, [PAGE_VIEW_QUERY_KEY]: view.id } })\n\t\t\tif (result && typeof result.catch === 'function') {\n\t\t\t\tresult.catch(() => {})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Look a widget definition up in the views, for a layout item of the\n\t\t * chosen view.\n\t\t *\n\t\t * @param {string} widgetId The widget id.\n\t\t * @return {object|null} The definition, or null.\n\t\t */\n\t\tfindViewWidget(widgetId) {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn view.widgets.find((def) => def && def.id === widgetId) || null\n\t\t},\n\n\t\t/**\n\t\t * The page id a view's arrangement is stored under.\n\t\t *\n\t\t * @param {string} viewId The view id.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\tviewLayoutPageId(viewId) {\n\t\t\tconst pageKey = this.viewPreferenceKey.slice(PAGE_VIEW_PREFERENCE_PREFIX.length)\n\t\t\treturn pageViewLayoutId(pageKey, viewId)\n\t\t},\n\n\t\t/**\n\t\t * The layout store calls: the page's own (`userLayoutApi`, so an\n\t\t * injected `userLayoutStore` covers the views too), else the user\n\t\t * preferences.\n\t\t *\n\t\t * @return {{load: (appId: string, pageId: string) => Promise<object|null>, save: (appId: string, pageId: string, layout: Array<object>) => Promise<boolean>, reset: (appId: string, pageId: string) => Promise<boolean>}}\n\t\t */\n\t\tviewLayoutApi() {\n\t\t\tif (typeof this.userLayoutApi === 'function') {\n\t\t\t\treturn this.userLayoutApi()\n\t\t\t}\n\t\t\treturn resolveUserLayoutApi(null)\n\t\t},\n\n\t\t/**\n\t\t * Read this user's arrangement of one view, once: the browser mirror\n\t\t * at once, then the server, which wins unless the user already moved\n\t\t * something in that view. A page without `userLayout` reads nothing.\n\t\t *\n\t\t * @param {string|null} viewId The view id.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\tasync loadViewUserLayout(viewId) {\n\t\t\tif (!this.viewUserLayoutOn || viewId === null || viewId === undefined) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst view = this.normalizedViews.find((candidate) => candidate.id === String(viewId))\n\t\t\tif (!view || Array.isArray(this.viewUserLayouts[view.id])) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst appId = this.viewPreferenceAppId\n\t\t\tconst pageId = this.viewLayoutPageId(view.id)\n\t\t\tconst mirror = readPreferenceMirror(appId, dashboardLayoutKey(pageId))\n\t\t\tthis.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: mergeUserLayout(view.layout, holdsArrangement(mirror) ? mirror : null) }\n\t\t\tif (!appId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet record\n\t\t\ttry {\n\t\t\t\trecord = await this.viewLayoutApi().load(appId, pageId)\n\t\t\t} catch {\n\t\t\t\t// The mirror, or the manifest, stands: a store that cannot\n\t\t\t\t// answer leaves the view as the user last saw it.\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.viewUserLayoutsDirty[view.id] || !holdsArrangement(record)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: mergeUserLayout(view.layout, record) }\n\t\t},\n\n\t\t/**\n\t\t * Store the arrangement of every view the user changed since the last\n\t\t * save. The page calls it when the user leaves edit mode, so a drag\n\t\t * is not a write per pixel gesture.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\tasync saveViewUserLayouts() {\n\t\t\tif (!this.viewUserLayoutOn) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ids = Object.keys(this.viewUserLayoutsDirty).filter((id) => this.viewUserLayoutsDirty[id])\n\t\t\tthis.viewUserLayoutsDirty = {}\n\t\t\tconst appId = this.viewPreferenceAppId\n\t\t\tfor (const id of ids) {\n\t\t\t\tconst pageId = this.viewLayoutPageId(id)\n\t\t\t\tconst items = this.viewUserLayouts[id] || []\n\t\t\t\ttry {\n\t\t\t\t\tif (appId) {\n\t\t\t\t\t\tawait this.viewLayoutApi().save(appId, pageId, items)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No app to address: keep it in this browser only, as\n\t\t\t\t\t\t// the chosen view is kept.\n\t\t\t\t\t\tawait writeUserPreference('', dashboardLayoutKey(pageId), { items: items.map((item) => this.viewGeometry(item, true)) })\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// The arrangement stands for this session. A toast on a\n\t\t\t\t\t// failed layout write is noise on an instance without the\n\t\t\t\t\t// preference route.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop this user's arrangement of one view and return it to the\n\t\t * manifest layout. Other views keep theirs.\n\t\t *\n\t\t * @param {string} [viewId] The view id; the chosen view when left out.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-resetting-a-view-restores-its-manifest-layout\n\t\t */\n\t\tasync resetViewUserLayout(viewId) {\n\t\t\tconst id = viewId === undefined || viewId === null ? this.activeViewId : String(viewId)\n\t\t\tconst view = this.normalizedViews.find((candidate) => candidate.id === id)\n\t\t\tif (!this.viewUserLayoutOn || !view) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst appId = this.viewPreferenceAppId\n\t\t\tconst pageId = this.viewLayoutPageId(view.id)\n\t\t\ttry {\n\t\t\t\tif (appId) {\n\t\t\t\t\tawait this.viewLayoutApi().reset(appId, pageId)\n\t\t\t\t} else {\n\t\t\t\t\tawait writeUserPreference('', dashboardLayoutKey(pageId), { items: [] })\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// The view returns to the manifest either way, which is what\n\t\t\t\t// the user asked for.\n\t\t\t}\n\t\t\tconst dirty = { ...this.viewUserLayoutsDirty }\n\t\t\tdelete dirty[view.id]\n\t\t\tthis.viewUserLayoutsDirty = dirty\n\t\t\tthis.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: mergeUserLayout(view.layout, null) }\n\t\t\t/**\n\t\t\t * Emitted when the user drops their arrangement of a view and it\n\t\t\t * returns to the manifest layout.\n\t\t\t *\n\t\t\t * @event view-layout-reset\n\t\t\t * @type {{view: string}}\n\t\t\t */\n\t\t\tthis.$emit('view-layout-reset', { view: view.id })\n\t\t},\n\n\t\t/**\n\t\t * The geometry fields of one layout item, and nothing else.\n\t\t *\n\t\t * @param {object} item A layout item, or a grid update.\n\t\t * @param {boolean} [withWidgetId] Whether to keep the widget id too.\n\t\t * @return {object}\n\t\t */\n\t\tviewGeometry(item, withWidgetId = false) {\n\t\t\tconst geometry = withWidgetId ? { widgetId: String(item?.widgetId ?? '') } : {}\n\t\t\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\t\t\tif (item?.[field] !== undefined) {\n\t\t\t\t\tgeometry[field] = item[field]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn geometry\n\t\t},\n\n\t\t/**\n\t\t * A drag or resize in the chosen view's grid. With `userLayout` it\n\t\t * moves this user's copy of the view and marks it for saving; the\n\t\t * view's manifest `layout` is not touched. Without it the geometry is\n\t\t * written back onto the layout items in place, as the page does for\n\t\t * its own layout, so the in-app manifest editor sees it.\n\t\t *\n\t\t * @param {Array<object>} updated The layout from the grid.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\tonViewLayoutChange(updated) {\n\t\t\tconst view = this.activeView\n\t\t\tif (!view || !Array.isArray(updated)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.viewUserLayoutOn) {\n\t\t\t\tconst current = Array.isArray(this.viewUserLayouts[view.id]) ? this.viewUserLayouts[view.id] : mergeUserLayout(view.layout, null)\n\t\t\t\tconst arranged = current.map((item) => {\n\t\t\t\t\tconst u = updated.find((candidate) => String(candidate.id) === String(item.id))\n\t\t\t\t\treturn u ? { ...item, ...this.viewGeometry(u) } : item\n\t\t\t\t})\n\t\t\t\tthis.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: arranged }\n\t\t\t\tthis.viewUserLayoutsDirty = { ...this.viewUserLayoutsDirty, [view.id]: true }\n\t\t\t\tthis.$emit('view-layout-change', { view: view.id, layout: arranged })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor (const u of updated) {\n\t\t\t\tconst item = view.layout.find((l) => String(l.id) === String(u.id))\n\t\t\t\tif (!item) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor (const key of ['gridX', 'gridY', 'gridWidth', 'gridHeight']) {\n\t\t\t\t\tif (u[key] !== undefined) {\n\t\t\t\t\t\titem[key] = u[key]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * Emitted when a widget in a view's grid is dragged or resized.\n\t\t\t *\n\t\t\t * @event view-layout-change\n\t\t\t * @type {{view: string, layout: Array<object>}}\n\t\t\t */\n\t\t\tthis.$emit('view-layout-change', { view: view.id, layout: view.layout })\n\t\t},\n\t},\n}\n"],"names":["translate","t"],"mappings":"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"}
|
|
@@ -154,6 +154,40 @@ function mergeUserLayout(manifestLayout, record) {
|
|
|
154
154
|
return [...arranged, ...appended]
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The three calls a user layout needs: from a store that carries the
|
|
159
|
+
* `dashboardLayoutsPlugin` actions, or straight over user preferences.
|
|
160
|
+
*
|
|
161
|
+
* Both paths write through `writeUserPreference`, so a record lands on the
|
|
162
|
+
* server and in the browser mirror whichever one a page uses. CnDashboardPage
|
|
163
|
+
* reads its own grid's record through this, and the page views mixin reads
|
|
164
|
+
* each view's record through it, so the two cannot disagree about where a
|
|
165
|
+
* user's arrangement lives.
|
|
166
|
+
*
|
|
167
|
+
* @param {object|null} [store] A store with `loadDashboardLayout`, `saveDashboardLayout` and `resetDashboardLayout`, or null.
|
|
168
|
+
* @return {{ load: (appId: string, pageId: string) => Promise<object|null>, save: (appId: string, pageId: string, layout: Array<object>) => Promise<boolean>, reset: (appId: string, pageId: string) => Promise<boolean> }} The api.
|
|
169
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
170
|
+
*/
|
|
171
|
+
function resolveUserLayoutApi(store = null) {
|
|
172
|
+
if (store && typeof store.loadDashboardLayout === 'function') {
|
|
173
|
+
return {
|
|
174
|
+
load: (...a) => store.loadDashboardLayout(...a),
|
|
175
|
+
save: (...a) => store.saveDashboardLayout(...a),
|
|
176
|
+
reset: (...a) => store.resetDashboardLayout(...a),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
load: (appId, pageId) => readUserPreference(appId, dashboardLayoutKey(pageId), null),
|
|
182
|
+
save: (appId, pageId, layout) => writeUserPreference(
|
|
183
|
+
appId,
|
|
184
|
+
dashboardLayoutKey(pageId),
|
|
185
|
+
{ items: (Array.isArray(layout) ? layout : []).map(geometryOf).filter((entry) => entry.widgetId !== '') },
|
|
186
|
+
),
|
|
187
|
+
reset: (appId, pageId) => writeUserPreference(appId, dashboardLayoutKey(pageId), { items: [] }),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
157
191
|
/**
|
|
158
192
|
* Plugin definition.
|
|
159
193
|
*
|
|
@@ -259,5 +293,5 @@ function dashboardLayoutsPlugin(options = {}) {
|
|
|
259
293
|
}
|
|
260
294
|
}
|
|
261
295
|
|
|
262
|
-
export { LAYOUT_GEOMETRY_FIELDS, dashboardLayoutKey, dashboardLayoutsPlugin, mergeUserLayout };
|
|
296
|
+
export { LAYOUT_GEOMETRY_FIELDS, dashboardLayoutKey, dashboardLayoutsPlugin, mergeUserLayout, resolveUserLayoutApi };
|
|
263
297
|
//# sourceMappingURL=dashboardLayouts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboardLayouts.js","sources":["../../../../src/store/plugins/dashboardLayouts.js"],"sourcesContent":["/**\n * Per-user dashboard layouts for the object store.\n *\n * A manifest dashboard is the same page for everybody. This plugin lets one\n * user keep their own arrangement of it without changing what anybody else\n * sees: the manifest layout stays the base and the reset target, and the\n * user's record is a thin overlay of geometry on top of it.\n *\n * 🔴 THE MANIFEST WINS ON MEMBERSHIP, THE USER WINS ON GEOMETRY. A widget the\n * admin removes disappears for everyone, stored record or not; a widget the\n * admin adds appears for everyone, at the end of the user's grid. What the\n * user owns is where their widgets sit, not which widgets exist. Reading it\n * the other way round would let a stored record resurrect a widget the admin\n * deliberately took off the page, and nothing on screen would say why it was\n * back.\n *\n * 🔴 IT WRITES THROUGH `writeUserPreference`, NOT A SECOND HTTP PATH. That\n * helper already addresses the app's preference route, mirrors to the browser\n * so a layout survives an instance without the endpoint, and refuses an SPA\n * shell answering 200 with HTML. A second client here would be a second answer\n * about where a user's settings live.\n *\n * State added:\n * - `dashboardLayouts` — stored records by page id\n *\n * Actions added:\n * - `loadDashboardLayout(appId, pageId)` — read this user's record\n * - `saveDashboardLayout(appId, pageId, layout)` — store the geometry\n * - `resetDashboardLayout(appId, pageId)` — drop the record\n *\n * @example\n * import { createObjectStore, dashboardLayoutsPlugin } from '@conduction/nextcloud-vue'\n *\n * const useMyStore = createObjectStore('myapp', {\n * plugins: [dashboardLayoutsPlugin()],\n * })\n *\n * const store = useMyStore()\n * await store.loadDashboardLayout('dossiq', 'Dashboard')\n * await store.saveDashboardLayout('dossiq', 'Dashboard', grid)\n *\n * @return {object} Plugin definition\n */\n\nimport {\n\treadUserPreference,\n\twriteUserPreference,\n} from '../../composables/useUserPreferences.js'\n\n/**\n * The preference key one page's layout is stored under.\n *\n * Keyed by page so two dashboards in one app do not overwrite each other. The\n * prefix is spelled once here because a read and a write that disagree about\n * the key read as a layout that never persists.\n *\n * @param {string} pageId The manifest page id.\n * @return {string} The preference key.\n */\nexport function dashboardLayoutKey(pageId) {\n\treturn 'dashboard-layout.' + String(pageId || '')\n}\n\n/**\n * The geometry fields a stored record keeps.\n *\n * Only these five travel. A record that carried the whole layout item would\n * carry the widget's title, its style and its config, and the next manifest\n * change would be silently overridden by a copy the user never edited.\n *\n * @type {Array<string>}\n */\nexport const LAYOUT_GEOMETRY_FIELDS = ['gridX', 'gridY', 'gridWidth', 'gridHeight']\n\n/**\n * One layout item reduced to what a user record stores.\n *\n * @param {object} item The layout item.\n * @return {object} The stored entry.\n */\nfunction geometryOf(item) {\n\tconst entry = { widgetId: String(item?.widgetId ?? '') }\n\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\tif (item?.[field] !== undefined) {\n\t\t\tentry[field] = item[field]\n\t\t}\n\t}\n\n\treturn entry\n}\n\n/**\n * Merge a user's stored record over the manifest layout.\n *\n * The rule, in the order it is applied:\n *\n * 1. The manifest layout is the base. Every widget it declares is present and\n * every widget it does not declare is absent, whatever the record says.\n * 2. A widget with a stored entry takes that entry's geometry.\n * 3. A widget with no stored entry keeps its manifest position, so a page that\n * gains a widget shows it rather than hiding it until the user next edits.\n * 4. The user's own order is kept for the widgets they arranged, and widgets\n * the admin added since are appended after them.\n *\n * Pure on purpose: this is the rule the whole change turns on, and a rule that\n * can only be exercised by mounting a grid is a rule nobody tests at the edges.\n *\n * @param {Array<object>} manifestLayout The layout the manifest ships.\n * @param {object|Array<object>|null} record The user's stored record, or its items.\n * @return {Array<object>} The merged layout.\n */\nexport function mergeUserLayout(manifestLayout, record) {\n\tconst base = Array.isArray(manifestLayout) ? manifestLayout : []\n\tconst items = Array.isArray(record) ? record : (record?.items || [])\n\tif (!Array.isArray(items) || items.length === 0) {\n\t\treturn [...base]\n\t}\n\n\tconst stored = new Map()\n\tfor (const entry of items) {\n\t\tconst widgetId = String(entry?.widgetId ?? '')\n\t\tif (widgetId !== '') {\n\t\t\tstored.set(widgetId, entry)\n\t\t}\n\t}\n\n\tconst arranged = []\n\tconst appended = []\n\n\tfor (const item of base) {\n\t\tconst widgetId = String(item?.widgetId ?? '')\n\t\tconst entry = stored.get(widgetId)\n\t\tif (!entry) {\n\t\t\t// A widget the admin added since the user last arranged the page.\n\t\t\t// It goes after what they arranged rather than into the middle of\n\t\t\t// it, where it would push their own grid around.\n\t\t\tappended.push({ ...item })\n\t\t\tcontinue\n\t\t}\n\n\t\tconst merged = { ...item }\n\t\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\t\tif (entry[field] !== undefined) {\n\t\t\t\tmerged[field] = entry[field]\n\t\t\t}\n\t\t}\n\t\tarranged.push(merged)\n\t}\n\n\t// The user's own order, for the widgets they arranged.\n\tarranged.sort((a, b) => {\n\t\tconst order = [...stored.keys()]\n\t\treturn order.indexOf(String(a.widgetId)) - order.indexOf(String(b.widgetId))\n\t})\n\n\treturn [...arranged, ...appended]\n}\n\n/**\n * Plugin definition.\n *\n * @param {object} [options] Options.\n * @param {object} [options.http] An axios-shaped client, for tests.\n * @param {object} [options.storage] A localStorage-shaped store, for tests.\n * @return {object} Plugin definition\n */\nexport function dashboardLayoutsPlugin(options = {}) {\n\treturn {\n\t\tname: 'dashboardLayouts',\n\n\t\tstate: () => ({\n\t\t\t/**\n\t\t\t * Stored layout records by page id.\n\t\t\t *\n\t\t\t * @type {Record<string, object|null>}\n\t\t\t */\n\t\t\tdashboardLayouts: {},\n\t\t}),\n\n\t\tactions: {\n\t\t\t/**\n\t\t\t * Read this user's stored layout for one page.\n\t\t\t *\n\t\t\t * Answers null when there is none, which is what a page with no\n\t\t\t * user record must show: the manifest layout, untouched. An empty\n\t\t\t * array would read the same to a caller that only checks length,\n\t\t\t * so null is returned and the difference is stated.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<object|null>} The record, or null.\n\t\t\t */\n\t\t\tasync loadDashboardLayout(appId, pageId) {\n\t\t\t\tconst record = await readUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\tnull,\n\t\t\t\t\toptions,\n\t\t\t\t)\n\n\t\t\t\tconst items = Array.isArray(record) ? record : record?.items\n\t\t\t\tconst stored = Array.isArray(items) && items.length > 0\n\t\t\t\t\t? { items }\n\t\t\t\t\t: null\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: stored }\n\n\t\t\t\treturn stored\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Store this user's geometry for one page.\n\t\t\t *\n\t\t\t * Only the geometry is stored, never the whole layout item: a\n\t\t\t * record carrying a widget's title and config would override the\n\t\t\t * next manifest change with a copy the user never edited.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @param {Array<object>} layout The layout as it now stands.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync saveDashboardLayout(appId, pageId, layout) {\n\t\t\t\tconst items = (Array.isArray(layout) ? layout : [])\n\t\t\t\t\t.map(geometryOf)\n\t\t\t\t\t.filter((entry) => entry.widgetId !== '')\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: { items } }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop this user's record, returning the page to the manifest.\n\t\t\t *\n\t\t\t * An empty record is written rather than a delete, because the\n\t\t\t * preference route answers one verb for a value and a written\n\t\t\t * empty reads back as \"no arrangement\" on every instance,\n\t\t\t * including one whose endpoint has no delete at all.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync resetDashboardLayout(appId, pageId) {\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: null }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items: [] },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dashboardLayouts.js","sources":["../../../../src/store/plugins/dashboardLayouts.js"],"sourcesContent":["/**\n * Per-user dashboard layouts for the object store.\n *\n * A manifest dashboard is the same page for everybody. This plugin lets one\n * user keep their own arrangement of it without changing what anybody else\n * sees: the manifest layout stays the base and the reset target, and the\n * user's record is a thin overlay of geometry on top of it.\n *\n * 🔴 THE MANIFEST WINS ON MEMBERSHIP, THE USER WINS ON GEOMETRY. A widget the\n * admin removes disappears for everyone, stored record or not; a widget the\n * admin adds appears for everyone, at the end of the user's grid. What the\n * user owns is where their widgets sit, not which widgets exist. Reading it\n * the other way round would let a stored record resurrect a widget the admin\n * deliberately took off the page, and nothing on screen would say why it was\n * back.\n *\n * 🔴 IT WRITES THROUGH `writeUserPreference`, NOT A SECOND HTTP PATH. That\n * helper already addresses the app's preference route, mirrors to the browser\n * so a layout survives an instance without the endpoint, and refuses an SPA\n * shell answering 200 with HTML. A second client here would be a second answer\n * about where a user's settings live.\n *\n * State added:\n * - `dashboardLayouts` — stored records by page id\n *\n * Actions added:\n * - `loadDashboardLayout(appId, pageId)` — read this user's record\n * - `saveDashboardLayout(appId, pageId, layout)` — store the geometry\n * - `resetDashboardLayout(appId, pageId)` — drop the record\n *\n * @example\n * import { createObjectStore, dashboardLayoutsPlugin } from '@conduction/nextcloud-vue'\n *\n * const useMyStore = createObjectStore('myapp', {\n * plugins: [dashboardLayoutsPlugin()],\n * })\n *\n * const store = useMyStore()\n * await store.loadDashboardLayout('dossiq', 'Dashboard')\n * await store.saveDashboardLayout('dossiq', 'Dashboard', grid)\n *\n * @return {object} Plugin definition\n */\n\nimport {\n\treadUserPreference,\n\twriteUserPreference,\n} from '../../composables/useUserPreferences.js'\n\n/**\n * The preference key one page's layout is stored under.\n *\n * Keyed by page so two dashboards in one app do not overwrite each other. The\n * prefix is spelled once here because a read and a write that disagree about\n * the key read as a layout that never persists.\n *\n * @param {string} pageId The manifest page id.\n * @return {string} The preference key.\n */\nexport function dashboardLayoutKey(pageId) {\n\treturn 'dashboard-layout.' + String(pageId || '')\n}\n\n/**\n * The geometry fields a stored record keeps.\n *\n * Only these five travel. A record that carried the whole layout item would\n * carry the widget's title, its style and its config, and the next manifest\n * change would be silently overridden by a copy the user never edited.\n *\n * @type {Array<string>}\n */\nexport const LAYOUT_GEOMETRY_FIELDS = ['gridX', 'gridY', 'gridWidth', 'gridHeight']\n\n/**\n * One layout item reduced to what a user record stores.\n *\n * @param {object} item The layout item.\n * @return {object} The stored entry.\n */\nfunction geometryOf(item) {\n\tconst entry = { widgetId: String(item?.widgetId ?? '') }\n\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\tif (item?.[field] !== undefined) {\n\t\t\tentry[field] = item[field]\n\t\t}\n\t}\n\n\treturn entry\n}\n\n/**\n * Merge a user's stored record over the manifest layout.\n *\n * The rule, in the order it is applied:\n *\n * 1. The manifest layout is the base. Every widget it declares is present and\n * every widget it does not declare is absent, whatever the record says.\n * 2. A widget with a stored entry takes that entry's geometry.\n * 3. A widget with no stored entry keeps its manifest position, so a page that\n * gains a widget shows it rather than hiding it until the user next edits.\n * 4. The user's own order is kept for the widgets they arranged, and widgets\n * the admin added since are appended after them.\n *\n * Pure on purpose: this is the rule the whole change turns on, and a rule that\n * can only be exercised by mounting a grid is a rule nobody tests at the edges.\n *\n * @param {Array<object>} manifestLayout The layout the manifest ships.\n * @param {object|Array<object>|null} record The user's stored record, or its items.\n * @return {Array<object>} The merged layout.\n */\nexport function mergeUserLayout(manifestLayout, record) {\n\tconst base = Array.isArray(manifestLayout) ? manifestLayout : []\n\tconst items = Array.isArray(record) ? record : (record?.items || [])\n\tif (!Array.isArray(items) || items.length === 0) {\n\t\treturn [...base]\n\t}\n\n\tconst stored = new Map()\n\tfor (const entry of items) {\n\t\tconst widgetId = String(entry?.widgetId ?? '')\n\t\tif (widgetId !== '') {\n\t\t\tstored.set(widgetId, entry)\n\t\t}\n\t}\n\n\tconst arranged = []\n\tconst appended = []\n\n\tfor (const item of base) {\n\t\tconst widgetId = String(item?.widgetId ?? '')\n\t\tconst entry = stored.get(widgetId)\n\t\tif (!entry) {\n\t\t\t// A widget the admin added since the user last arranged the page.\n\t\t\t// It goes after what they arranged rather than into the middle of\n\t\t\t// it, where it would push their own grid around.\n\t\t\tappended.push({ ...item })\n\t\t\tcontinue\n\t\t}\n\n\t\tconst merged = { ...item }\n\t\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\t\tif (entry[field] !== undefined) {\n\t\t\t\tmerged[field] = entry[field]\n\t\t\t}\n\t\t}\n\t\tarranged.push(merged)\n\t}\n\n\t// The user's own order, for the widgets they arranged.\n\tarranged.sort((a, b) => {\n\t\tconst order = [...stored.keys()]\n\t\treturn order.indexOf(String(a.widgetId)) - order.indexOf(String(b.widgetId))\n\t})\n\n\treturn [...arranged, ...appended]\n}\n\n/**\n * The three calls a user layout needs: from a store that carries the\n * `dashboardLayoutsPlugin` actions, or straight over user preferences.\n *\n * Both paths write through `writeUserPreference`, so a record lands on the\n * server and in the browser mirror whichever one a page uses. CnDashboardPage\n * reads its own grid's record through this, and the page views mixin reads\n * each view's record through it, so the two cannot disagree about where a\n * user's arrangement lives.\n *\n * @param {object|null} [store] A store with `loadDashboardLayout`, `saveDashboardLayout` and `resetDashboardLayout`, or null.\n * @return {{ load: (appId: string, pageId: string) => Promise<object|null>, save: (appId: string, pageId: string, layout: Array<object>) => Promise<boolean>, reset: (appId: string, pageId: string) => Promise<boolean> }} The api.\n * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n */\nexport function resolveUserLayoutApi(store = null) {\n\tif (store && typeof store.loadDashboardLayout === 'function') {\n\t\treturn {\n\t\t\tload: (...a) => store.loadDashboardLayout(...a),\n\t\t\tsave: (...a) => store.saveDashboardLayout(...a),\n\t\t\treset: (...a) => store.resetDashboardLayout(...a),\n\t\t}\n\t}\n\n\treturn {\n\t\tload: (appId, pageId) => readUserPreference(appId, dashboardLayoutKey(pageId), null),\n\t\tsave: (appId, pageId, layout) => writeUserPreference(\n\t\t\tappId,\n\t\t\tdashboardLayoutKey(pageId),\n\t\t\t{ items: (Array.isArray(layout) ? layout : []).map(geometryOf).filter((entry) => entry.widgetId !== '') },\n\t\t),\n\t\treset: (appId, pageId) => writeUserPreference(appId, dashboardLayoutKey(pageId), { items: [] }),\n\t}\n}\n\n/**\n * Plugin definition.\n *\n * @param {object} [options] Options.\n * @param {object} [options.http] An axios-shaped client, for tests.\n * @param {object} [options.storage] A localStorage-shaped store, for tests.\n * @return {object} Plugin definition\n */\nexport function dashboardLayoutsPlugin(options = {}) {\n\treturn {\n\t\tname: 'dashboardLayouts',\n\n\t\tstate: () => ({\n\t\t\t/**\n\t\t\t * Stored layout records by page id.\n\t\t\t *\n\t\t\t * @type {Record<string, object|null>}\n\t\t\t */\n\t\t\tdashboardLayouts: {},\n\t\t}),\n\n\t\tactions: {\n\t\t\t/**\n\t\t\t * Read this user's stored layout for one page.\n\t\t\t *\n\t\t\t * Answers null when there is none, which is what a page with no\n\t\t\t * user record must show: the manifest layout, untouched. An empty\n\t\t\t * array would read the same to a caller that only checks length,\n\t\t\t * so null is returned and the difference is stated.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<object|null>} The record, or null.\n\t\t\t */\n\t\t\tasync loadDashboardLayout(appId, pageId) {\n\t\t\t\tconst record = await readUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\tnull,\n\t\t\t\t\toptions,\n\t\t\t\t)\n\n\t\t\t\tconst items = Array.isArray(record) ? record : record?.items\n\t\t\t\tconst stored = Array.isArray(items) && items.length > 0\n\t\t\t\t\t? { items }\n\t\t\t\t\t: null\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: stored }\n\n\t\t\t\treturn stored\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Store this user's geometry for one page.\n\t\t\t *\n\t\t\t * Only the geometry is stored, never the whole layout item: a\n\t\t\t * record carrying a widget's title and config would override the\n\t\t\t * next manifest change with a copy the user never edited.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @param {Array<object>} layout The layout as it now stands.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync saveDashboardLayout(appId, pageId, layout) {\n\t\t\t\tconst items = (Array.isArray(layout) ? layout : [])\n\t\t\t\t\t.map(geometryOf)\n\t\t\t\t\t.filter((entry) => entry.widgetId !== '')\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: { items } }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop this user's record, returning the page to the manifest.\n\t\t\t *\n\t\t\t * An empty record is written rather than a delete, because the\n\t\t\t * preference route answers one verb for a value and a written\n\t\t\t * empty reads back as \"no arrangement\" on every instance,\n\t\t\t * including one whose endpoint has no delete at all.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync resetDashboardLayout(appId, pageId) {\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: null }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items: [] },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCanonicalLocale } from '@nextcloud/l10n';
|
|
1
2
|
import { resolveFilterValue } from './resolveFilterTokens.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -26,20 +27,25 @@ function unwrapAppConfig(cnAppConfig) {
|
|
|
26
27
|
* Resolve `@config.<key>` tokens in a `content.format` spec's string fields.
|
|
27
28
|
*
|
|
28
29
|
* Each of `currency` / `prefix` / `suffix` may be a `@config.<key>` token (e.g.
|
|
29
|
-
* `currency: '@config.currency'`)
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* `currency: '@config.currency'`), or an `@object.<field>` token read off the
|
|
31
|
+
* detail page's object when `object` is given. Tokens are resolved against
|
|
32
|
+
* `configCtx`; a token that stays unresolved (the config key is unset, or the
|
|
33
|
+
* config map has not been injected yet during a route transition) is DROPPED
|
|
34
|
+
* so the downstream literal default applies instead of a raw `@config.…`
|
|
35
|
+
* string leaking into `Intl.NumberFormat`.
|
|
34
36
|
*
|
|
35
37
|
* @param {object} format The raw `content.format` spec.
|
|
36
38
|
* @param {object} configCtx The page-level app-config map (from `unwrapAppConfig`).
|
|
39
|
+
* @param {object|null} [object] The detail page's object, for `@object.<field>` tokens.
|
|
37
40
|
* @return {object} A copy of `format` with its string tokens resolved/dropped.
|
|
38
41
|
*/
|
|
39
|
-
function resolveConfigFormat(format, configCtx) {
|
|
42
|
+
function resolveConfigFormat(format, configCtx, object = null) {
|
|
40
43
|
const fmt = format || {};
|
|
41
44
|
const out = { ...fmt };
|
|
42
|
-
const ctx = {
|
|
45
|
+
const ctx = {
|
|
46
|
+
config: (configCtx && typeof configCtx === 'object') ? configCtx : {},
|
|
47
|
+
object: (object && typeof object === 'object') ? object : null,
|
|
48
|
+
};
|
|
43
49
|
for (const key of ['currency', 'prefix', 'suffix']) {
|
|
44
50
|
const raw = fmt[key];
|
|
45
51
|
if (typeof raw !== 'string' || raw.charAt(0) !== '@') {
|
|
@@ -69,6 +75,114 @@ function safeCurrencyCode(currency) {
|
|
|
69
75
|
: 'EUR'
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Whether a value is a three-letter currency code.
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown} value The candidate.
|
|
82
|
+
* @return {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function isCurrencyCode(value) {
|
|
85
|
+
return typeof value === 'string' && /^[A-Za-z]{3}$/.test(value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read a dot-path off an object.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} object The object.
|
|
92
|
+
* @param {string} path The path, e.g. `currency` or `price.currency`.
|
|
93
|
+
* @return {unknown} The value, or undefined.
|
|
94
|
+
*/
|
|
95
|
+
function readPath(object, path) {
|
|
96
|
+
return String(path).split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), object)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The currency a money value is shown in. In order:
|
|
101
|
+
*
|
|
102
|
+
* 1. `currencyField`: the field of the page's object that holds the currency
|
|
103
|
+
* (a detail page; e.g. a contract's own `currency`).
|
|
104
|
+
* 2. `currency`: a literal code, or a token (`@config.currency`,
|
|
105
|
+
* `@object.currency`).
|
|
106
|
+
* 3. The app's reporting currency, `currency` in the page's app config.
|
|
107
|
+
* 4. `EUR`.
|
|
108
|
+
*
|
|
109
|
+
* A step whose value is not a three-letter code is skipped, so an object
|
|
110
|
+
* without a currency falls back to the reporting currency instead of
|
|
111
|
+
* throwing in `Intl.NumberFormat`.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} format The format spec (`{ currency?, currencyField? }`).
|
|
114
|
+
* @param {object} configCtx The page-level app-config map.
|
|
115
|
+
* @param {object|null} [object] The detail page's object.
|
|
116
|
+
* @return {string} An upper-case three-letter code.
|
|
117
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
118
|
+
*/
|
|
119
|
+
function resolveFormatCurrency(format, configCtx, object = null) {
|
|
120
|
+
const fmt = format || {};
|
|
121
|
+
if (typeof fmt.currencyField === 'string' && fmt.currencyField !== '' && object && typeof object === 'object') {
|
|
122
|
+
const own = readPath(object, fmt.currencyField);
|
|
123
|
+
if (isCurrencyCode(own)) {
|
|
124
|
+
return own.toUpperCase()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const declared = resolveConfigFormat({ currency: fmt.currency }, configCtx, object).currency;
|
|
128
|
+
if (isCurrencyCode(declared)) {
|
|
129
|
+
return declared.toUpperCase()
|
|
130
|
+
}
|
|
131
|
+
const reporting = configCtx && typeof configCtx === 'object' ? configCtx.currency : undefined;
|
|
132
|
+
return safeCurrencyCode(reporting)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The format spec of a stat or stats-block entry in one shape.
|
|
137
|
+
*
|
|
138
|
+
* `format` may be a style name (`"currency"`) or an object (`{ style,
|
|
139
|
+
* currency, currencyField, decimals, prefix, suffix }`). A sibling `currency`
|
|
140
|
+
* or `currencyField` on the entry itself fills the same keys, and either one
|
|
141
|
+
* makes the style `currency` when none is named. Answers null when the entry
|
|
142
|
+
* declares no format at all, so the caller keeps its own rendering.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} entry The declaration (`{ format?, currency?, currencyField? }`).
|
|
145
|
+
* @return {object|null} The format spec, or null.
|
|
146
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value
|
|
147
|
+
*/
|
|
148
|
+
function normalizeMetricFormat(entry) {
|
|
149
|
+
const source = entry && typeof entry === 'object' ? entry : {};
|
|
150
|
+
let fmt = null;
|
|
151
|
+
if (typeof source.format === 'string' && source.format !== '') {
|
|
152
|
+
fmt = { style: source.format };
|
|
153
|
+
} else if (source.format && typeof source.format === 'object' && !Array.isArray(source.format)) {
|
|
154
|
+
fmt = { ...source.format };
|
|
155
|
+
}
|
|
156
|
+
for (const key of ['currency', 'currencyField']) {
|
|
157
|
+
if (typeof source[key] === 'string' && source[key] !== '') {
|
|
158
|
+
fmt = fmt || {};
|
|
159
|
+
if (fmt[key] === undefined) {
|
|
160
|
+
fmt[key] = source[key];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (fmt && !fmt.style && (fmt.currency || fmt.currencyField)) {
|
|
165
|
+
fmt.style = 'currency';
|
|
166
|
+
}
|
|
167
|
+
return fmt
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The user's locale for number formatting: the Nextcloud locale setting,
|
|
172
|
+
* else the browser's.
|
|
173
|
+
*
|
|
174
|
+
* @return {string|undefined} A BCP 47 tag, or undefined for the runtime default.
|
|
175
|
+
*/
|
|
176
|
+
function metricLocale() {
|
|
177
|
+
try {
|
|
178
|
+
const tag = getCanonicalLocale();
|
|
179
|
+
// A tag Intl does not accept would throw a RangeError on every value.
|
|
180
|
+
return tag && Intl.NumberFormat.supportedLocalesOf([tag]).length > 0 ? tag : undefined
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
72
186
|
/**
|
|
73
187
|
* Format a numeric value per a manifest `content.format` spec.
|
|
74
188
|
*
|
|
@@ -86,12 +200,18 @@ function safeCurrencyCode(currency) {
|
|
|
86
200
|
* - `decimal` is a plain number with ONE fraction digit by default (the
|
|
87
201
|
* fleet KPIs' `toFixed(1)` convention), where `number` defaults to 0.
|
|
88
202
|
*
|
|
203
|
+
* The currency follows `resolveFormatCurrency`: the object's own currency
|
|
204
|
+
* (`currencyField`), then `currency`, then the app's reporting currency, then
|
|
205
|
+
* EUR. Numbers are formatted in the user's locale (`metricLocale`).
|
|
206
|
+
*
|
|
89
207
|
* @param {unknown} value The raw value to format.
|
|
90
|
-
* @param {object} format The `content.format` spec (`{ style, currency, decimals, prefix, suffix }`).
|
|
208
|
+
* @param {object} format The `content.format` spec (`{ style, currency, currencyField, decimals, prefix, suffix }`).
|
|
91
209
|
* @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.
|
|
210
|
+
* @param {object|null} [object] The detail page's object, for `currencyField` and `@object.<field>`.
|
|
92
211
|
* @return {string} The formatted display string.
|
|
212
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-money-value-is-shown-in-the-currency-the-entry-names
|
|
93
213
|
*/
|
|
94
|
-
function formatMetricValue(value, format, configCtx) {
|
|
214
|
+
function formatMetricValue(value, format, configCtx, object = null) {
|
|
95
215
|
if (value === null || value === undefined) {
|
|
96
216
|
return '—'
|
|
97
217
|
}
|
|
@@ -100,7 +220,8 @@ function formatMetricValue(value, format, configCtx) {
|
|
|
100
220
|
return String(value)
|
|
101
221
|
}
|
|
102
222
|
|
|
103
|
-
const fmt = resolveConfigFormat(format, configCtx);
|
|
223
|
+
const fmt = resolveConfigFormat(format, configCtx, object);
|
|
224
|
+
const locale = metricLocale();
|
|
104
225
|
// `duration-hours` and `decimal` default to ONE fraction digit; the
|
|
105
226
|
// other styles keep the pre-existing default of 0.
|
|
106
227
|
const oneDigitDefault = fmt.style === 'duration-hours' || fmt.style === 'decimal';
|
|
@@ -108,26 +229,26 @@ function formatMetricValue(value, format, configCtx) {
|
|
|
108
229
|
|
|
109
230
|
let body;
|
|
110
231
|
if (fmt.style === 'currency') {
|
|
111
|
-
body = new Intl.NumberFormat(
|
|
232
|
+
body = new Intl.NumberFormat(locale, {
|
|
112
233
|
style: 'currency',
|
|
113
|
-
currency:
|
|
234
|
+
currency: resolveFormatCurrency(format, configCtx, object),
|
|
114
235
|
minimumFractionDigits: decimals,
|
|
115
236
|
maximumFractionDigits: decimals,
|
|
116
237
|
}).format(num);
|
|
117
238
|
} else if (fmt.style === 'percent') {
|
|
118
239
|
// Values are stored as the literal percent (83.3), not a 0–1 ratio.
|
|
119
|
-
body = new Intl.NumberFormat(
|
|
240
|
+
body = new Intl.NumberFormat(locale, {
|
|
120
241
|
minimumFractionDigits: decimals,
|
|
121
242
|
maximumFractionDigits: decimals,
|
|
122
243
|
}).format(num) + '%';
|
|
123
244
|
} else if (fmt.style === 'duration-hours') {
|
|
124
245
|
// An hour count (e.g. mean resolution time): `42.5h`.
|
|
125
|
-
body = new Intl.NumberFormat(
|
|
246
|
+
body = new Intl.NumberFormat(locale, {
|
|
126
247
|
minimumFractionDigits: decimals,
|
|
127
248
|
maximumFractionDigits: decimals,
|
|
128
249
|
}).format(num) + 'h';
|
|
129
250
|
} else {
|
|
130
|
-
body = new Intl.NumberFormat(
|
|
251
|
+
body = new Intl.NumberFormat(locale, {
|
|
131
252
|
minimumFractionDigits: decimals,
|
|
132
253
|
maximumFractionDigits: decimals,
|
|
133
254
|
}).format(num);
|
|
@@ -135,5 +256,5 @@ function formatMetricValue(value, format, configCtx) {
|
|
|
135
256
|
return `${fmt.prefix || ''}${body}${fmt.suffix || ''}`
|
|
136
257
|
}
|
|
137
258
|
|
|
138
|
-
export { formatMetricValue, resolveConfigFormat, safeCurrencyCode, unwrapAppConfig };
|
|
259
|
+
export { formatMetricValue, metricLocale, normalizeMetricFormat, resolveConfigFormat, resolveFormatCurrency, safeCurrencyCode, unwrapAppConfig };
|
|
139
260
|
//# 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 { resolveFilterValue } from './resolveFilterTokens.js'\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'`). Tokens are resolved against `configCtx`; a\n * token that stays unresolved (the config key is unset, or the config map has\n * not been injected yet during a route transition) is DROPPED so the downstream\n * literal default applies instead of a raw `@config.…` string leaking into\n * `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 * @return {object} A copy of `format` with its string tokens resolved/dropped.\n */\nexport function resolveConfigFormat(format, configCtx) {\n\tconst fmt = format || {}\n\tconst out = { ...fmt }\n\tconst ctx = { config: (configCtx && typeof configCtx === 'object') ? configCtx : {} }\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 * 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 * @param {unknown} value The raw value to format.\n * @param {object} format The `content.format` spec (`{ style, currency, decimals, prefix, suffix }`).\n * @param {object} configCtx The page-level app-config map for `@config.<key>` resolution.\n * @return {string} The formatted display string.\n */\nexport function formatMetricValue(value, format, configCtx) {\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)\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(undefined, {\n\t\t\tstyle: 'currency',\n\t\t\tcurrency: safeCurrencyCode(fmt.currency),\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(undefined, {\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(undefined, {\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(undefined, {\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":";;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,eAAe,CAAC,WAAW,EAAE;AAC7C,CAAC,MAAM,CAAC,GAAG,YAAW;AACtB,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,EAAC;AAC7E,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,GAAG,EAAE;AACrE,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE;AACvD,CAAC,MAAM,GAAG,GAAG,MAAM,IAAI,GAAE;AACzB,CAAC,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,GAAE;AACvB,CAAC,MAAM,GAAG,GAAG,EAAE,MAAM,EAAE,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,GAAG,EAAE,GAAE;AACtF,CAAC,KAAK,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE;AACrD,EAAE,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,EAAC;AACtB,EAAE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;AACxD,GAAG,QAAQ;AACX,GAAG;AACH,EAAE,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAC;AAC/C,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,SAAS,GAAG,SAAQ;AAChG,EAAE;AACF,CAAC,OAAO,GAAG;AACX,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,QAAQ,EAAE;AAC3C,CAAC,OAAO,CAAC,OAAO,QAAQ,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;AACvE,IAAI,QAAQ,CAAC,WAAW,EAAE;AAC1B,IAAI,KAAK;AACT,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;AAC5D,CAAC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE;AAC5C,EAAE,OAAO,GAAG;AACZ,EAAE;AACF,CAAC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAC;AAC1B,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;AAC5B,EAAE,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,EAAE;AACF;AACA,CAAC,MAAM,GAAG,GAAG,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAC;AACnD;AACA;AACA,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,KAAK,gBAAgB,IAAI,GAAG,CAAC,KAAK,KAAK,UAAS;AAClF,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC,QAAQ,IAAI,eAAe,GAAG,CAAC,GAAG,CAAC,EAAC;AAC1F;AACA,CAAC,IAAI,KAAI;AACT,CAAC,IAAI,GAAG,CAAC,KAAK,KAAK,UAAU,EAAE;AAC/B,EAAE,IAAI,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE;AAC1C,GAAG,KAAK,EAAE,UAAU;AACpB,GAAG,QAAQ,EAAE,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC;AAC3C,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,EAAC;AAChB,EAAE,MAAM,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE;AACrC;AACA,EAAE,IAAI,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE;AAC1C,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAG;AACtB,EAAE,MAAM,IAAI,GAAG,CAAC,KAAK,KAAK,gBAAgB,EAAE;AAC5C;AACA,EAAE,IAAI,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE;AAC1C,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAG;AACtB,EAAE,MAAM;AACR,EAAE,IAAI,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE;AAC1C,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,qBAAqB,EAAE,QAAQ;AAClC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,EAAC;AAChB,EAAE;AACF,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;AACvD;;;;"}
|
|
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 { 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 * 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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"}
|