@conduction/nextcloud-vue 2.56.0-beta.24 → 2.56.0-beta.26
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/CnAppNav/CnAppNav.vue2.js +19 -14
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +2 -2
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +77 -26
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +22 -14
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +23 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js +9 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js +107 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js +79 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +8 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +144 -6
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +14 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
- package/dist/esm/composables/useObjectLock.js +41 -10
- package/dist/esm/composables/useObjectLock.js.map +1 -1
- package/dist/esm/directives/cnSelectAria.js +55 -0
- package/dist/esm/directives/cnSelectAria.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +8 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +154 -2
- package/dist/esm/store/useObjectStore.js +37 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/cnFetch.js +17 -2
- package/dist/esm/utils/cnFetch.js.map +1 -1
- package/dist/esm/utils/objectSignals.js +44 -0
- package/dist/esm/utils/objectSignals.js.map +1 -0
- package/dist/esm/utils/resolveBrand.js +38 -0
- package/dist/esm/utils/resolveBrand.js.map +1 -0
- 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 +9640 -8649
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +154 -2
- package/l10n/en.json +7 -1
- package/l10n/nl.json +8 -2
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +21 -16
- package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
- package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
- package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
- package/src/components/CnCredentials/CnCredentials.vue +17 -16
- package/src/components/CnDetailPage/CnDetailPage.vue +8 -5
- package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
- package/src/components/CnFormPage/CnFormPage.vue +33 -0
- package/src/components/CnIndexPage/CnIndexPage.md +15 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +157 -18
- package/src/components/CnIndexPage/selfModeActions.js +89 -13
- package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
- package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
- package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
- package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
- package/src/composables/useObjectLock.js +41 -10
- package/src/css/form-field.css +1 -1
- package/src/css/look-board-index.css +35 -1
- package/src/directives/cnSelectAria.js +54 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +6 -1
- package/src/store/useObjectStore.js +37 -9
- package/src/utils/cnFetch.js +16 -1
- package/src/utils/index.js +1 -0
- package/src/utils/objectSignals.js +41 -0
- package/src/utils/resolveBrand.js +37 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnStatsBlock.vue2.js","sources":["../../../../src/components/CnStatsBlock/CnStatsBlock.vue"],"sourcesContent":["<!--\n - CnStatsBlock renders the CANONICAL KPI card (src/css/kpi-card.css) — the\n - same markup and the same stylesheet CnStatWidget renders, so OpenCatalogi's\n - KPIs and dossiq's are one design rather than two. This component's own\n - `cn-stats-block*` classes stay on the same elements for backwards\n - compatibility with app CSS that targets them; the LOOK comes from the\n - shared sheet.\n -\n - The comment lives OUT here rather than inside <template>: a comment node\n - beside the root element makes the component multi-root in Vue 3, and a\n - multi-root component has no root to put class attributes on — `rootClasses`\n - silently stopped reaching the DOM.\n-->\n<template>\n\t<component\n\t\t:is=\"componentTag\"\n\t\tclass=\"cn-kpi-card cn-stats-block\"\n\t\t:class=\"rootClasses\"\n\t\tv-bind=\"componentAttrs\"\n\t\t@click=\"onClick\">\n\t\t<!-- Icon -->\n\t\t<div v-if=\"hasIcon && !isStacked && !isBoardLook\" class=\"cn-kpi-card__icon cn-stats-block__icon\" :class=\"iconClasses\">\n\t\t\t<slot name=\"icon\">\n\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"iconSize\" />\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<div class=\"cn-kpi-card__body cn-stats-block__content\">\n\t\t\t<div class=\"cn-stats-block__header\">\n\t\t\t\t<!-- Board look: an 18px glyph before the label, only on a link tile. -->\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"isBoardLook && isInteractive && hasIcon\"\n\t\t\t\t\tclass=\"cn-kpi-card__glyph cn-stats-block__glyph\"\n\t\t\t\t\tdata-testid=\"cn-stats-block-glyph\"\n\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"18\" />\n\t\t\t\t\t</slot>\n\t\t\t\t</span>\n\t\t\t\t<h4 class=\"cn-kpi-card__title\" :title=\"title || undefined\">\n\t\t\t\t\t{{ title || t('nextcloud-vue', 'Objects') }}\n\t\t\t\t</h4>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"hasError\" class=\"cn-stats-block__count cn-stats-block__count--error\">\n\t\t\t\t<span class=\"cn-stats-block__count-value\">—</span>\n\t\t\t\t<span class=\"cn-stats-block__count-label\">{{ errorLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"hasValueSlot || count > 0 || (showZeroCount && count === 0)\" class=\"cn-kpi-card__value-row cn-stats-block__count\">\n\t\t\t\t<span class=\"cn-kpi-card__value cn-stats-block__count-value\">\n\t\t\t\t\t<!-- @slot Override the prominently-displayed value — render a pre-formatted string (currency, percent, a \"—\" placeholder, …). `count` stays the raw number; this is presentation only. Defaults to the localized count. -->\n\t\t\t\t\t<!-- @binding {number} count The raw numeric count. -->\n\t\t\t\t\t<!-- @binding {string} formatted The default localized count string. -->\n\t\t\t\t\t<slot name=\"value\" :count=\"count\" :formatted=\"formattedCount\">{{ formattedCount }}</slot>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-kpi-card__label cn-stats-block__count-label\">{{ countLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"loading\" class=\"cn-stats-block__loading\">\n\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t{{ loadingLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-stats-block__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Breakdown details -->\n\t\t\t<div\n\t\t\t\tv-if=\"hasError === false && breakdown && (hasValueSlot || count > 0 || showZeroCount)\"\n\t\t\t\tclass=\"cn-stats-block__breakdown\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(value, key) in breakdown\"\n\t\t\t\t\t:key=\"key\"\n\t\t\t\t\tclass=\"cn-stats-block__breakdown-item\">\n\t\t\t\t\t<span class=\"cn-stats-block__breakdown-label\">{{ formatBreakdownLabel(key) }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-stats-block__breakdown-value\"\n\t\t\t\t\t\t:class=\"'cn-stats-block__breakdown-value--' + key\">\n\t\t\t\t\t\t{{ value }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { normalizeLook } from '../../composables/useLook.js'\n\n// The canonical KPI look, shared with CnStatWidget. Imported here so the\n// card is styled even when the consuming app has not pulled in the\n// library's global css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.\n *\n * Renders the canonical KPI card: a large circular icon beside a left-aligned\n * number, drawing no box of its own because the wrapper around it already\n * draws one. Supports colour variants, icons and clickable state. Use in a\n * CnKpiGrid for responsive dashboard layouts.\n *\n * Basic (canonical horizontal card)\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" count-label=\"open cases\" />\n * ```\n *\n * With icon and variant\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * clickable\n * @click=\"goToCases\" />\n * ```\n *\n * Stacked and boxed — for a block mounted with no wrapper around it\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" vertical filled />\n * ```\n *\n * With route-based navigation (renders as <router-link>)\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * :route=\"{ name: 'Cases', query: { status: 'open' } }\" />\n * ```\n *\n * With breakdown\n * ```vue\n * <CnStatsBlock\n * title=\"Cases\"\n * :count=\"42\"\n * :breakdown=\"{ total: 100, invalid: 3, deleted: 5, published: 92 }\" />\n * ```\n *\n * Custom icon slot\n * ```vue\n * <CnStatsBlock title=\"Files\" :count=\"128\">\n * <template #icon>\n * <FileDocumentOutline :size=\"24\" />\n * </template>\n * </CnStatsBlock>\n * ```\n */\nexport default {\n\tname: 'CnStatsBlock',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The look the app is drawn in, provided by CnAppRoot.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile\n\t\t */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Block title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The main count number to display prominently */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Label displayed next to the count */\n\t\tcountLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'objects'),\n\t\t},\n\n\t\t/** Detailed breakdown object (key-value pairs) */\n\t\tbreakdown: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is currently loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/** Text shown when count is 0 */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/**\n\t\t * The tile could not load its number. Anything truthy counts — pass the\n\t\t * caught error itself, or just `true`.\n\t\t *\n\t\t * The tile then shows a dash and `errorLabel` INSTEAD of a count, and\n\t\t * tints itself `error`. This takes precedence over `count`, `loading`\n\t\t * and `emptyLabel` deliberately: a stale or defaulted number rendered\n\t\t * during a failure is the exact thing this prop exists to stop. A\n\t\t * dashboard showing 0 because the backend is down is worse than one\n\t\t * showing nothing, because 0 is a number a reader will believe.\n\t\t *\n\t\t * @type {boolean|string|Error|null}\n\t\t */\n\t\terror: {\n\t\t\t// TYPE ORDER IS SEMANTIC HERE. Vue casts an empty value to `true`\n\t\t\t// when Boolean appears BEFORE String in the union, so\n\t\t\t// `[Boolean, String]` would read `error=\"\"` as an error. A caller\n\t\t\t// clearing its message back to '' is reporting RECOVERY, and that\n\t\t\t// would have pinned the tile in its error state after the fetch\n\t\t\t// succeeded again. String first keeps '' falsy.\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- Boolean first would make `error=\"\"` truthy, which is the recovery signal.\n\t\t\ttype: [String, Boolean, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown in place of the count when `error` is set. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Unavailable'),\n\t\t},\n\n\t\t/** Icon component (e.g., imported MDI icon) */\n\t\ticon: {\n\t\t\ttype: [Object, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 24,\n\t\t},\n\n\t\t/** Color variant: 'default', 'primary', 'success', 'warning', 'error' */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Stack the icon above a centred number instead of placing it beside\n\t\t * one. The canonical KPI card is horizontal, so this is the opt-out;\n\t\t * `horizontal` below is kept only for callers that already pass it.\n\t\t */\n\t\tvertical: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * `stacked` is the board's tile, as CnStatWidget `content.layout:\n\t\t * \"stacked\"`: no icon circle, a 14px muted title, the number at\n\t\t * 34px/700 in the text colour. Empty (the default) keeps the KPI card.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lay the icon left of the content. No longer needed — this is the\n\t\t * canonical card's own layout — and kept so existing callers that pass\n\t\t * `horizontal` keep working. Pass `vertical` to stack instead.\n\t\t *\n\t\t * @deprecated since 2.25.0, the horizontal layout is the default.\n\t\t */\n\t\thorizontal: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the card's own grey box. Off by default: a stats block is\n\t\t * normally rendered inside a wrapper that already draws a card, and a\n\t\t * second box reads as a card inside a card. Turn it on for a block\n\t\t * mounted with no wrapper around it.\n\t\t */\n\t\tfilled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the card is clickable */\n\t\tclickable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to display 0 as a count value instead of the empty label */\n\t\tshowZeroCount: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Vue Router location object for declarative navigation.\n\t\t * When set, the card renders as a <router-link> and clickable styles are implied.\n\t\t * { name: 'Cases', query: { status: 'open' } }\n\t\t * { path: '/catalogi' }\n\t\t */\n\t\troute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the app takes the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\thasIcon() {\n\t\t\treturn this.icon !== null || this.$slots.icon || this.$slots.icon\n\t\t},\n\n\t\t/**\n\t\t * Whether a consumer provided the `value` slot. When set, the value\n\t\t * area always renders (the slot decides what to show, even at count 0).\n\t\t */\n\t\thasValueSlot() {\n\t\t\treturn !!this.$slots.value || !!this.$slots.value\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is in its error state.\n\t\t *\n\t\t * Anything truthy counts, so a caller can pass the caught error itself\n\t\t * rather than converting it to a boolean first. An empty string is NOT\n\t\t * an error — that is a caller who cleared the message, not one\n\t\t * reporting a failure.\n\t\t *\n\t\t * @return {boolean} True when the tile should show `errorLabel`.\n\t\t */\n\t\thasError() {\n\t\t\treturn Boolean(this.error)\n\t\t},\n\n\t\tformattedCount() {\n\t\t\treturn this.count.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Whether the card is interactive (clickable or has a route).\n\t\t * Used for applying hover/focus styles.\n\t\t */\n\t\tisInteractive() {\n\t\t\treturn !!this.route || this.clickable\n\t\t},\n\n\t\t/**\n\t\t * Determines which HTML element or component to render.\n\t\t * - route set → 'router-link' (SPA navigation)\n\t\t * - clickable → 'a' (app handles click via event)\n\t\t * - neither → 'div' (static display)\n\t\t */\n\t\tcomponentTag() {\n\t\t\tif (this.route) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn 'div'\n\t\t},\n\n\t\t/**\n\t\t * Dynamic attributes for the root element based on rendering mode.\n\t\t */\n\t\tcomponentAttrs() {\n\t\t\tif (this.route) {\n\t\t\t\treturn { to: this.route, tabindex: '0' }\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn { href: '#', role: 'button', tabindex: '0' }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the block takes the stacked board look (`layout`).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t * @return {boolean}\n\t\t */\n\t\tisStacked() {\n\t\t\treturn this.layout === 'stacked'\n\t\t},\n\n\t\trootClasses() {\n\t\t\treturn {\n\t\t\t\t// Canonical + legacy, in pairs: the canonical class carries the\n\t\t\t\t// look, the legacy one keeps existing app CSS matching.\n\t\t\t\t// The canonical card is already horizontal, so the legacy pair\n\t\t\t\t// is emitted for app CSS but carries no look of its own.\n\t\t\t\t'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),\n\t\t\t\t'cn-kpi-card--stacked': this.isStacked,\n\t\t\t\t'cn-kpi-card--board': this.isBoardLook,\n\t\t\t\t'cn-stats-block--horizontal': this.horizontal || !this.vertical,\n\t\t\t\t'cn-kpi-card--vertical': this.vertical,\n\t\t\t\t'cn-stats-block--vertical': this.vertical,\n\t\t\t\t'cn-kpi-card--filled': this.filled,\n\t\t\t\t'cn-kpi-card--clickable': this.isInteractive,\n\t\t\t\t'cn-stats-block--clickable': this.isInteractive,\n\t\t\t\t'cn-kpi-card--error': this.hasError,\n\t\t\t\t[`cn-kpi-card--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t\t// An errored tile tints itself. Requiring the consumer to also\n\t\t\t\t// pass `variant=\"error\"` would mean every caller remembering\n\t\t\t\t// two props to express one state, and forgetting the second is\n\t\t\t\t// invisible — a tile that reads \"Unavailable\" in the default\n\t\t\t\t// colour looks like ordinary content.\n\t\t\t\t'cn-stats-block--error': this.hasError,\n\t\t\t\t[`cn-stats-block--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t}\n\t\t},\n\n\t\ticonClasses() {\n\t\t\treturn {\n\t\t\t\t[`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',\n\t\t\t}\n\t\t},\n\n\t},\n\n\tmethods: {\n\t\tformatBreakdownLabel(key) {\n\t\t\treturn key.charAt(0).toUpperCase() + key.slice(1) + ':'\n\t\t},\n\n\t\tonClick(event) {\n\t\t\t// When route is set, router-link handles navigation — do not emit click\n\t\t\tif (this.route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$emit('click', event)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card's own look — box, body stack, icon circle, title, value, variants,\n * hover — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget. The rules that lived here duplicated it in different numbers\n * (a 1.2rem count row, a 40px icon, hardcoded rgba() tints that ignored a\n * re-themed palette), and that duplication IS how the two KPI looks drifted.\n * What is left below is this component's own extras: the breakdown block, the\n * loading line and the empty line.\n *\n * Those extras are secondary KPI text, so they read the shared\n * `--cn-kpi-label-*` scale rather than carrying sizes of their own — an empty\n * state that is a different size from the label it replaces is the same drift\n * in miniature.\n */\n\n.cn-stats-block__header {\n\tmax-width: 100%;\n\tmin-width: 0;\n}\n\n.cn-stats-block__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-start;\n\tgap: 6px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-stats-block__empty {\n\ttext-align: start;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n}\n\n.cn-stats-block__breakdown {\n\tmargin-top: 4px;\n\tpadding: 8px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tborder: 1px solid var(--color-border);\n\twidth: 100%;\n\ttext-align: start;\n}\n\n.cn-stats-block__breakdown-item {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-stats-block__breakdown-item:last-child {\n\tmargin-bottom: 0;\n}\n\n.cn-stats-block__breakdown-label {\n\tfont-weight: 500;\n\tcolor: var(--color-main-text);\n}\n\n/* The value takes the card's accent, so a breakdown reads as part of the tile\n it sits in rather than as a table that happened to land there. It used to\n carry a `--color-background-hover` chip — on a `--color-background-hover`\n breakdown box, which is to say an invisible chip drawn at the cost of two\n extra declarations and a differently-shaped hit area. The per-key modifiers\n below still win: `invalid`, `deleted` and `published` mean something\n specific and outrank the tile's own colour. */\n.cn-stats-block__breakdown-value {\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n\tcolor: var(--cn-kpi-accent, var(--color-primary-element));\n}\n\n.cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }\n\n.cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }\n\n.cn-stats-block__breakdown-value--published { color: var(--color-element-success); }\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnStatsBlock.vue2.js","sources":["../../../../src/components/CnStatsBlock/CnStatsBlock.vue"],"sourcesContent":["<!--\n - CnStatsBlock renders the CANONICAL KPI card (src/css/kpi-card.css) — the\n - same markup and the same stylesheet CnStatWidget renders, so OpenCatalogi's\n - KPIs and dossiq's are one design rather than two. This component's own\n - `cn-stats-block*` classes stay on the same elements for backwards\n - compatibility with app CSS that targets them; the LOOK comes from the\n - shared sheet.\n -\n - The comment lives OUT here rather than inside <template>: a comment node\n - beside the root element makes the component multi-root in Vue 3, and a\n - multi-root component has no root to put class attributes on — `rootClasses`\n - silently stopped reaching the DOM.\n-->\n<template>\n\t<component\n\t\t:is=\"componentTag\"\n\t\tclass=\"cn-kpi-card cn-stats-block\"\n\t\t:class=\"rootClasses\"\n\t\tv-bind=\"componentAttrs\"\n\t\t@click=\"onClick\">\n\t\t<!-- Icon -->\n\t\t<div v-if=\"hasIcon && !isStacked && !isBoardLook\" class=\"cn-kpi-card__icon cn-stats-block__icon\" :class=\"iconClasses\">\n\t\t\t<slot name=\"icon\">\n\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"iconSize\" />\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<div class=\"cn-kpi-card__body cn-stats-block__content\">\n\t\t\t<div class=\"cn-stats-block__header\">\n\t\t\t\t<!-- Board look: an 18px glyph before the label, only on a link tile. -->\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"isBoardLook && isInteractive && hasIcon\"\n\t\t\t\t\tclass=\"cn-kpi-card__glyph cn-stats-block__glyph\"\n\t\t\t\t\tdata-testid=\"cn-stats-block-glyph\"\n\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"18\" />\n\t\t\t\t\t</slot>\n\t\t\t\t</span>\n\t\t\t\t<h4 class=\"cn-kpi-card__title\" :title=\"title || undefined\">\n\t\t\t\t\t{{ title ? tl(title) : t('nextcloud-vue', 'Objects') }}\n\t\t\t\t</h4>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"hasError\" class=\"cn-stats-block__count cn-stats-block__count--error\">\n\t\t\t\t<span class=\"cn-stats-block__count-value\">—</span>\n\t\t\t\t<span class=\"cn-stats-block__count-label\">{{ errorLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"hasValueSlot || count > 0 || (showZeroCount && count === 0)\" class=\"cn-kpi-card__value-row cn-stats-block__count\">\n\t\t\t\t<span class=\"cn-kpi-card__value cn-stats-block__count-value\">\n\t\t\t\t\t<!-- @slot Override the prominently-displayed value — render a pre-formatted string (currency, percent, a \"—\" placeholder, …). `count` stays the raw number; this is presentation only. Defaults to the localized count. -->\n\t\t\t\t\t<!-- @binding {number} count The raw numeric count. -->\n\t\t\t\t\t<!-- @binding {string} formatted The default localized count string. -->\n\t\t\t\t\t<slot name=\"value\" :count=\"count\" :formatted=\"formattedCount\">{{ formattedCount }}</slot>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-kpi-card__label cn-stats-block__count-label\">{{ countLabel ? tl(countLabel) : countLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"loading\" class=\"cn-stats-block__loading\">\n\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t{{ loadingLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-stats-block__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Breakdown details -->\n\t\t\t<div\n\t\t\t\tv-if=\"hasError === false && breakdown && (hasValueSlot || count > 0 || showZeroCount)\"\n\t\t\t\tclass=\"cn-stats-block__breakdown\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(value, key) in breakdown\"\n\t\t\t\t\t:key=\"key\"\n\t\t\t\t\tclass=\"cn-stats-block__breakdown-item\">\n\t\t\t\t\t<span class=\"cn-stats-block__breakdown-label\">{{ formatBreakdownLabel(key) }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-stats-block__breakdown-value\"\n\t\t\t\t\t\t:class=\"'cn-stats-block__breakdown-value--' + key\">\n\t\t\t\t\t\t{{ value }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { normalizeLook } from '../../composables/useLook.js'\n\n// The canonical KPI look, shared with CnStatWidget. Imported here so the\n// card is styled even when the consuming app has not pulled in the\n// library's global css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.\n *\n * Renders the canonical KPI card: a large circular icon beside a left-aligned\n * number, drawing no box of its own because the wrapper around it already\n * draws one. Supports colour variants, icons and clickable state. Use in a\n * CnKpiGrid for responsive dashboard layouts.\n *\n * Basic (canonical horizontal card)\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" count-label=\"open cases\" />\n * ```\n *\n * With icon and variant\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * clickable\n * @click=\"goToCases\" />\n * ```\n *\n * Stacked and boxed — for a block mounted with no wrapper around it\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" vertical filled />\n * ```\n *\n * With route-based navigation (renders as <router-link>)\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * :route=\"{ name: 'Cases', query: { status: 'open' } }\" />\n * ```\n *\n * With breakdown\n * ```vue\n * <CnStatsBlock\n * title=\"Cases\"\n * :count=\"42\"\n * :breakdown=\"{ total: 100, invalid: 3, deleted: 5, published: 92 }\" />\n * ```\n *\n * Custom icon slot\n * ```vue\n * <CnStatsBlock title=\"Files\" :count=\"128\">\n * <template #icon>\n * <FileDocumentOutline :size=\"24\" />\n * </template>\n * </CnStatsBlock>\n * ```\n */\nexport default {\n\tname: 'CnStatsBlock',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The look the app is drawn in, provided by CnAppRoot.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile\n\t\t */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/**\n\t\t * The host's label lookup (CnAppRoot provides it); identity without one, so a\n\t\t * label that is not a key renders as written.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Block title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The main count number to display prominently */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Label displayed next to the count */\n\t\tcountLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'objects'),\n\t\t},\n\n\t\t/** Detailed breakdown object (key-value pairs) */\n\t\tbreakdown: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is currently loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/** Text shown when count is 0 */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/**\n\t\t * The tile could not load its number. Anything truthy counts — pass the\n\t\t * caught error itself, or just `true`.\n\t\t *\n\t\t * The tile then shows a dash and `errorLabel` INSTEAD of a count, and\n\t\t * tints itself `error`. This takes precedence over `count`, `loading`\n\t\t * and `emptyLabel` deliberately: a stale or defaulted number rendered\n\t\t * during a failure is the exact thing this prop exists to stop. A\n\t\t * dashboard showing 0 because the backend is down is worse than one\n\t\t * showing nothing, because 0 is a number a reader will believe.\n\t\t *\n\t\t * @type {boolean|string|Error|null}\n\t\t */\n\t\terror: {\n\t\t\t// TYPE ORDER IS SEMANTIC HERE. Vue casts an empty value to `true`\n\t\t\t// when Boolean appears BEFORE String in the union, so\n\t\t\t// `[Boolean, String]` would read `error=\"\"` as an error. A caller\n\t\t\t// clearing its message back to '' is reporting RECOVERY, and that\n\t\t\t// would have pinned the tile in its error state after the fetch\n\t\t\t// succeeded again. String first keeps '' falsy.\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- Boolean first would make `error=\"\"` truthy, which is the recovery signal.\n\t\t\ttype: [String, Boolean, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown in place of the count when `error` is set. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Unavailable'),\n\t\t},\n\n\t\t/** Icon component (e.g., imported MDI icon) */\n\t\ticon: {\n\t\t\ttype: [Object, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 24,\n\t\t},\n\n\t\t/** Color variant: 'default', 'primary', 'success', 'warning', 'error' */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Stack the icon above a centred number instead of placing it beside\n\t\t * one. The canonical KPI card is horizontal, so this is the opt-out;\n\t\t * `horizontal` below is kept only for callers that already pass it.\n\t\t */\n\t\tvertical: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * `stacked` is the board's tile, as CnStatWidget `content.layout:\n\t\t * \"stacked\"`: no icon circle, a 14px muted title, the number at\n\t\t * 34px/700 in the text colour. Empty (the default) keeps the KPI card.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lay the icon left of the content. No longer needed — this is the\n\t\t * canonical card's own layout — and kept so existing callers that pass\n\t\t * `horizontal` keep working. Pass `vertical` to stack instead.\n\t\t *\n\t\t * @deprecated since 2.25.0, the horizontal layout is the default.\n\t\t */\n\t\thorizontal: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the card's own grey box. Off by default: a stats block is\n\t\t * normally rendered inside a wrapper that already draws a card, and a\n\t\t * second box reads as a card inside a card. Turn it on for a block\n\t\t * mounted with no wrapper around it.\n\t\t */\n\t\tfilled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the card is clickable */\n\t\tclickable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to display 0 as a count value instead of the empty label */\n\t\tshowZeroCount: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Vue Router location object for declarative navigation.\n\t\t * When set, the card renders as a <router-link> and clickable styles are implied.\n\t\t * { name: 'Cases', query: { status: 'open' } }\n\t\t * { path: '/catalogi' }\n\t\t */\n\t\troute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the app takes the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\thasIcon() {\n\t\t\treturn this.icon !== null || this.$slots.icon || this.$slots.icon\n\t\t},\n\n\t\t/**\n\t\t * Whether a consumer provided the `value` slot. When set, the value\n\t\t * area always renders (the slot decides what to show, even at count 0).\n\t\t */\n\t\thasValueSlot() {\n\t\t\treturn !!this.$slots.value || !!this.$slots.value\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is in its error state.\n\t\t *\n\t\t * Anything truthy counts, so a caller can pass the caught error itself\n\t\t * rather than converting it to a boolean first. An empty string is NOT\n\t\t * an error — that is a caller who cleared the message, not one\n\t\t * reporting a failure.\n\t\t *\n\t\t * @return {boolean} True when the tile should show `errorLabel`.\n\t\t */\n\t\thasError() {\n\t\t\treturn Boolean(this.error)\n\t\t},\n\n\t\tformattedCount() {\n\t\t\treturn this.count.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Whether the card is interactive (clickable or has a route).\n\t\t * Used for applying hover/focus styles.\n\t\t */\n\t\tisInteractive() {\n\t\t\treturn !!this.route || this.clickable\n\t\t},\n\n\t\t/**\n\t\t * Determines which HTML element or component to render.\n\t\t * - route set → 'router-link' (SPA navigation)\n\t\t * - clickable → 'a' (app handles click via event)\n\t\t * - neither → 'div' (static display)\n\t\t */\n\t\tcomponentTag() {\n\t\t\tif (this.route) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn 'div'\n\t\t},\n\n\t\t/**\n\t\t * Dynamic attributes for the root element based on rendering mode.\n\t\t */\n\t\tcomponentAttrs() {\n\t\t\tif (this.route) {\n\t\t\t\treturn { to: this.route, tabindex: '0' }\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn { href: '#', role: 'button', tabindex: '0' }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the block takes the stacked board look (`layout`).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t * @return {boolean}\n\t\t */\n\t\tisStacked() {\n\t\t\treturn this.layout === 'stacked'\n\t\t},\n\n\t\trootClasses() {\n\t\t\treturn {\n\t\t\t\t// Canonical + legacy, in pairs: the canonical class carries the\n\t\t\t\t// look, the legacy one keeps existing app CSS matching.\n\t\t\t\t// The canonical card is already horizontal, so the legacy pair\n\t\t\t\t// is emitted for app CSS but carries no look of its own.\n\t\t\t\t'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),\n\t\t\t\t'cn-kpi-card--stacked': this.isStacked,\n\t\t\t\t'cn-kpi-card--board': this.isBoardLook,\n\t\t\t\t'cn-stats-block--horizontal': this.horizontal || !this.vertical,\n\t\t\t\t'cn-kpi-card--vertical': this.vertical,\n\t\t\t\t'cn-stats-block--vertical': this.vertical,\n\t\t\t\t'cn-kpi-card--filled': this.filled,\n\t\t\t\t'cn-kpi-card--clickable': this.isInteractive,\n\t\t\t\t'cn-stats-block--clickable': this.isInteractive,\n\t\t\t\t'cn-kpi-card--error': this.hasError,\n\t\t\t\t[`cn-kpi-card--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t\t// An errored tile tints itself. Requiring the consumer to also\n\t\t\t\t// pass `variant=\"error\"` would mean every caller remembering\n\t\t\t\t// two props to express one state, and forgetting the second is\n\t\t\t\t// invisible — a tile that reads \"Unavailable\" in the default\n\t\t\t\t// colour looks like ordinary content.\n\t\t\t\t'cn-stats-block--error': this.hasError,\n\t\t\t\t[`cn-stats-block--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t}\n\t\t},\n\n\t\ticonClasses() {\n\t\t\treturn {\n\t\t\t\t[`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',\n\t\t\t}\n\t\t},\n\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest string through the host's label lookup (`cnTranslate`), so a key\n\t\t * such as \"Mine\" reads in the user's language.\n\t\t *\n\t\t * @param {string} text The text as written in the manifest.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttl(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\tformatBreakdownLabel(key) {\n\t\t\treturn key.charAt(0).toUpperCase() + key.slice(1) + ':'\n\t\t},\n\n\t\tonClick(event) {\n\t\t\t// When route is set, router-link handles navigation — do not emit click\n\t\t\tif (this.route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$emit('click', event)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card's own look — box, body stack, icon circle, title, value, variants,\n * hover — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget. The rules that lived here duplicated it in different numbers\n * (a 1.2rem count row, a 40px icon, hardcoded rgba() tints that ignored a\n * re-themed palette), and that duplication IS how the two KPI looks drifted.\n * What is left below is this component's own extras: the breakdown block, the\n * loading line and the empty line.\n *\n * Those extras are secondary KPI text, so they read the shared\n * `--cn-kpi-label-*` scale rather than carrying sizes of their own — an empty\n * state that is a different size from the label it replaces is the same drift\n * in miniature.\n */\n\n.cn-stats-block__header {\n\tmax-width: 100%;\n\tmin-width: 0;\n}\n\n.cn-stats-block__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-start;\n\tgap: 6px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-stats-block__empty {\n\ttext-align: start;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n}\n\n.cn-stats-block__breakdown {\n\tmargin-top: 4px;\n\tpadding: 8px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tborder: 1px solid var(--color-border);\n\twidth: 100%;\n\ttext-align: start;\n}\n\n.cn-stats-block__breakdown-item {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-stats-block__breakdown-item:last-child {\n\tmargin-bottom: 0;\n}\n\n.cn-stats-block__breakdown-label {\n\tfont-weight: 500;\n\tcolor: var(--color-main-text);\n}\n\n/* The value takes the card's accent, so a breakdown reads as part of the tile\n it sits in rather than as a table that happened to land there. It used to\n carry a `--color-background-hover` chip — on a `--color-background-hover`\n breakdown box, which is to say an invisible chip drawn at the cost of two\n extra declarations and a differently-shaped hit area. The per-key modifiers\n below still win: `invalid`, `deleted` and `published` mean something\n specific and outrank the tile's own colour. */\n.cn-stats-block__breakdown-value {\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n\tcolor: var(--cn-kpi-accent, var(--color-primary-element));\n}\n\n.cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }\n\n.cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }\n\n.cn-stats-block__breakdown-value--published { color: var(--color-element-success); }\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -73,7 +73,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
73
73
|
createElementVNode("h4", {
|
|
74
74
|
class: "cn-kpi-card__title",
|
|
75
75
|
title: $props.title || undefined
|
|
76
|
-
}, toDisplayString($props.title
|
|
76
|
+
}, toDisplayString($props.title ? $options.tl($props.title) : _ctx.t('nextcloud-vue', 'Objects')), 9 /* TEXT, PROPS */, _hoisted_4)
|
|
77
77
|
]),
|
|
78
78
|
($options.hasError)
|
|
79
79
|
? (openBlock(), createElementBlock("div", _hoisted_5, [
|
|
@@ -90,7 +90,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
90
90
|
createTextVNode(toDisplayString($options.formattedCount), 1 /* TEXT */)
|
|
91
91
|
])
|
|
92
92
|
]),
|
|
93
|
-
createElementVNode("span", _hoisted_9, toDisplayString($props.countLabel), 1 /* TEXT */)
|
|
93
|
+
createElementVNode("span", _hoisted_9, toDisplayString($props.countLabel ? $options.tl($props.countLabel) : $props.countLabel), 1 /* TEXT */)
|
|
94
94
|
]))
|
|
95
95
|
: ($props.loading)
|
|
96
96
|
? (openBlock(), createElementBlock("div", _hoisted_10, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnStatsBlock.vue3.js","sources":["../../../../src/components/CnStatsBlock/CnStatsBlock.vue?vue&type=template&id=722d9aef&lang.js"],"sourcesContent":["<!--\n - CnStatsBlock renders the CANONICAL KPI card (src/css/kpi-card.css) — the\n - same markup and the same stylesheet CnStatWidget renders, so OpenCatalogi's\n - KPIs and dossiq's are one design rather than two. This component's own\n - `cn-stats-block*` classes stay on the same elements for backwards\n - compatibility with app CSS that targets them; the LOOK comes from the\n - shared sheet.\n -\n - The comment lives OUT here rather than inside <template>: a comment node\n - beside the root element makes the component multi-root in Vue 3, and a\n - multi-root component has no root to put class attributes on — `rootClasses`\n - silently stopped reaching the DOM.\n-->\n<template>\n\t<component\n\t\t:is=\"componentTag\"\n\t\tclass=\"cn-kpi-card cn-stats-block\"\n\t\t:class=\"rootClasses\"\n\t\tv-bind=\"componentAttrs\"\n\t\t@click=\"onClick\">\n\t\t<!-- Icon -->\n\t\t<div v-if=\"hasIcon && !isStacked && !isBoardLook\" class=\"cn-kpi-card__icon cn-stats-block__icon\" :class=\"iconClasses\">\n\t\t\t<slot name=\"icon\">\n\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"iconSize\" />\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<div class=\"cn-kpi-card__body cn-stats-block__content\">\n\t\t\t<div class=\"cn-stats-block__header\">\n\t\t\t\t<!-- Board look: an 18px glyph before the label, only on a link tile. -->\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"isBoardLook && isInteractive && hasIcon\"\n\t\t\t\t\tclass=\"cn-kpi-card__glyph cn-stats-block__glyph\"\n\t\t\t\t\tdata-testid=\"cn-stats-block-glyph\"\n\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"18\" />\n\t\t\t\t\t</slot>\n\t\t\t\t</span>\n\t\t\t\t<h4 class=\"cn-kpi-card__title\" :title=\"title || undefined\">\n\t\t\t\t\t{{ title || t('nextcloud-vue', 'Objects') }}\n\t\t\t\t</h4>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"hasError\" class=\"cn-stats-block__count cn-stats-block__count--error\">\n\t\t\t\t<span class=\"cn-stats-block__count-value\">—</span>\n\t\t\t\t<span class=\"cn-stats-block__count-label\">{{ errorLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"hasValueSlot || count > 0 || (showZeroCount && count === 0)\" class=\"cn-kpi-card__value-row cn-stats-block__count\">\n\t\t\t\t<span class=\"cn-kpi-card__value cn-stats-block__count-value\">\n\t\t\t\t\t<!-- @slot Override the prominently-displayed value — render a pre-formatted string (currency, percent, a \"—\" placeholder, …). `count` stays the raw number; this is presentation only. Defaults to the localized count. -->\n\t\t\t\t\t<!-- @binding {number} count The raw numeric count. -->\n\t\t\t\t\t<!-- @binding {string} formatted The default localized count string. -->\n\t\t\t\t\t<slot name=\"value\" :count=\"count\" :formatted=\"formattedCount\">{{ formattedCount }}</slot>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-kpi-card__label cn-stats-block__count-label\">{{ countLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"loading\" class=\"cn-stats-block__loading\">\n\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t{{ loadingLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-stats-block__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Breakdown details -->\n\t\t\t<div\n\t\t\t\tv-if=\"hasError === false && breakdown && (hasValueSlot || count > 0 || showZeroCount)\"\n\t\t\t\tclass=\"cn-stats-block__breakdown\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(value, key) in breakdown\"\n\t\t\t\t\t:key=\"key\"\n\t\t\t\t\tclass=\"cn-stats-block__breakdown-item\">\n\t\t\t\t\t<span class=\"cn-stats-block__breakdown-label\">{{ formatBreakdownLabel(key) }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-stats-block__breakdown-value\"\n\t\t\t\t\t\t:class=\"'cn-stats-block__breakdown-value--' + key\">\n\t\t\t\t\t\t{{ value }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { normalizeLook } from '../../composables/useLook.js'\n\n// The canonical KPI look, shared with CnStatWidget. Imported here so the\n// card is styled even when the consuming app has not pulled in the\n// library's global css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.\n *\n * Renders the canonical KPI card: a large circular icon beside a left-aligned\n * number, drawing no box of its own because the wrapper around it already\n * draws one. Supports colour variants, icons and clickable state. Use in a\n * CnKpiGrid for responsive dashboard layouts.\n *\n * Basic (canonical horizontal card)\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" count-label=\"open cases\" />\n * ```\n *\n * With icon and variant\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * clickable\n * @click=\"goToCases\" />\n * ```\n *\n * Stacked and boxed — for a block mounted with no wrapper around it\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" vertical filled />\n * ```\n *\n * With route-based navigation (renders as <router-link>)\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * :route=\"{ name: 'Cases', query: { status: 'open' } }\" />\n * ```\n *\n * With breakdown\n * ```vue\n * <CnStatsBlock\n * title=\"Cases\"\n * :count=\"42\"\n * :breakdown=\"{ total: 100, invalid: 3, deleted: 5, published: 92 }\" />\n * ```\n *\n * Custom icon slot\n * ```vue\n * <CnStatsBlock title=\"Files\" :count=\"128\">\n * <template #icon>\n * <FileDocumentOutline :size=\"24\" />\n * </template>\n * </CnStatsBlock>\n * ```\n */\nexport default {\n\tname: 'CnStatsBlock',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The look the app is drawn in, provided by CnAppRoot.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile\n\t\t */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Block title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The main count number to display prominently */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Label displayed next to the count */\n\t\tcountLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'objects'),\n\t\t},\n\n\t\t/** Detailed breakdown object (key-value pairs) */\n\t\tbreakdown: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is currently loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/** Text shown when count is 0 */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/**\n\t\t * The tile could not load its number. Anything truthy counts — pass the\n\t\t * caught error itself, or just `true`.\n\t\t *\n\t\t * The tile then shows a dash and `errorLabel` INSTEAD of a count, and\n\t\t * tints itself `error`. This takes precedence over `count`, `loading`\n\t\t * and `emptyLabel` deliberately: a stale or defaulted number rendered\n\t\t * during a failure is the exact thing this prop exists to stop. A\n\t\t * dashboard showing 0 because the backend is down is worse than one\n\t\t * showing nothing, because 0 is a number a reader will believe.\n\t\t *\n\t\t * @type {boolean|string|Error|null}\n\t\t */\n\t\terror: {\n\t\t\t// TYPE ORDER IS SEMANTIC HERE. Vue casts an empty value to `true`\n\t\t\t// when Boolean appears BEFORE String in the union, so\n\t\t\t// `[Boolean, String]` would read `error=\"\"` as an error. A caller\n\t\t\t// clearing its message back to '' is reporting RECOVERY, and that\n\t\t\t// would have pinned the tile in its error state after the fetch\n\t\t\t// succeeded again. String first keeps '' falsy.\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- Boolean first would make `error=\"\"` truthy, which is the recovery signal.\n\t\t\ttype: [String, Boolean, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown in place of the count when `error` is set. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Unavailable'),\n\t\t},\n\n\t\t/** Icon component (e.g., imported MDI icon) */\n\t\ticon: {\n\t\t\ttype: [Object, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 24,\n\t\t},\n\n\t\t/** Color variant: 'default', 'primary', 'success', 'warning', 'error' */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Stack the icon above a centred number instead of placing it beside\n\t\t * one. The canonical KPI card is horizontal, so this is the opt-out;\n\t\t * `horizontal` below is kept only for callers that already pass it.\n\t\t */\n\t\tvertical: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * `stacked` is the board's tile, as CnStatWidget `content.layout:\n\t\t * \"stacked\"`: no icon circle, a 14px muted title, the number at\n\t\t * 34px/700 in the text colour. Empty (the default) keeps the KPI card.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lay the icon left of the content. No longer needed — this is the\n\t\t * canonical card's own layout — and kept so existing callers that pass\n\t\t * `horizontal` keep working. Pass `vertical` to stack instead.\n\t\t *\n\t\t * @deprecated since 2.25.0, the horizontal layout is the default.\n\t\t */\n\t\thorizontal: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the card's own grey box. Off by default: a stats block is\n\t\t * normally rendered inside a wrapper that already draws a card, and a\n\t\t * second box reads as a card inside a card. Turn it on for a block\n\t\t * mounted with no wrapper around it.\n\t\t */\n\t\tfilled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the card is clickable */\n\t\tclickable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to display 0 as a count value instead of the empty label */\n\t\tshowZeroCount: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Vue Router location object for declarative navigation.\n\t\t * When set, the card renders as a <router-link> and clickable styles are implied.\n\t\t * { name: 'Cases', query: { status: 'open' } }\n\t\t * { path: '/catalogi' }\n\t\t */\n\t\troute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the app takes the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\thasIcon() {\n\t\t\treturn this.icon !== null || this.$slots.icon || this.$slots.icon\n\t\t},\n\n\t\t/**\n\t\t * Whether a consumer provided the `value` slot. When set, the value\n\t\t * area always renders (the slot decides what to show, even at count 0).\n\t\t */\n\t\thasValueSlot() {\n\t\t\treturn !!this.$slots.value || !!this.$slots.value\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is in its error state.\n\t\t *\n\t\t * Anything truthy counts, so a caller can pass the caught error itself\n\t\t * rather than converting it to a boolean first. An empty string is NOT\n\t\t * an error — that is a caller who cleared the message, not one\n\t\t * reporting a failure.\n\t\t *\n\t\t * @return {boolean} True when the tile should show `errorLabel`.\n\t\t */\n\t\thasError() {\n\t\t\treturn Boolean(this.error)\n\t\t},\n\n\t\tformattedCount() {\n\t\t\treturn this.count.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Whether the card is interactive (clickable or has a route).\n\t\t * Used for applying hover/focus styles.\n\t\t */\n\t\tisInteractive() {\n\t\t\treturn !!this.route || this.clickable\n\t\t},\n\n\t\t/**\n\t\t * Determines which HTML element or component to render.\n\t\t * - route set → 'router-link' (SPA navigation)\n\t\t * - clickable → 'a' (app handles click via event)\n\t\t * - neither → 'div' (static display)\n\t\t */\n\t\tcomponentTag() {\n\t\t\tif (this.route) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn 'div'\n\t\t},\n\n\t\t/**\n\t\t * Dynamic attributes for the root element based on rendering mode.\n\t\t */\n\t\tcomponentAttrs() {\n\t\t\tif (this.route) {\n\t\t\t\treturn { to: this.route, tabindex: '0' }\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn { href: '#', role: 'button', tabindex: '0' }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the block takes the stacked board look (`layout`).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t * @return {boolean}\n\t\t */\n\t\tisStacked() {\n\t\t\treturn this.layout === 'stacked'\n\t\t},\n\n\t\trootClasses() {\n\t\t\treturn {\n\t\t\t\t// Canonical + legacy, in pairs: the canonical class carries the\n\t\t\t\t// look, the legacy one keeps existing app CSS matching.\n\t\t\t\t// The canonical card is already horizontal, so the legacy pair\n\t\t\t\t// is emitted for app CSS but carries no look of its own.\n\t\t\t\t'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),\n\t\t\t\t'cn-kpi-card--stacked': this.isStacked,\n\t\t\t\t'cn-kpi-card--board': this.isBoardLook,\n\t\t\t\t'cn-stats-block--horizontal': this.horizontal || !this.vertical,\n\t\t\t\t'cn-kpi-card--vertical': this.vertical,\n\t\t\t\t'cn-stats-block--vertical': this.vertical,\n\t\t\t\t'cn-kpi-card--filled': this.filled,\n\t\t\t\t'cn-kpi-card--clickable': this.isInteractive,\n\t\t\t\t'cn-stats-block--clickable': this.isInteractive,\n\t\t\t\t'cn-kpi-card--error': this.hasError,\n\t\t\t\t[`cn-kpi-card--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t\t// An errored tile tints itself. Requiring the consumer to also\n\t\t\t\t// pass `variant=\"error\"` would mean every caller remembering\n\t\t\t\t// two props to express one state, and forgetting the second is\n\t\t\t\t// invisible — a tile that reads \"Unavailable\" in the default\n\t\t\t\t// colour looks like ordinary content.\n\t\t\t\t'cn-stats-block--error': this.hasError,\n\t\t\t\t[`cn-stats-block--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t}\n\t\t},\n\n\t\ticonClasses() {\n\t\t\treturn {\n\t\t\t\t[`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',\n\t\t\t}\n\t\t},\n\n\t},\n\n\tmethods: {\n\t\tformatBreakdownLabel(key) {\n\t\t\treturn key.charAt(0).toUpperCase() + key.slice(1) + ':'\n\t\t},\n\n\t\tonClick(event) {\n\t\t\t// When route is set, router-link handles navigation — do not emit click\n\t\t\tif (this.route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$emit('click', event)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card's own look — box, body stack, icon circle, title, value, variants,\n * hover — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget. The rules that lived here duplicated it in different numbers\n * (a 1.2rem count row, a 40px icon, hardcoded rgba() tints that ignored a\n * re-themed palette), and that duplication IS how the two KPI looks drifted.\n * What is left below is this component's own extras: the breakdown block, the\n * loading line and the empty line.\n *\n * Those extras are secondary KPI text, so they read the shared\n * `--cn-kpi-label-*` scale rather than carrying sizes of their own — an empty\n * state that is a different size from the label it replaces is the same drift\n * in miniature.\n */\n\n.cn-stats-block__header {\n\tmax-width: 100%;\n\tmin-width: 0;\n}\n\n.cn-stats-block__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-start;\n\tgap: 6px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-stats-block__empty {\n\ttext-align: start;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n}\n\n.cn-stats-block__breakdown {\n\tmargin-top: 4px;\n\tpadding: 8px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tborder: 1px solid var(--color-border);\n\twidth: 100%;\n\ttext-align: start;\n}\n\n.cn-stats-block__breakdown-item {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-stats-block__breakdown-item:last-child {\n\tmargin-bottom: 0;\n}\n\n.cn-stats-block__breakdown-label {\n\tfont-weight: 500;\n\tcolor: var(--color-main-text);\n}\n\n/* The value takes the card's accent, so a breakdown reads as part of the tile\n it sits in rather than as a table that happened to land there. It used to\n carry a `--color-background-hover` chip — on a `--color-background-hover`\n breakdown box, which is to say an invisible chip drawn at the cost of two\n extra declarations and a differently-shaped hit area. The per-key modifiers\n below still win: `invalid`, `deleted` and `published` mean something\n specific and outrank the tile's own colour. */\n.cn-stats-block__breakdown-value {\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n\tcolor: var(--cn-kpi-accent, var(--color-primary-element));\n}\n\n.cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }\n\n.cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }\n\n.cn-stats-block__breakdown-value--published { color: var(--color-element-success); }\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_openBlock","_createBlock","_resolveDynamicComponent","_createElementVNode","_toDisplayString","_createVNode","_createTextVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnStatsBlock.vue3.js","sources":["../../../../src/components/CnStatsBlock/CnStatsBlock.vue?vue&type=template&id=722d9aef&lang.js"],"sourcesContent":["<!--\n - CnStatsBlock renders the CANONICAL KPI card (src/css/kpi-card.css) — the\n - same markup and the same stylesheet CnStatWidget renders, so OpenCatalogi's\n - KPIs and dossiq's are one design rather than two. This component's own\n - `cn-stats-block*` classes stay on the same elements for backwards\n - compatibility with app CSS that targets them; the LOOK comes from the\n - shared sheet.\n -\n - The comment lives OUT here rather than inside <template>: a comment node\n - beside the root element makes the component multi-root in Vue 3, and a\n - multi-root component has no root to put class attributes on — `rootClasses`\n - silently stopped reaching the DOM.\n-->\n<template>\n\t<component\n\t\t:is=\"componentTag\"\n\t\tclass=\"cn-kpi-card cn-stats-block\"\n\t\t:class=\"rootClasses\"\n\t\tv-bind=\"componentAttrs\"\n\t\t@click=\"onClick\">\n\t\t<!-- Icon -->\n\t\t<div v-if=\"hasIcon && !isStacked && !isBoardLook\" class=\"cn-kpi-card__icon cn-stats-block__icon\" :class=\"iconClasses\">\n\t\t\t<slot name=\"icon\">\n\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"iconSize\" />\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<div class=\"cn-kpi-card__body cn-stats-block__content\">\n\t\t\t<div class=\"cn-stats-block__header\">\n\t\t\t\t<!-- Board look: an 18px glyph before the label, only on a link tile. -->\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"isBoardLook && isInteractive && hasIcon\"\n\t\t\t\t\tclass=\"cn-kpi-card__glyph cn-stats-block__glyph\"\n\t\t\t\t\tdata-testid=\"cn-stats-block-glyph\"\n\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"18\" />\n\t\t\t\t\t</slot>\n\t\t\t\t</span>\n\t\t\t\t<h4 class=\"cn-kpi-card__title\" :title=\"title || undefined\">\n\t\t\t\t\t{{ title ? tl(title) : t('nextcloud-vue', 'Objects') }}\n\t\t\t\t</h4>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"hasError\" class=\"cn-stats-block__count cn-stats-block__count--error\">\n\t\t\t\t<span class=\"cn-stats-block__count-value\">—</span>\n\t\t\t\t<span class=\"cn-stats-block__count-label\">{{ errorLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"hasValueSlot || count > 0 || (showZeroCount && count === 0)\" class=\"cn-kpi-card__value-row cn-stats-block__count\">\n\t\t\t\t<span class=\"cn-kpi-card__value cn-stats-block__count-value\">\n\t\t\t\t\t<!-- @slot Override the prominently-displayed value — render a pre-formatted string (currency, percent, a \"—\" placeholder, …). `count` stays the raw number; this is presentation only. Defaults to the localized count. -->\n\t\t\t\t\t<!-- @binding {number} count The raw numeric count. -->\n\t\t\t\t\t<!-- @binding {string} formatted The default localized count string. -->\n\t\t\t\t\t<slot name=\"value\" :count=\"count\" :formatted=\"formattedCount\">{{ formattedCount }}</slot>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-kpi-card__label cn-stats-block__count-label\">{{ countLabel ? tl(countLabel) : countLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"loading\" class=\"cn-stats-block__loading\">\n\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t{{ loadingLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-stats-block__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Breakdown details -->\n\t\t\t<div\n\t\t\t\tv-if=\"hasError === false && breakdown && (hasValueSlot || count > 0 || showZeroCount)\"\n\t\t\t\tclass=\"cn-stats-block__breakdown\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(value, key) in breakdown\"\n\t\t\t\t\t:key=\"key\"\n\t\t\t\t\tclass=\"cn-stats-block__breakdown-item\">\n\t\t\t\t\t<span class=\"cn-stats-block__breakdown-label\">{{ formatBreakdownLabel(key) }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-stats-block__breakdown-value\"\n\t\t\t\t\t\t:class=\"'cn-stats-block__breakdown-value--' + key\">\n\t\t\t\t\t\t{{ value }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { normalizeLook } from '../../composables/useLook.js'\n\n// The canonical KPI look, shared with CnStatWidget. Imported here so the\n// card is styled even when the consuming app has not pulled in the\n// library's global css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.\n *\n * Renders the canonical KPI card: a large circular icon beside a left-aligned\n * number, drawing no box of its own because the wrapper around it already\n * draws one. Supports colour variants, icons and clickable state. Use in a\n * CnKpiGrid for responsive dashboard layouts.\n *\n * Basic (canonical horizontal card)\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" count-label=\"open cases\" />\n * ```\n *\n * With icon and variant\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * clickable\n * @click=\"goToCases\" />\n * ```\n *\n * Stacked and boxed — for a block mounted with no wrapper around it\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" vertical filled />\n * ```\n *\n * With route-based navigation (renders as <router-link>)\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * :route=\"{ name: 'Cases', query: { status: 'open' } }\" />\n * ```\n *\n * With breakdown\n * ```vue\n * <CnStatsBlock\n * title=\"Cases\"\n * :count=\"42\"\n * :breakdown=\"{ total: 100, invalid: 3, deleted: 5, published: 92 }\" />\n * ```\n *\n * Custom icon slot\n * ```vue\n * <CnStatsBlock title=\"Files\" :count=\"128\">\n * <template #icon>\n * <FileDocumentOutline :size=\"24\" />\n * </template>\n * </CnStatsBlock>\n * ```\n */\nexport default {\n\tname: 'CnStatsBlock',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The look the app is drawn in, provided by CnAppRoot.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile\n\t\t */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/**\n\t\t * The host's label lookup (CnAppRoot provides it); identity without one, so a\n\t\t * label that is not a key renders as written.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Block title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The main count number to display prominently */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Label displayed next to the count */\n\t\tcountLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'objects'),\n\t\t},\n\n\t\t/** Detailed breakdown object (key-value pairs) */\n\t\tbreakdown: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is currently loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/** Text shown when count is 0 */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/**\n\t\t * The tile could not load its number. Anything truthy counts — pass the\n\t\t * caught error itself, or just `true`.\n\t\t *\n\t\t * The tile then shows a dash and `errorLabel` INSTEAD of a count, and\n\t\t * tints itself `error`. This takes precedence over `count`, `loading`\n\t\t * and `emptyLabel` deliberately: a stale or defaulted number rendered\n\t\t * during a failure is the exact thing this prop exists to stop. A\n\t\t * dashboard showing 0 because the backend is down is worse than one\n\t\t * showing nothing, because 0 is a number a reader will believe.\n\t\t *\n\t\t * @type {boolean|string|Error|null}\n\t\t */\n\t\terror: {\n\t\t\t// TYPE ORDER IS SEMANTIC HERE. Vue casts an empty value to `true`\n\t\t\t// when Boolean appears BEFORE String in the union, so\n\t\t\t// `[Boolean, String]` would read `error=\"\"` as an error. A caller\n\t\t\t// clearing its message back to '' is reporting RECOVERY, and that\n\t\t\t// would have pinned the tile in its error state after the fetch\n\t\t\t// succeeded again. String first keeps '' falsy.\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- Boolean first would make `error=\"\"` truthy, which is the recovery signal.\n\t\t\ttype: [String, Boolean, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown in place of the count when `error` is set. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Unavailable'),\n\t\t},\n\n\t\t/** Icon component (e.g., imported MDI icon) */\n\t\ticon: {\n\t\t\ttype: [Object, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 24,\n\t\t},\n\n\t\t/** Color variant: 'default', 'primary', 'success', 'warning', 'error' */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Stack the icon above a centred number instead of placing it beside\n\t\t * one. The canonical KPI card is horizontal, so this is the opt-out;\n\t\t * `horizontal` below is kept only for callers that already pass it.\n\t\t */\n\t\tvertical: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * `stacked` is the board's tile, as CnStatWidget `content.layout:\n\t\t * \"stacked\"`: no icon circle, a 14px muted title, the number at\n\t\t * 34px/700 in the text colour. Empty (the default) keeps the KPI card.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lay the icon left of the content. No longer needed — this is the\n\t\t * canonical card's own layout — and kept so existing callers that pass\n\t\t * `horizontal` keep working. Pass `vertical` to stack instead.\n\t\t *\n\t\t * @deprecated since 2.25.0, the horizontal layout is the default.\n\t\t */\n\t\thorizontal: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the card's own grey box. Off by default: a stats block is\n\t\t * normally rendered inside a wrapper that already draws a card, and a\n\t\t * second box reads as a card inside a card. Turn it on for a block\n\t\t * mounted with no wrapper around it.\n\t\t */\n\t\tfilled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the card is clickable */\n\t\tclickable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to display 0 as a count value instead of the empty label */\n\t\tshowZeroCount: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Vue Router location object for declarative navigation.\n\t\t * When set, the card renders as a <router-link> and clickable styles are implied.\n\t\t * { name: 'Cases', query: { status: 'open' } }\n\t\t * { path: '/catalogi' }\n\t\t */\n\t\troute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the app takes the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\thasIcon() {\n\t\t\treturn this.icon !== null || this.$slots.icon || this.$slots.icon\n\t\t},\n\n\t\t/**\n\t\t * Whether a consumer provided the `value` slot. When set, the value\n\t\t * area always renders (the slot decides what to show, even at count 0).\n\t\t */\n\t\thasValueSlot() {\n\t\t\treturn !!this.$slots.value || !!this.$slots.value\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is in its error state.\n\t\t *\n\t\t * Anything truthy counts, so a caller can pass the caught error itself\n\t\t * rather than converting it to a boolean first. An empty string is NOT\n\t\t * an error — that is a caller who cleared the message, not one\n\t\t * reporting a failure.\n\t\t *\n\t\t * @return {boolean} True when the tile should show `errorLabel`.\n\t\t */\n\t\thasError() {\n\t\t\treturn Boolean(this.error)\n\t\t},\n\n\t\tformattedCount() {\n\t\t\treturn this.count.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Whether the card is interactive (clickable or has a route).\n\t\t * Used for applying hover/focus styles.\n\t\t */\n\t\tisInteractive() {\n\t\t\treturn !!this.route || this.clickable\n\t\t},\n\n\t\t/**\n\t\t * Determines which HTML element or component to render.\n\t\t * - route set → 'router-link' (SPA navigation)\n\t\t * - clickable → 'a' (app handles click via event)\n\t\t * - neither → 'div' (static display)\n\t\t */\n\t\tcomponentTag() {\n\t\t\tif (this.route) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn 'div'\n\t\t},\n\n\t\t/**\n\t\t * Dynamic attributes for the root element based on rendering mode.\n\t\t */\n\t\tcomponentAttrs() {\n\t\t\tif (this.route) {\n\t\t\t\treturn { to: this.route, tabindex: '0' }\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn { href: '#', role: 'button', tabindex: '0' }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the block takes the stacked board look (`layout`).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t * @return {boolean}\n\t\t */\n\t\tisStacked() {\n\t\t\treturn this.layout === 'stacked'\n\t\t},\n\n\t\trootClasses() {\n\t\t\treturn {\n\t\t\t\t// Canonical + legacy, in pairs: the canonical class carries the\n\t\t\t\t// look, the legacy one keeps existing app CSS matching.\n\t\t\t\t// The canonical card is already horizontal, so the legacy pair\n\t\t\t\t// is emitted for app CSS but carries no look of its own.\n\t\t\t\t'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),\n\t\t\t\t'cn-kpi-card--stacked': this.isStacked,\n\t\t\t\t'cn-kpi-card--board': this.isBoardLook,\n\t\t\t\t'cn-stats-block--horizontal': this.horizontal || !this.vertical,\n\t\t\t\t'cn-kpi-card--vertical': this.vertical,\n\t\t\t\t'cn-stats-block--vertical': this.vertical,\n\t\t\t\t'cn-kpi-card--filled': this.filled,\n\t\t\t\t'cn-kpi-card--clickable': this.isInteractive,\n\t\t\t\t'cn-stats-block--clickable': this.isInteractive,\n\t\t\t\t'cn-kpi-card--error': this.hasError,\n\t\t\t\t[`cn-kpi-card--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t\t// An errored tile tints itself. Requiring the consumer to also\n\t\t\t\t// pass `variant=\"error\"` would mean every caller remembering\n\t\t\t\t// two props to express one state, and forgetting the second is\n\t\t\t\t// invisible — a tile that reads \"Unavailable\" in the default\n\t\t\t\t// colour looks like ordinary content.\n\t\t\t\t'cn-stats-block--error': this.hasError,\n\t\t\t\t[`cn-stats-block--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t}\n\t\t},\n\n\t\ticonClasses() {\n\t\t\treturn {\n\t\t\t\t[`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',\n\t\t\t}\n\t\t},\n\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest string through the host's label lookup (`cnTranslate`), so a key\n\t\t * such as \"Mine\" reads in the user's language.\n\t\t *\n\t\t * @param {string} text The text as written in the manifest.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttl(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\tformatBreakdownLabel(key) {\n\t\t\treturn key.charAt(0).toUpperCase() + key.slice(1) + ':'\n\t\t},\n\n\t\tonClick(event) {\n\t\t\t// When route is set, router-link handles navigation — do not emit click\n\t\t\tif (this.route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$emit('click', event)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card's own look — box, body stack, icon circle, title, value, variants,\n * hover — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget. The rules that lived here duplicated it in different numbers\n * (a 1.2rem count row, a 40px icon, hardcoded rgba() tints that ignored a\n * re-themed palette), and that duplication IS how the two KPI looks drifted.\n * What is left below is this component's own extras: the breakdown block, the\n * loading line and the empty line.\n *\n * Those extras are secondary KPI text, so they read the shared\n * `--cn-kpi-label-*` scale rather than carrying sizes of their own — an empty\n * state that is a different size from the label it replaces is the same drift\n * in miniature.\n */\n\n.cn-stats-block__header {\n\tmax-width: 100%;\n\tmin-width: 0;\n}\n\n.cn-stats-block__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-start;\n\tgap: 6px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-stats-block__empty {\n\ttext-align: start;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n}\n\n.cn-stats-block__breakdown {\n\tmargin-top: 4px;\n\tpadding: 8px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tborder: 1px solid var(--color-border);\n\twidth: 100%;\n\ttext-align: start;\n}\n\n.cn-stats-block__breakdown-item {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-stats-block__breakdown-item:last-child {\n\tmargin-bottom: 0;\n}\n\n.cn-stats-block__breakdown-label {\n\tfont-weight: 500;\n\tcolor: var(--color-main-text);\n}\n\n/* The value takes the card's accent, so a breakdown reads as part of the tile\n it sits in rather than as a table that happened to land there. It used to\n carry a `--color-background-hover` chip — on a `--color-background-hover`\n breakdown box, which is to say an invisible chip drawn at the cost of two\n extra declarations and a differently-shaped hit area. The per-key modifiers\n below still win: `invalid`, `deleted` and `published` mean something\n specific and outrank the tile's own colour. */\n.cn-stats-block__breakdown-value {\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n\tcolor: var(--cn-kpi-accent, var(--color-primary-element));\n}\n\n.cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }\n\n.cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }\n\n.cn-stats-block__breakdown-value--published { color: var(--color-element-success); }\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_openBlock","_createBlock","_resolveDynamicComponent","_createElementVNode","_toDisplayString","_createVNode","_createTextVNode"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { getCurrentUser } from '@nextcloud/auth';
|
|
|
2
2
|
import axios__default from '@nextcloud/axios';
|
|
3
3
|
import { generateUrl } from '@nextcloud/router';
|
|
4
4
|
import { tryOnScopeDispose } from '@vueuse/core';
|
|
5
|
-
import { computed, onMounted, onBeforeUnmount,
|
|
5
|
+
import { computed, onMounted, onBeforeUnmount, toValue } from 'vue';
|
|
6
6
|
|
|
7
7
|
// SPDX-License-Identifier: EUPL-1.2
|
|
8
8
|
// Copyright (C) 2026 Conduction B.V.
|
|
@@ -47,14 +47,14 @@ class PermissionError extends Error {
|
|
|
47
47
|
* other path that keeps `objectStore.objects[type][id]` fresh).
|
|
48
48
|
*
|
|
49
49
|
* @param {object} objectStore Pinia store instance (`useObjectStore()` result).
|
|
50
|
-
* @param {string|import('vue').Ref<string
|
|
51
|
-
* @param {string|import('vue').Ref<string
|
|
52
|
-
* @param {string|import('vue').Ref<string
|
|
50
|
+
* @param {string|import('vue').Ref<string>|function(): string} register OR register slug.
|
|
51
|
+
* @param {string|import('vue').Ref<string>|function(): string} schema OR schema slug (also the GraphQL field name).
|
|
52
|
+
* @param {string|import('vue').Ref<string>|function(): string} id Object UUID.
|
|
53
53
|
* @param {object} [options] Optional config.
|
|
54
54
|
* @param {boolean} [options.autoRenew] Renew the lock on a fixed interval while editing + visible.
|
|
55
55
|
* @param {number} [options.renewIntervalMs] Renew every N ms (default 10 min).
|
|
56
56
|
* @param {number} [options.lockDurationSec] Server-side TTL requested on acquire (default 30 min).
|
|
57
|
-
* @param {string|import('vue').Ref<string
|
|
57
|
+
* @param {string|import('vue').Ref<string>|function(): string} [options.schemaSlug] The schema slug for the LOCK URL, when `schema` is the object-cache key rather than the slug (CnDetailPage passes `<register>-<schema>` as the key). Defaults to `schema`.
|
|
58
58
|
* @return {{
|
|
59
59
|
* locked: import('vue').ComputedRef<boolean>,
|
|
60
60
|
* lockedByMe: import('vue').ComputedRef<boolean>,
|
|
@@ -63,6 +63,7 @@ class PermissionError extends Error {
|
|
|
63
63
|
* acquire: () => Promise<void>,
|
|
64
64
|
* release: () => Promise<void>
|
|
65
65
|
* }} Reactive lock state + actions.
|
|
66
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-lock/spec.md
|
|
66
67
|
*/
|
|
67
68
|
function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
68
69
|
const autoRenew = options.autoRenew !== false;
|
|
@@ -75,14 +76,23 @@ function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
75
76
|
const isVisible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden';
|
|
76
77
|
let renewTimer = null;
|
|
77
78
|
|
|
79
|
+
/*
|
|
80
|
+
* 🔴 `toValue`, NOT `unref`. CnDetailPage passes GETTER FUNCTIONS
|
|
81
|
+
* (`() => props.objectId`) so the lock follows the route. `unref` returns a
|
|
82
|
+
* function untouched, so the getter's SOURCE was interpolated into the URL:
|
|
83
|
+
* every detail page sent `/api/objects/()=>pn/()=>Ct.schema||Bn/()=>Ct.objectId/lock`
|
|
84
|
+
* and got a 404, and the cache lookup used the function as its key, so the
|
|
85
|
+
* banner never saw a lock either. `toValue` reads a plain value, a ref and a
|
|
86
|
+
* getter alike.
|
|
87
|
+
*/
|
|
78
88
|
function readType() {
|
|
79
|
-
return
|
|
89
|
+
return toValue(schema)
|
|
80
90
|
}
|
|
81
91
|
function readId() {
|
|
82
|
-
return
|
|
92
|
+
return toValue(id)
|
|
83
93
|
}
|
|
84
94
|
function readRegister() {
|
|
85
|
-
return
|
|
95
|
+
return toValue(register)
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
function readSelfLock() {
|
|
@@ -128,7 +138,22 @@ function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
128
138
|
});
|
|
129
139
|
|
|
130
140
|
function readSchemaSlug() {
|
|
131
|
-
return
|
|
141
|
+
return toValue(options.schemaSlug ?? schema)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether register, schema slug and object id are all known.
|
|
146
|
+
*
|
|
147
|
+
* A detail page mounts before its route params or object have resolved.
|
|
148
|
+
* Without this a lock call would go out with `undefined` in the path, a
|
|
149
|
+
* request OpenRegister can only answer with a 404.
|
|
150
|
+
*
|
|
151
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-lock/spec.md
|
|
152
|
+
* @return {boolean} True when a lock URL can be built.
|
|
153
|
+
*/
|
|
154
|
+
function hasTarget() {
|
|
155
|
+
return [readRegister(), readSchemaSlug(), readId()]
|
|
156
|
+
.every((part) => typeof part === 'string' ? part !== '' : typeof part === 'number')
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
/**
|
|
@@ -177,6 +202,9 @@ function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
async function acquire() {
|
|
205
|
+
if (!hasTarget()) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
180
208
|
try {
|
|
181
209
|
await axios__default.post(endpoint(), { duration: lockDurationSec });
|
|
182
210
|
} catch (e) {
|
|
@@ -209,6 +237,9 @@ function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
209
237
|
|
|
210
238
|
async function release() {
|
|
211
239
|
stopRenewTimer();
|
|
240
|
+
if (!hasTarget()) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
212
243
|
try {
|
|
213
244
|
await axios__default.post(unlockEndpoint());
|
|
214
245
|
} catch (e) {
|
|
@@ -252,7 +283,7 @@ function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
252
283
|
}
|
|
253
284
|
|
|
254
285
|
function beaconRelease() {
|
|
255
|
-
if (!lockedByMe.value) {
|
|
286
|
+
if (!lockedByMe.value || !hasTarget()) {
|
|
256
287
|
return
|
|
257
288
|
}
|
|
258
289
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectLock.js","sources":["../../../src/composables/useObjectLock.js"],"sourcesContent":["// SPDX-License-Identifier: EUPL-1.2\n// Copyright (C) 2026 Conduction B.V.\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { tryOnScopeDispose } from '@vueuse/core'\nimport { computed, onBeforeUnmount, onMounted, unref } from 'vue'\n\n/**\n * Thrown when an `acquire()` POST returns 409 (Conflict) or 423\n * (Locked). Carries the conflicting user info so the UI can render\n * a \"Locked by X\" banner without a second fetch.\n */\nexport class LockConflictError extends Error {\n\t/**\n\t * @param {string} message Human-readable description.\n\t * @param {object|null} info Lock metadata (`{ user, displayName, expiresAt }`) parsed from the response.\n\t */\n\tconstructor(message, info = null) {\n\t\tsuper(message)\n\t\tthis.name = 'LockConflictError'\n\t\tthis.lockedBy = info?.user ?? info?.displayName ?? null\n\t\tthis.expiresAt = info?.expiresAt ?? null\n\t}\n}\n\n/**\n * Thrown when an `acquire()` / `release()` call returns 401 / 403.\n * Consumers may fall back to \"edit without lock\" UX.\n */\nexport class PermissionError extends Error {\n\tconstructor(message) {\n\t\tsuper(message)\n\t\tthis.name = 'PermissionError'\n\t}\n}\n\n/**\n * useObjectLock — manage the OR pessimistic lock for a single object.\n *\n * Reactive lock state is **read from the object cache** populated by\n * `liveUpdatesPlugin`, NOT from a polling lock endpoint. This means\n * remote lock acquisitions are visible without explicit refetch as\n * long as the consumer also uses `useObjectSubscription` (or any\n * other path that keeps `objectStore.objects[type][id]` fresh).\n *\n * @param {object} objectStore Pinia store instance (`useObjectStore()` result).\n * @param {string|import('vue').Ref<string>} register OR register slug.\n * @param {string|import('vue').Ref<string>} schema OR schema slug (also the GraphQL field name).\n * @param {string|import('vue').Ref<string>} id Object UUID.\n * @param {object} [options] Optional config.\n * @param {boolean} [options.autoRenew] Renew the lock on a fixed interval while editing + visible.\n * @param {number} [options.renewIntervalMs] Renew every N ms (default 10 min).\n * @param {number} [options.lockDurationSec] Server-side TTL requested on acquire (default 30 min).\n * @param {string|import('vue').Ref<string>} [options.schemaSlug] The schema slug for the LOCK URL, when `schema` is the object-cache key rather than the slug (CnDetailPage passes `<register>-<schema>` as the key). Defaults to `schema`.\n * @return {{\n * locked: import('vue').ComputedRef<boolean>,\n * lockedByMe: import('vue').ComputedRef<boolean>,\n * lockedBy: import('vue').ComputedRef<string|null>,\n * expiresAt: import('vue').ComputedRef<Date|null>,\n * acquire: () => Promise<void>,\n * release: () => Promise<void>\n * }} Reactive lock state + actions.\n */\nexport function useObjectLock(objectStore, register, schema, id, options = {}) {\n\tconst autoRenew = options.autoRenew !== false\n\tconst renewIntervalMs = options.renewIntervalMs ?? 600000\n\tconst lockDurationSec = options.lockDurationSec ?? 1800\n\n\t// Use native document.visibilityState directly to avoid pinning a\n\t// minimum @vueuse/core version (`useDocumentVisibility` landed in a\n\t// later cut and isn't available everywhere).\n\tconst isVisible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden'\n\tlet renewTimer = null\n\n\tfunction readType() {\n\t\treturn unref(schema)\n\t}\n\tfunction readId() {\n\t\treturn unref(id)\n\t}\n\tfunction readRegister() {\n\t\treturn unref(register)\n\t}\n\n\tfunction readSelfLock() {\n\t\tconst t = readType()\n\t\tconst i = readId()\n\t\tif (!t || !i) {\n\t\t\treturn null\n\t\t}\n\t\tconst obj = objectStore.objects?.[t]?.[i]\n\t\tconst self = obj?.['@self'] ?? obj\n\t\treturn self?.locked ?? null\n\t}\n\n\tconst locked = computed(() => {\n\t\tconst l = readSelfLock()\n\t\tif (!l) {\n\t\t\treturn false\n\t\t}\n\t\tif (l.expiresAt) {\n\t\t\tconst exp = new Date(l.expiresAt).getTime()\n\t\t\tif (Number.isFinite(exp) && exp <= Date.now()) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n\n\tconst lockedBy = computed(() => readSelfLock()?.user ?? readSelfLock()?.displayName ?? null)\n\n\tconst lockedByMe = computed(() => {\n\t\tconst me = getCurrentUser()?.uid\n\t\tconst lb = readSelfLock()?.user\n\t\treturn Boolean(me && lb && me === lb)\n\t})\n\n\tconst expiresAt = computed(() => {\n\t\tconst raw = readSelfLock()?.expiresAt\n\t\tif (!raw) {\n\t\t\treturn null\n\t\t}\n\t\tconst d = new Date(raw)\n\t\treturn Number.isNaN(d.getTime()) ? null : d\n\t})\n\n\tfunction readSchemaSlug() {\n\t\treturn unref(options.schemaSlug ?? schema)\n\t}\n\n\t/**\n\t * The object's own URL, without the lock verb.\n\t *\n\t * 🔴 THE SLUG AND THE STORE KEY ARE NOT THE SAME STRING, AND THEY USED TO\n\t * BE. `schema` doubles as the key the object cache is read under, and\n\t * CnDetailPage passes it `objectType`, which is `<register>-<schema>`. Put\n\t * straight into the URL that produced\n\t * `/api/objects/dossiq/dossiq-case/<id>/lock`, a schema slug no register\n\t * has, so the acquire 404ed on every manifest-driven detail page and the\n\t * lock was never taken. `options.schemaSlug` is the URL half and defaults\n\t * to `schema`, so a caller that passes one string for both is unaffected.\n\t *\n\t * @return {string} The object's base url.\n\t */\n\tfunction objectUrl() {\n\t\tconst r = readRegister()\n\t\tconst s = readSchemaSlug()\n\t\tconst i = readId()\n\t\treturn generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(i)}`)\n\t}\n\n\tfunction endpoint() {\n\t\treturn `${objectUrl()}/lock`\n\t}\n\n\t/**\n\t * Where a lock is handed back.\n\t *\n\t * 🔴 IT IS A POST TO `/unlock`, AND IT USED TO BE `DELETE /lock`.\n\t * OpenRegister declares exactly two lock routes, `objects#lock` and\n\t * `objects#unlock`, both POST (`appinfo/routes.php`, verified 2026-09-18\n\t * against `development`); there is no DELETE on `/lock` and there never\n\t * was. So every release 404ed, and `release()` reads a 404 as \"already\n\t * released; idempotent\" and returns without a word. The lock outlived the\n\t * editor, the holder was told nothing, and the next person was refused by a\n\t * lock whose owner had closed the page twenty minutes earlier. That is the\n\t * silent no-op this line ends, and it is why the release test asserts the\n\t * URL and the METHOD rather than only that the promise resolved.\n\t *\n\t * @return {string} The unlock url.\n\t */\n\tfunction unlockEndpoint() {\n\t\treturn `${objectUrl()}/unlock`\n\t}\n\n\tasync function acquire() {\n\t\ttry {\n\t\t\tawait axios.post(endpoint(), { duration: lockDurationSec })\n\t\t} catch (e) {\n\t\t\tconst status = e?.response?.status\n\t\t\tconst data = e?.response?.data\n\t\t\tif (status === 409 || status === 423) {\n\t\t\t\tthrow new LockConflictError(\n\t\t\t\t\tdata?.message || 'Object is locked by another user',\n\t\t\t\t\tdata?.lock || data,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (status === 401 || status === 403) {\n\t\t\t\tthrow new PermissionError(data?.message || 'No permission to lock this object')\n\t\t\t}\n\t\t\tthrow e\n\t\t}\n\t\t// Refresh the cached object so `@self.locked` is populated;\n\t\t// liveUpdatesPlugin will also fire a refetch on the\n\t\t// or-object-{uuid} event, but doing it here avoids a render\n\t\t// gap between acquire() resolution and the push frame.\n\t\ttry {\n\t\t\tawait objectStore.fetchObject(readType(), readId())\n\t\t} catch {\n\t\t\t// swallow — the lock is held; the cache will catch up\n\t\t}\n\t\tif (autoRenew) {\n\t\t\tstartRenewTimer()\n\t\t}\n\t}\n\n\tasync function release() {\n\t\tstopRenewTimer()\n\t\ttry {\n\t\t\tawait axios.post(unlockEndpoint())\n\t\t} catch (e) {\n\t\t\tconst status = e?.response?.status\n\t\t\tif (status === 404) {\n\t\t\t\treturn\n\t\t\t} // already released; idempotent\n\t\t\tif (status === 401 || status === 403) {\n\t\t\t\tthrow new PermissionError(e?.response?.data?.message || 'No permission to release this lock')\n\t\t\t}\n\t\t\tthrow e\n\t\t}\n\t\ttry {\n\t\t\tawait objectStore.fetchObject(readType(), readId())\n\t\t} catch { /* ignore */ }\n\t}\n\n\tfunction startRenewTimer() {\n\t\tstopRenewTimer()\n\t\tif (!autoRenew) {\n\t\t\treturn\n\t\t}\n\t\trenewTimer = setInterval(() => {\n\t\t\tif (!isVisible()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!lockedByMe.value) {\n\t\t\t\tstopRenewTimer()\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Re-issue acquire; idempotent on the server (resets TTL).\n\t\t\tacquire().catch(() => stopRenewTimer())\n\t\t}, renewIntervalMs)\n\t}\n\n\tfunction stopRenewTimer() {\n\t\tif (renewTimer) {\n\t\t\tclearInterval(renewTimer)\n\t\t\trenewTimer = null\n\t\t}\n\t}\n\n\tfunction beaconRelease() {\n\t\tif (!lockedByMe.value) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\t// A beacon is always a POST, which is what `/unlock` takes: the\n\t\t\t// old `?_method=DELETE` was a query parameter on a URL that had no\n\t\t\t// DELETE route to reach in the first place.\n\t\t\tnavigator.sendBeacon?.(unlockEndpoint())\n\t\t} catch { /* best-effort */ }\n\t}\n\n\tonMounted(() => {\n\t\twindow.addEventListener('beforeunload', beaconRelease)\n\t})\n\n\tonBeforeUnmount(() => {\n\t\twindow.removeEventListener('beforeunload', beaconRelease)\n\t})\n\n\ttryOnScopeDispose(() => {\n\t\tstopRenewTimer()\n\t\tif (lockedByMe.value) {\n\t\t\t// Fire-and-forget; the Vue scope is gone, no point awaiting.\n\t\t\trelease().catch(() => {})\n\t\t}\n\t})\n\n\treturn { locked, lockedByMe, lockedBy, expiresAt, acquire, release }\n}\n"],"names":["axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useObjectLock.js","sources":["../../../src/composables/useObjectLock.js"],"sourcesContent":["// SPDX-License-Identifier: EUPL-1.2\n// Copyright (C) 2026 Conduction B.V.\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { tryOnScopeDispose } from '@vueuse/core'\nimport { computed, onBeforeUnmount, onMounted, toValue } from 'vue'\n\n/**\n * Thrown when an `acquire()` POST returns 409 (Conflict) or 423\n * (Locked). Carries the conflicting user info so the UI can render\n * a \"Locked by X\" banner without a second fetch.\n */\nexport class LockConflictError extends Error {\n\t/**\n\t * @param {string} message Human-readable description.\n\t * @param {object|null} info Lock metadata (`{ user, displayName, expiresAt }`) parsed from the response.\n\t */\n\tconstructor(message, info = null) {\n\t\tsuper(message)\n\t\tthis.name = 'LockConflictError'\n\t\tthis.lockedBy = info?.user ?? info?.displayName ?? null\n\t\tthis.expiresAt = info?.expiresAt ?? null\n\t}\n}\n\n/**\n * Thrown when an `acquire()` / `release()` call returns 401 / 403.\n * Consumers may fall back to \"edit without lock\" UX.\n */\nexport class PermissionError extends Error {\n\tconstructor(message) {\n\t\tsuper(message)\n\t\tthis.name = 'PermissionError'\n\t}\n}\n\n/**\n * useObjectLock — manage the OR pessimistic lock for a single object.\n *\n * Reactive lock state is **read from the object cache** populated by\n * `liveUpdatesPlugin`, NOT from a polling lock endpoint. This means\n * remote lock acquisitions are visible without explicit refetch as\n * long as the consumer also uses `useObjectSubscription` (or any\n * other path that keeps `objectStore.objects[type][id]` fresh).\n *\n * @param {object} objectStore Pinia store instance (`useObjectStore()` result).\n * @param {string|import('vue').Ref<string>|function(): string} register OR register slug.\n * @param {string|import('vue').Ref<string>|function(): string} schema OR schema slug (also the GraphQL field name).\n * @param {string|import('vue').Ref<string>|function(): string} id Object UUID.\n * @param {object} [options] Optional config.\n * @param {boolean} [options.autoRenew] Renew the lock on a fixed interval while editing + visible.\n * @param {number} [options.renewIntervalMs] Renew every N ms (default 10 min).\n * @param {number} [options.lockDurationSec] Server-side TTL requested on acquire (default 30 min).\n * @param {string|import('vue').Ref<string>|function(): string} [options.schemaSlug] The schema slug for the LOCK URL, when `schema` is the object-cache key rather than the slug (CnDetailPage passes `<register>-<schema>` as the key). Defaults to `schema`.\n * @return {{\n * locked: import('vue').ComputedRef<boolean>,\n * lockedByMe: import('vue').ComputedRef<boolean>,\n * lockedBy: import('vue').ComputedRef<string|null>,\n * expiresAt: import('vue').ComputedRef<Date|null>,\n * acquire: () => Promise<void>,\n * release: () => Promise<void>\n * }} Reactive lock state + actions.\n * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-lock/spec.md\n */\nexport function useObjectLock(objectStore, register, schema, id, options = {}) {\n\tconst autoRenew = options.autoRenew !== false\n\tconst renewIntervalMs = options.renewIntervalMs ?? 600000\n\tconst lockDurationSec = options.lockDurationSec ?? 1800\n\n\t// Use native document.visibilityState directly to avoid pinning a\n\t// minimum @vueuse/core version (`useDocumentVisibility` landed in a\n\t// later cut and isn't available everywhere).\n\tconst isVisible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden'\n\tlet renewTimer = null\n\n\t/*\n\t * 🔴 `toValue`, NOT `unref`. CnDetailPage passes GETTER FUNCTIONS\n\t * (`() => props.objectId`) so the lock follows the route. `unref` returns a\n\t * function untouched, so the getter's SOURCE was interpolated into the URL:\n\t * every detail page sent `/api/objects/()=>pn/()=>Ct.schema||Bn/()=>Ct.objectId/lock`\n\t * and got a 404, and the cache lookup used the function as its key, so the\n\t * banner never saw a lock either. `toValue` reads a plain value, a ref and a\n\t * getter alike.\n\t */\n\tfunction readType() {\n\t\treturn toValue(schema)\n\t}\n\tfunction readId() {\n\t\treturn toValue(id)\n\t}\n\tfunction readRegister() {\n\t\treturn toValue(register)\n\t}\n\n\tfunction readSelfLock() {\n\t\tconst t = readType()\n\t\tconst i = readId()\n\t\tif (!t || !i) {\n\t\t\treturn null\n\t\t}\n\t\tconst obj = objectStore.objects?.[t]?.[i]\n\t\tconst self = obj?.['@self'] ?? obj\n\t\treturn self?.locked ?? null\n\t}\n\n\tconst locked = computed(() => {\n\t\tconst l = readSelfLock()\n\t\tif (!l) {\n\t\t\treturn false\n\t\t}\n\t\tif (l.expiresAt) {\n\t\t\tconst exp = new Date(l.expiresAt).getTime()\n\t\t\tif (Number.isFinite(exp) && exp <= Date.now()) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n\n\tconst lockedBy = computed(() => readSelfLock()?.user ?? readSelfLock()?.displayName ?? null)\n\n\tconst lockedByMe = computed(() => {\n\t\tconst me = getCurrentUser()?.uid\n\t\tconst lb = readSelfLock()?.user\n\t\treturn Boolean(me && lb && me === lb)\n\t})\n\n\tconst expiresAt = computed(() => {\n\t\tconst raw = readSelfLock()?.expiresAt\n\t\tif (!raw) {\n\t\t\treturn null\n\t\t}\n\t\tconst d = new Date(raw)\n\t\treturn Number.isNaN(d.getTime()) ? null : d\n\t})\n\n\tfunction readSchemaSlug() {\n\t\treturn toValue(options.schemaSlug ?? schema)\n\t}\n\n\t/**\n\t * Whether register, schema slug and object id are all known.\n\t *\n\t * A detail page mounts before its route params or object have resolved.\n\t * Without this a lock call would go out with `undefined` in the path, a\n\t * request OpenRegister can only answer with a 404.\n\t *\n\t * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-lock/spec.md\n\t * @return {boolean} True when a lock URL can be built.\n\t */\n\tfunction hasTarget() {\n\t\treturn [readRegister(), readSchemaSlug(), readId()]\n\t\t\t.every((part) => typeof part === 'string' ? part !== '' : typeof part === 'number')\n\t}\n\n\t/**\n\t * The object's own URL, without the lock verb.\n\t *\n\t * 🔴 THE SLUG AND THE STORE KEY ARE NOT THE SAME STRING, AND THEY USED TO\n\t * BE. `schema` doubles as the key the object cache is read under, and\n\t * CnDetailPage passes it `objectType`, which is `<register>-<schema>`. Put\n\t * straight into the URL that produced\n\t * `/api/objects/dossiq/dossiq-case/<id>/lock`, a schema slug no register\n\t * has, so the acquire 404ed on every manifest-driven detail page and the\n\t * lock was never taken. `options.schemaSlug` is the URL half and defaults\n\t * to `schema`, so a caller that passes one string for both is unaffected.\n\t *\n\t * @return {string} The object's base url.\n\t */\n\tfunction objectUrl() {\n\t\tconst r = readRegister()\n\t\tconst s = readSchemaSlug()\n\t\tconst i = readId()\n\t\treturn generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(i)}`)\n\t}\n\n\tfunction endpoint() {\n\t\treturn `${objectUrl()}/lock`\n\t}\n\n\t/**\n\t * Where a lock is handed back.\n\t *\n\t * 🔴 IT IS A POST TO `/unlock`, AND IT USED TO BE `DELETE /lock`.\n\t * OpenRegister declares exactly two lock routes, `objects#lock` and\n\t * `objects#unlock`, both POST (`appinfo/routes.php`, verified 2026-09-18\n\t * against `development`); there is no DELETE on `/lock` and there never\n\t * was. So every release 404ed, and `release()` reads a 404 as \"already\n\t * released; idempotent\" and returns without a word. The lock outlived the\n\t * editor, the holder was told nothing, and the next person was refused by a\n\t * lock whose owner had closed the page twenty minutes earlier. That is the\n\t * silent no-op this line ends, and it is why the release test asserts the\n\t * URL and the METHOD rather than only that the promise resolved.\n\t *\n\t * @return {string} The unlock url.\n\t */\n\tfunction unlockEndpoint() {\n\t\treturn `${objectUrl()}/unlock`\n\t}\n\n\tasync function acquire() {\n\t\tif (!hasTarget()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait axios.post(endpoint(), { duration: lockDurationSec })\n\t\t} catch (e) {\n\t\t\tconst status = e?.response?.status\n\t\t\tconst data = e?.response?.data\n\t\t\tif (status === 409 || status === 423) {\n\t\t\t\tthrow new LockConflictError(\n\t\t\t\t\tdata?.message || 'Object is locked by another user',\n\t\t\t\t\tdata?.lock || data,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (status === 401 || status === 403) {\n\t\t\t\tthrow new PermissionError(data?.message || 'No permission to lock this object')\n\t\t\t}\n\t\t\tthrow e\n\t\t}\n\t\t// Refresh the cached object so `@self.locked` is populated;\n\t\t// liveUpdatesPlugin will also fire a refetch on the\n\t\t// or-object-{uuid} event, but doing it here avoids a render\n\t\t// gap between acquire() resolution and the push frame.\n\t\ttry {\n\t\t\tawait objectStore.fetchObject(readType(), readId())\n\t\t} catch {\n\t\t\t// swallow — the lock is held; the cache will catch up\n\t\t}\n\t\tif (autoRenew) {\n\t\t\tstartRenewTimer()\n\t\t}\n\t}\n\n\tasync function release() {\n\t\tstopRenewTimer()\n\t\tif (!hasTarget()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait axios.post(unlockEndpoint())\n\t\t} catch (e) {\n\t\t\tconst status = e?.response?.status\n\t\t\tif (status === 404) {\n\t\t\t\treturn\n\t\t\t} // already released; idempotent\n\t\t\tif (status === 401 || status === 403) {\n\t\t\t\tthrow new PermissionError(e?.response?.data?.message || 'No permission to release this lock')\n\t\t\t}\n\t\t\tthrow e\n\t\t}\n\t\ttry {\n\t\t\tawait objectStore.fetchObject(readType(), readId())\n\t\t} catch { /* ignore */ }\n\t}\n\n\tfunction startRenewTimer() {\n\t\tstopRenewTimer()\n\t\tif (!autoRenew) {\n\t\t\treturn\n\t\t}\n\t\trenewTimer = setInterval(() => {\n\t\t\tif (!isVisible()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!lockedByMe.value) {\n\t\t\t\tstopRenewTimer()\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Re-issue acquire; idempotent on the server (resets TTL).\n\t\t\tacquire().catch(() => stopRenewTimer())\n\t\t}, renewIntervalMs)\n\t}\n\n\tfunction stopRenewTimer() {\n\t\tif (renewTimer) {\n\t\t\tclearInterval(renewTimer)\n\t\t\trenewTimer = null\n\t\t}\n\t}\n\n\tfunction beaconRelease() {\n\t\tif (!lockedByMe.value || !hasTarget()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\t// A beacon is always a POST, which is what `/unlock` takes: the\n\t\t\t// old `?_method=DELETE` was a query parameter on a URL that had no\n\t\t\t// DELETE route to reach in the first place.\n\t\t\tnavigator.sendBeacon?.(unlockEndpoint())\n\t\t} catch { /* best-effort */ }\n\t}\n\n\tonMounted(() => {\n\t\twindow.addEventListener('beforeunload', beaconRelease)\n\t})\n\n\tonBeforeUnmount(() => {\n\t\twindow.removeEventListener('beforeunload', beaconRelease)\n\t})\n\n\ttryOnScopeDispose(() => {\n\t\tstopRenewTimer()\n\t\tif (lockedByMe.value) {\n\t\t\t// Fire-and-forget; the Vue scope is gone, no point awaiting.\n\t\t\trelease().catch(() => {})\n\t\t}\n\t})\n\n\treturn { locked, lockedByMe, lockedBy, expiresAt, acquire, release }\n}\n"],"names":["axios"],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* `v-cn-select-aria`: puts `aria-invalid`, `aria-describedby` and
|
|
6
|
+
* `aria-required` on the search input of an NcSelect.
|
|
7
|
+
*
|
|
8
|
+
* NcSelect forwards no attributes to its input: an attribute bound on it lands
|
|
9
|
+
* on the wrapping `.v-select` div, where a screen reader never reads it. The
|
|
10
|
+
* input is the combobox, so the attributes belong there. This directive sets
|
|
11
|
+
* them on that input after every render, and removes the ones that go away
|
|
12
|
+
* (a field that becomes valid loses `aria-invalid`).
|
|
13
|
+
*
|
|
14
|
+
* Usage: `<NcSelect v-cn-select-aria="{ 'aria-invalid': 'true', 'aria-describedby': 'err-id' }" />`
|
|
15
|
+
* A `null` or `undefined` value does nothing, so a caller can bind it
|
|
16
|
+
* unconditionally.
|
|
17
|
+
*
|
|
18
|
+
* @spec openspec/changes/screens-form-parity/tasks.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ARIA_ATTRIBUTES = ['aria-invalid', 'aria-describedby', 'aria-required'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply the bound attributes to the combobox input inside the element.
|
|
25
|
+
*
|
|
26
|
+
* @param {HTMLElement} el The element the directive sits on (the select root).
|
|
27
|
+
* @param {{value: object|null|undefined}} binding The directive binding.
|
|
28
|
+
* @return {void}
|
|
29
|
+
*/
|
|
30
|
+
function apply(el, binding) {
|
|
31
|
+
const attributes = binding.value;
|
|
32
|
+
if (!attributes || typeof el.querySelector !== 'function') {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
const input = el.querySelector('input.vs__search') || el.querySelector('input');
|
|
36
|
+
if (!input) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
for (const name of ARIA_ATTRIBUTES) {
|
|
40
|
+
const value = attributes[name];
|
|
41
|
+
if (value === undefined || value === null || value === false) {
|
|
42
|
+
input.removeAttribute(name);
|
|
43
|
+
} else {
|
|
44
|
+
input.setAttribute(name, String(value));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cnSelectAria = {
|
|
50
|
+
mounted: apply,
|
|
51
|
+
updated: apply,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { cnSelectAria, cnSelectAria as default };
|
|
55
|
+
//# sourceMappingURL=cnSelectAria.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cnSelectAria.js","sources":["../../../src/directives/cnSelectAria.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * `v-cn-select-aria`: puts `aria-invalid`, `aria-describedby` and\n * `aria-required` on the search input of an NcSelect.\n *\n * NcSelect forwards no attributes to its input: an attribute bound on it lands\n * on the wrapping `.v-select` div, where a screen reader never reads it. The\n * input is the combobox, so the attributes belong there. This directive sets\n * them on that input after every render, and removes the ones that go away\n * (a field that becomes valid loses `aria-invalid`).\n *\n * Usage: `<NcSelect v-cn-select-aria=\"{ 'aria-invalid': 'true', 'aria-describedby': 'err-id' }\" />`\n * A `null` or `undefined` value does nothing, so a caller can bind it\n * unconditionally.\n *\n * @spec openspec/changes/screens-form-parity/tasks.md\n */\n\nconst ARIA_ATTRIBUTES = ['aria-invalid', 'aria-describedby', 'aria-required']\n\n/**\n * Apply the bound attributes to the combobox input inside the element.\n *\n * @param {HTMLElement} el The element the directive sits on (the select root).\n * @param {{value: object|null|undefined}} binding The directive binding.\n * @return {void}\n */\nfunction apply(el, binding) {\n\tconst attributes = binding.value\n\tif (!attributes || typeof el.querySelector !== 'function') {\n\t\treturn\n\t}\n\tconst input = el.querySelector('input.vs__search') || el.querySelector('input')\n\tif (!input) {\n\t\treturn\n\t}\n\tfor (const name of ARIA_ATTRIBUTES) {\n\t\tconst value = attributes[name]\n\t\tif (value === undefined || value === null || value === false) {\n\t\t\tinput.removeAttribute(name)\n\t\t} else {\n\t\t\tinput.setAttribute(name, String(value))\n\t\t}\n\t}\n}\n\nexport const cnSelectAria = {\n\tmounted: apply,\n\tupdated: apply,\n}\n\nexport default cnSelectAria\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,eAAe,GAAG,CAAC,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAC;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE;AAC5B,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAK;AACjC,CAAC,IAAI,CAAC,UAAU,IAAI,OAAO,EAAE,CAAC,aAAa,KAAK,UAAU,EAAE;AAC5D,EAAE,MAAM;AACR,EAAE;AACF,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC,aAAa,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC,aAAa,CAAC,OAAO,EAAC;AAChF,CAAC,IAAI,CAAC,KAAK,EAAE;AACb,EAAE,MAAM;AACR,EAAE;AACF,CAAC,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE;AACrC,EAAE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAC;AAChC,EAAE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,EAAE;AAChE,GAAG,KAAK,CAAC,eAAe,CAAC,IAAI,EAAC;AAC9B,GAAG,MAAM;AACT,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAC;AAC1C,GAAG;AACH,EAAE;AACF,CAAC;AACD;AACY,MAAC,YAAY,GAAG;AAC5B,CAAC,OAAO,EAAE,KAAK;AACf,CAAC,OAAO,EAAE,KAAK;AACf;;;;"}
|
package/dist/esm/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export { BSN_ERROR_CHECKSUM, BSN_ERROR_LENGTH, maskBsn, validateBsn } from './ut
|
|
|
27
27
|
export { registerTranslations } from './l10n/index.js';
|
|
28
28
|
export { CnHttpError, cnFetch, cnFetchJson, configureCnFetch } from './utils/cnFetch.js';
|
|
29
29
|
export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './utils/objectLock.js';
|
|
30
|
+
export { dispatchObjectsChanged } from './utils/objectSignals.js';
|
|
30
31
|
export { validateManifest, validateManifestV2 } from './utils/validateManifest.js';
|
|
31
32
|
export { clearResolveCache, resolveManifestSentinels } from './utils/resolveManifestSentinels.js';
|
|
32
33
|
export { clearRouteSentinelWarnings, resolveRouteSentinels } from './utils/resolveRouteSentinels.js';
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/en.json.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
var translations = {
|
|
2
|
+
"Linked items": "Linked items",
|
|
3
|
+
"Still loading: {sections}": "Still loading: {sections}",
|
|
4
|
+
"Could not load {section}.": "Could not load {section}.",
|
|
5
|
+
"Organisation credentials let apps act for your organisation. Keepiq, the credential vault of Nextcloud, keeps the secret. The app never holds it. You choose which apps may use each credential.": "Organisation credentials let apps act for your organisation. Keepiq, the credential vault of Nextcloud, keeps the secret. The app never holds it. You choose which apps may use each credential.",
|
|
6
|
+
"Apps sometimes act for you on an external service. You give the secret to Nextcloud once, and Keepiq keeps it in an encrypted vault. Apps make the call through Keepiq and never see the secret. You choose which apps may use each credential. Share one across apps, or keep one per app.": "Apps sometimes act for you on an external service. You give the secret to Nextcloud once, and Keepiq keeps it in an encrypted vault. Apps make the call through Keepiq and never see the secret. You choose which apps may use each credential. Share one across apps, or keep one per app.",
|
|
2
7
|
"Show as": "Show as",
|
|
3
8
|
Board: "Board",
|
|
4
9
|
Calendar: "Calendar",
|
|
@@ -1283,7 +1288,8 @@ var translations = {
|
|
|
1283
1288
|
"Development environment": "Development environment",
|
|
1284
1289
|
"Test environment": "Test environment",
|
|
1285
1290
|
"Acceptance environment": "Acceptance environment",
|
|
1286
|
-
"About {label}": "About {label}"
|
|
1291
|
+
"About {label}": "About {label}",
|
|
1292
|
+
"Could not load the form for this item.": "Could not load the form for this item."
|
|
1287
1293
|
};
|
|
1288
1294
|
var plurals = {
|
|
1289
1295
|
"%n year": [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/nl.json.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
var translations = {
|
|
2
|
+
"Linked items": "Gekoppelde items",
|
|
3
|
+
"Still loading: {sections}": "Nog aan het laden: {sections}",
|
|
4
|
+
"Could not load {section}.": "{section} kon niet worden geladen.",
|
|
5
|
+
"Organisation credentials let apps act for your organisation. Keepiq, the credential vault of Nextcloud, keeps the secret. The app never holds it. You choose which apps may use each credential.": "Met inloggegevens van de organisatie handelen apps namens je organisatie. Keepiq, de kluis voor inloggegevens van Nextcloud, bewaart het geheim. De app krijgt het nooit in handen. Je kiest welke apps elk inloggegeven mogen gebruiken.",
|
|
6
|
+
"Apps sometimes act for you on an external service. You give the secret to Nextcloud once, and Keepiq keeps it in an encrypted vault. Apps make the call through Keepiq and never see the secret. You choose which apps may use each credential. Share one across apps, or keep one per app.": "Apps handelen soms namens jou bij een externe dienst. Je geeft het geheim één keer aan Nextcloud, en Keepiq bewaart het in een versleutelde kluis. Apps doen de aanvraag via Keepiq en zien het geheim nooit. Je kiest welke apps elk inloggegeven mogen gebruiken. Deel er één tussen apps, of houd er één per app.",
|
|
2
7
|
"Show as": "Toon als",
|
|
3
8
|
Board: "Bord",
|
|
4
9
|
Calendar: "Kalender",
|
|
@@ -170,7 +175,7 @@ var translations = {
|
|
|
170
175
|
"This integration is not set up yet. Configure a connection to start using it.": "Deze integratie is nog niet ingesteld. Configureer een verbinding om hem te gebruiken.",
|
|
171
176
|
"This integration is not set up yet. Install and enable the {app} app to start using it.": "Deze integratie is nog niet ingesteld. Installeer en activeer de app {app} om hem te gebruiken.",
|
|
172
177
|
"Set up {app}": "{app} instellen",
|
|
173
|
-
"Add {type}": "
|
|
178
|
+
"Add {type}": "{type} toevoegen",
|
|
174
179
|
"Add new contact": "Nieuw contact toevoegen",
|
|
175
180
|
"Link contact": "Contact koppelen",
|
|
176
181
|
"Search contacts": "Contacten zoeken",
|
|
@@ -1334,7 +1339,8 @@ var translations = {
|
|
|
1334
1339
|
"Development environment": "Ontwikkelomgeving",
|
|
1335
1340
|
"Test environment": "Testomgeving",
|
|
1336
1341
|
"Acceptance environment": "Acceptatieomgeving",
|
|
1337
|
-
"About {label}": "Over {label}"
|
|
1342
|
+
"About {label}": "Over {label}",
|
|
1343
|
+
"Could not load the form for this item.": "Kon het formulier voor dit item niet laden."
|
|
1338
1344
|
};
|
|
1339
1345
|
var plurals = {
|
|
1340
1346
|
"%n year": [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|