@conduction/nextcloud-vue 2.56.0-beta.34 → 2.56.0-beta.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +5 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +16 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +38 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +525 -318
- package/dist/esm/utils/buildManifest.js +37 -3
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +2250 -708
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +525 -318
- package/l10n/en.json +16 -1
- package/l10n/nl.json +38 -1
- package/package.json +1 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
- package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +31 -0
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +23 -2
- package/src/utils/buildManifest.js +36 -2
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -2,7 +2,7 @@ import script from './CnAuditTrailCard.vue2.js';
|
|
|
2
2
|
import { render } from './CnAuditTrailCard.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-280a5ace";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -109,7 +109,7 @@ var script = {
|
|
|
109
109
|
if (!this.register || !this.schema || !this.objectId) {
|
|
110
110
|
return ''
|
|
111
111
|
}
|
|
112
|
-
return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-
|
|
112
|
+
return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`
|
|
113
113
|
},
|
|
114
114
|
},
|
|
115
115
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnAuditTrailCard.vue2.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnAuditTrailCard.vue2.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnAuditTrailCard.vue3.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue?vue&type=template&id=
|
|
1
|
+
{"version":3,"file":"CnAuditTrailCard.vue3.js","sources":["../../../../src/components/CnAuditTrailCard/CnAuditTrailCard.vue?vue&type=template&id=280a5ace&lang.js"],"sourcesContent":["<!--\n CnAuditTrailCard — compact audit-trail widget for the integration\n registry.\n\n Surface-aware shell around the `audit-trail` integration: fetches\n the object's recent audit-trail entries (query-time storage strategy\n per AD-22) and renders them in a CnDetailCard.\n\n Per umbrella change `pluggable-integration-registry` AD-19 (surface\n fallback), one component handles all surfaces — `surface` is\n forwarded so the component can branch internally if desired.\n-->\n<template>\n\t<CnDetailCard\n\t\t:title=\"resolvedTitle\"\n\t\t:icon=\"History\"\n\t\t:collapsible=\"collapsible\"\n\t\t:data-surface=\"surface\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\t\t<div v-else-if=\"entries.length === 0\" class=\"cn-audit-card__empty\">\n\t\t\t{{ noEntriesLabel }}\n\t\t</div>\n\t\t<ul v-else class=\"cn-audit-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"entry in displayedEntries\"\n\t\t\t\t:key=\"entry.id\"\n\t\t\t\tclass=\"cn-audit-card__row\">\n\t\t\t\t<div class=\"cn-audit-card__row-head\">\n\t\t\t\t\t<strong class=\"cn-audit-card__action\">{{ entry.action || entry.event || actionLabel }}</strong>\n\t\t\t\t\t<span class=\"cn-audit-card__when\">{{ formatWhen(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-audit-card__row-body\">\n\t\t\t\t\t<span class=\"cn-audit-card__actor\">{{ formatActor(entry) }}</span>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t\t<template v-if=\"entries.length > maxDisplay\" #footer>\n\t\t\t<button class=\"cn-audit-card__show-all\" @click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ entries.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\n\n/**\n * CnAuditTrailCard — compact audit-trail widget rendered by the\n * integration registry on dashboard and detail surfaces.\n *\n * Basic usage\n * ```vue\n * <CnAuditTrailCard\n * :register=\"registerId\"\n * :schema=\"schemaId\"\n * :object-id=\"objectId\"\n * surface=\"detail-page\" />\n * ```\n */\nexport default {\n\tname: 'CnAuditTrailCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon },\n\n\tprops: {\n\t\t/** OpenRegister register id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). Optional for `scope: \"app\"`. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id. Required for `scope: \"object\"` (the default); unused for `scope: \"app\"`. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/**\n\t\t * What the card lists. `object` (the default) is one object's trail\n\t\t * and needs `register`, `schema` and `objectId`. `app` is the\n\t\t * app-wide feed: the entries of every object the caller may read\n\t\t * (OpenRegister's `/audit-trails/readable`), narrowed to `register`\n\t\t * and `schema` when those are set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'object',\n\t\t\tvalidator: (value) => ['object', 'app'].includes(value),\n\t\t},\n\n\t\t/** Rendering surface — passed for AD-19 surface fallback consumers. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'].includes(value),\n\t\t},\n\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Maximum rows to render. */\n\t\tmaxDisplay: { type: Number, default: 5 },\n\t\t/** Whether the card collapses. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Override the card title (defaults to the translated label). */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Pre-translated empty label. */\n\t\tnoEntriesLabel: { type: String, default: () => t('nextcloud-vue', 'No audit entries yet') },\n\t\t/** Pre-translated overflow label. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Pre-translated fallback action label. */\n\t\tactionLabel: { type: String, default: () => t('nextcloud-vue', 'Change') },\n\t},\n\n\temits: ['show-all'],\n\n\tdata() {\n\t\treturn {\n\t\t\tHistory: markRaw(History),\n\t\t\tentries: [],\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tresolvedTitle() {\n\t\t\treturn this.title || t('nextcloud-vue', 'Audit trail')\n\t\t},\n\n\t\tdisplayedEntries() {\n\t\t\treturn this.entries.slice(0, this.maxDisplay)\n\t\t},\n\n\t\t/**\n\t\t * The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the\n\t\t * object's own trail, or the app-wide readable feed narrowed to the\n\t\t * register and schema when set.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {string}\n\t\t */\n\t\tfetchUrl() {\n\t\t\tconst params = new URLSearchParams({ limit: String(this.maxDisplay) })\n\t\t\tif (this.scope === 'app') {\n\t\t\t\tif (this.register) {\n\t\t\t\t\tparams.set('register', this.register)\n\t\t\t\t}\n\t\t\t\tif (this.schema) {\n\t\t\t\t\tparams.set('schema', this.schema)\n\t\t\t\t}\n\t\t\t\treturn `${this.apiBase}/audit-trails/readable?${params.toString()}`\n\t\t\t}\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails?${params.toString()}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tfetchUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (url) {\n\t\t\t\t\tthis.fetchEntries()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the entries from `fetchUrl`. The readable feed answers\n\t\t * `{ rows, nextCursor }`, the object trail `{ results }`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchEntries() {\n\t\t\tconst url = this.fetchUrl\n\t\t\tif (!url) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tconst list = data.results || data.rows || data\n\t\t\t\t\tthis.entries = Array.isArray(list) ? list : []\n\t\t\t\t} else {\n\t\t\t\t\tthis.entries = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnAuditTrailCard] failed to fetch audit trail', err)\n\t\t\t\tthis.entries = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tformatActor(entry) {\n\t\t\treturn entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''\n\t\t},\n\n\t\tformatWhen(entry) {\n\t\t\tconst raw = entry.creationDateTime || entry.created || entry.timestamp\n\t\t\tif (raw === undefined || raw === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst d = new Date(raw)\n\t\t\tif (Number.isNaN(d.getTime()) === true) {\n\t\t\t\treturn String(raw)\n\t\t\t}\n\t\t\treturn d.toLocaleString()\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-audit-card__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-audit-card__row {\n\tpadding: 6px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-audit-card__row:last-child {\n\tborder-bottom: none;\n}\n\n.cn-audit-card__row-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-audit-card__action {\n\tcolor: var(--color-main-text);\n}\n\n.cn-audit-card__when {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-audit-card__row-body {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-audit-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n\tpadding: 12px 0;\n}\n\n.cn-audit-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tpadding: 4px 0;\n\tfont: inherit;\n}\n\n.cn-audit-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;;;AA+BwC,EAAA,KAAA,EAAA,sBAAA;;;;AAGjC,EAAA,KAAA,EAAA,qBAAA;;AAKE,MAAA,UAAA,GAAA,EAAA,KAAE,EAAA,yBAAA,GAAA;AACC,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,uBAAA,GAAA;AACD,MAAA,UAAA,GAAA,EAAA,KAAI,EAAA,qBAAA,GAAA;;;;;;;;uBAhBXA;SACI,EAAE,QAAa,CAAA,aAAA;AACnB,IAAA,IAAG,EAAE,KAAO,CAAA,OAAA;AACX,IAAA,WAAS,EAAE,MAAW,CAAA,WAAA;kBACZ,EAAE,MAAO,CAAA,OAAA;;2BACf;OAAA,KAAA,CAAA,OAAA;mCAAA,CAAA,wBAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;AACS,UAAA,CAAA,KAAA,CAAA,OAAO,CAAC,MAAM,KAAA,CAAA;0BAA5BC,kBAEI,CAAA,KAAA,EAFJ,UAEI,EAAAC,eAAA,CADF,MAAc,CAAA,cAAA,CAAA,EAAA,CAAA,YAAA;;gCAGjBD,8CACiB,QAAgB,CAAA,gBAAA,EAAA,MAApB,KAAA;qCADbA;kBAEE,GAAG,EAAC,KAAM,CAAC,EAAE;AACd,kBAAA,KAAK,EAAC,oBAAoB;;AAC1B,kBAAAE,kBAAA,QAAA;oBACCA,kBAAO,CAAA,QAAA,EAAP,UAAO,EAAAD,eAAA,CAAA,KAAA,CAAA,MAAA,IAAA,KAAA,CAAA,KAAA,IAAA,MAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;AACP,oBAAAC,kBAAA,CAAU,MAAV,EAAA,UAAU,EAAAD,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnBodySections.vue2.js';
|
|
|
2
2
|
import { render } from './CnBodySections.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-44324dca";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -15,6 +15,8 @@ var script = {
|
|
|
15
15
|
cnRegistry: { default: () => ({}) },
|
|
16
16
|
/** Legacy custom-component map provided by CnAppRoot. */
|
|
17
17
|
cnCustomComponents: { default: () => ({}) },
|
|
18
|
+
/** The host's translate function for manifest-authored strings, provided by CnAppRoot. */
|
|
19
|
+
cnTranslate: { default: null },
|
|
18
20
|
},
|
|
19
21
|
|
|
20
22
|
props: {
|
|
@@ -134,7 +136,7 @@ var script = {
|
|
|
134
136
|
return {
|
|
135
137
|
id,
|
|
136
138
|
name,
|
|
137
|
-
title: (section && section.title) || '',
|
|
139
|
+
title: this.translateTitle((section && section.title) || ''),
|
|
138
140
|
card: !!(section && section.card),
|
|
139
141
|
colSpan: section && section.colSpan,
|
|
140
142
|
placement: section && section.placement,
|
|
@@ -146,6 +148,25 @@ var script = {
|
|
|
146
148
|
},
|
|
147
149
|
|
|
148
150
|
methods: {
|
|
151
|
+
/**
|
|
152
|
+
* A manifest-authored section title in the reader's language.
|
|
153
|
+
*
|
|
154
|
+
* The title is written in the manifest, so it belongs to the host
|
|
155
|
+
* app's catalogue, the same as every other manifest title the detail
|
|
156
|
+
* page shows. A section heading printed raw read "Asked about" to a
|
|
157
|
+
* Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).
|
|
158
|
+
*
|
|
159
|
+
* @param {string} title The title as written in the manifest.
|
|
160
|
+
* @return {string} The translated title, or '' when there is none.
|
|
161
|
+
* @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
|
|
162
|
+
*/
|
|
163
|
+
translateTitle(title) {
|
|
164
|
+
if (title === '' || typeof this.cnTranslate !== 'function') {
|
|
165
|
+
return title
|
|
166
|
+
}
|
|
167
|
+
return this.cnTranslate(title)
|
|
168
|
+
},
|
|
169
|
+
|
|
149
170
|
/**
|
|
150
171
|
* Resolve a section's host-app component by registry name. Precedence
|
|
151
172
|
* matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnBodySections.vue2.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: (section && section.title) || '',\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnBodySections.vue2.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/** The host's translate function for manifest-authored strings, provided by CnAppRoot. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: this.translateTitle((section && section.title) || ''),\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest-authored section title in the reader's language.\n\t\t *\n\t\t * The title is written in the manifest, so it belongs to the host\n\t\t * app's catalogue, the same as every other manifest title the detail\n\t\t * page shows. A section heading printed raw read \"Asked about\" to a\n\t\t * Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).\n\t\t *\n\t\t * @param {string} title The title as written in the manifest.\n\t\t * @return {string} The translated title, or '' when there is none.\n\t\t * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language\n\t\t */\n\t\ttranslateTitle(title) {\n\t\t\tif (title === '' || typeof this.cnTranslate !== 'function') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn this.cnTranslate(title)\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnBodySections.vue3.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue?vue&type=template&id=19a0caa4&lang.js"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: (section && section.title) || '',\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_normalizeStyle"],"mappings":";;;;;;;;;AAkDQ,EAAA,OAAA,CAAA,QAAA,CAAA,gBAAK,CAAA,MAAA,GAAA,CAAA;;;QACR,KAAE,EAAAA,cAAA,CAAA,CAAC,kBAAiB,EAAA,EAAA,wBAAA,EACD,QAAA,CAAA,OAAA,EAAA,CAAA,CAAA;qBACX,EAAC,kBAAkB;;0BAC7BC,8CACgB,QAAgB,CAAA,gBAAA,EAAA,CAAzB,KAAK,KAAA;+BADZA;YAEC,GAAE,EAAA,KAAA,CAAA,EAAA;iBACE,EAACD,cAAA,CAAA,CAAA,2BAAA,EACyB,EAAA,iCAAA,EAAA,KAAA,CAAA,IAAA,EAAA,CAAA,CAAA;YAC9B,KAAK,EAAAE,cAAA,CAAE,QAAY,CAAA,YAAA,CAAC,KAAK,CAAA,CAAA;YACzB,iBAAe,EAAE,KAAK,CAAC,EAAC;AACzB,YAAA,aAAK,EAAA,iBAAA;;AAEF,YAAA,CAAA,KAAA,CAAA,KAAA;8BADHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnBodySections.vue3.js","sources":["../../../../src/components/CnBodySections/CnBodySections.vue?vue&type=template&id=44324dca&lang.js"],"sourcesContent":["<!--\n CnBodySections — declarative in-body sections primitive.\n\n Renders an array of `sections` (manifest `config.bodyWidgets` /\n `config.sections`) as titled cards IN THE PAGE BODY, each hosting a\n registered host-app component. Unlike the integration registry (which\n forces a sidebar tab + widget parity pair), a body section resolves its\n component straight from the v2 component registry (`cnRegistry`) or the\n legacy `cnCustomComponents` map — the SAME resolver CnPageRenderer uses\n for `type:\"custom\"` page components. No sidebar tab is required.\n\n Each section's `props` are token-resolved via resolveFilterValue\n (`@objectId`, `@object.<field>`, `@workspace.<key>`, the time tokens,\n `@me`, …). Optional `?`-suffixed tokens are dropped when unset. The\n loaded object + objectId are also `provide`d on `cnSectionContext` so a\n host component written to inject context works without explicit props.\n\n A section whose component cannot be resolved, or whose render throws,\n degrades to an inline error card — it never breaks the page.\n\n Reusable across detail / dashboard / index pages: pass the page's\n `context` ({ objectId, object, workspace, register, schema }) and the\n list of sections.\n-->\n<template>\n\t<div\n\t\tv-if=\"resolvedSections.length > 0\"\n\t\tclass=\"cn-body-sections\"\n\t\t:class=\"{ 'cn-body-sections--grid': useGrid }\"\n\t\tdata-testid=\"cn-body-sections\">\n\t\t<section\n\t\t\tv-for=\"entry in resolvedSections\"\n\t\t\t:key=\"entry.id\"\n\t\t\tclass=\"cn-body-sections__section\"\n\t\t\t:class=\"{ 'cn-body-sections__section--card': entry.card }\"\n\t\t\t:style=\"sectionStyle(entry)\"\n\t\t\t:data-section-id=\"entry.id\"\n\t\t\tdata-testid=\"cn-body-section\">\n\t\t\t<h3\n\t\t\t\tv-if=\"entry.title\"\n\t\t\t\tclass=\"cn-body-sections__title\"\n\t\t\t\t:data-testid=\"`cn-body-section-title-${entry.id}`\">\n\t\t\t\t{{ entry.title }}\n\t\t\t</h3>\n\t\t\t<!-- Resolved + rendered host component, guarded by an error\n\t\t\t boundary so a throwing section degrades inline. -->\n\t\t\t<CnSectionBoundary\n\t\t\t\tv-if=\"entry.component\"\n\t\t\t\t:sectionId=\"entry.id\"\n\t\t\t\t:errorLabel=\"errorLabel\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"entry.component\"\n\t\t\t\t\tv-bind=\"entry.props\"\n\t\t\t\t\t:data-testid=\"`cn-body-section-component-${entry.id}`\" />\n\t\t\t</CnSectionBoundary>\n\t\t\t<!-- Unresolved component: inline error, page survives. -->\n\t\t\t<div\n\t\t\t\tv-else\n\t\t\t\tclass=\"cn-body-sections__error\"\n\t\t\t\t:data-testid=\"`cn-body-section-error-${entry.id}`\">\n\t\t\t\t{{ unresolvedLabel(entry) }}\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { provide, ref, watch } from 'vue'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport CnSectionBoundary from './CnSectionBoundary.js'\n\nexport default {\n\tname: 'CnBodySections',\n\n\tcomponents: {\n\t\tCnSectionBoundary,\n\t},\n\n\tinject: {\n\t\t/** V2 component registry (kind-tagged) provided by CnAppRoot. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Legacy custom-component map provided by CnAppRoot. */\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/** The host's translate function for manifest-authored strings, provided by CnAppRoot. */\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Declarative in-body section descriptors. Each entry:\n\t\t * `{ id, component, title?, props?, placement?, colSpan?, card? }`.\n\t\t * `card: true` draws the section in the same card as the grid's\n\t\t * widgets, for a component that has no card of its own.\n\t\t * `component` is the registry name of a host-app component\n\t\t * (resolved from `cnRegistry` then `cnCustomComponents`).\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number, card?: boolean}>}\n\t\t */\n\t\tsections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page/object context used to resolve `@objectId` / `@object.<field>`\n\t\t * / `@workspace.<key>` / `@config.<key>` tokens in each section's `props`,\n\t\t * and `provide`d on `cnSectionContext` for host components that inject\n\t\t * instead of taking props. Shape:\n\t\t * `{ objectId?, object?, workspace?, config?, register?, schema? }`.\n\t\t *\n\t\t * @type {{objectId?: (string|number), object?: object, workspace?: object, config?: object, register?: string, schema?: string}}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional placement filter — when set, only sections whose\n\t\t * `placement` matches this value render. CnDetailPage mounts one\n\t\t * CnBodySections per placement slot so sections land in the right\n\t\t * spot in the body. When unset, ALL sections render (no filter).\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tplacement: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether to lay sections out on a 12-column responsive grid (honouring\n\t\t * each section's `colSpan`). Defaults to true. Set false to stack\n\t\t * full-width.\n\t\t */\n\t\tgrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Inline label shown when a section's component throws while rendering. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This section failed to load.'),\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\t// Reactive holder published so a host section component can\n\t\t// `inject('cnSectionContext')` and read the loaded object / objectId\n\t\t// without the manifest having to spell out @object props. Mirrors the\n\t\t// dashboard's cnWorkspaceContext / detail page's cnObjectContext pattern.\n\t\tconst cnSectionContext = ref({ ...(props.context || {}) })\n\t\tprovide('cnSectionContext', cnSectionContext)\n\t\twatch(\n\t\t\t() => props.context,\n\t\t\t(next) => {\n\t\t\t\tcnSectionContext.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\t\treturn {}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid layout is active — only when `grid` is true AND at\n\t\t * least one rendered section sets a `colSpan`. A plain stack otherwise.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tuseGrid() {\n\t\t\treturn this.grid && this.filteredSections.some((s) => typeof s.colSpan === 'number')\n\t\t},\n\n\t\t/**\n\t\t * Sections filtered by the `placement` prop (when set). Order is\n\t\t * preserved from the input array.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tfilteredSections() {\n\t\t\tconst list = Array.isArray(this.sections) ? this.sections : []\n\t\t\tif (this.placement === null || this.placement === undefined) {\n\t\t\t\treturn list\n\t\t\t}\n\t\t\treturn list.filter((s) => (s && s.placement) === this.placement)\n\t\t},\n\n\t\t/**\n\t\t * Each filtered section augmented with its resolved Vue `component`\n\t\t * (or null) and its token-resolved `props`. Unresolved components keep\n\t\t * `component: null` so the template renders an inline error rather than\n\t\t * crashing.\n\t\t *\n\t\t * @return {Array<{id: string, title: string, component: object|null, props: object, name: string, colSpan: number|undefined, placement: string|undefined}>}\n\t\t */\n\t\tresolvedSections() {\n\t\t\treturn this.filteredSections.map((section, index) => {\n\t\t\t\tconst name = section && section.component\n\t\t\t\tconst id = (section && section.id) || name || `section-${index}`\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tname,\n\t\t\t\t\ttitle: this.translateTitle((section && section.title) || ''),\n\t\t\t\t\tcard: !!(section && section.card),\n\t\t\t\t\tcolSpan: section && section.colSpan,\n\t\t\t\t\tplacement: section && section.placement,\n\t\t\t\t\tcomponent: this.resolveSectionComponent(name),\n\t\t\t\t\tprops: this.resolveSectionProps(section),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest-authored section title in the reader's language.\n\t\t *\n\t\t * The title is written in the manifest, so it belongs to the host\n\t\t * app's catalogue, the same as every other manifest title the detail\n\t\t * page shows. A section heading printed raw read \"Asked about\" to a\n\t\t * Dutch handler on a pipelinq ticket (Zuiddrecht live round 3).\n\t\t *\n\t\t * @param {string} title The title as written in the manifest.\n\t\t * @return {string} The translated title, or '' when there is none.\n\t\t * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language\n\t\t */\n\t\ttranslateTitle(title) {\n\t\t\tif (title === '' || typeof this.cnTranslate !== 'function') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn this.cnTranslate(title)\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's host-app component by registry name. Precedence\n\t\t * matches CnPageRenderer.resolveCustomComponent: the v2 `cnRegistry`\n\t\t * (any kind exposing a `.component`) wins, else the legacy\n\t\t * `cnCustomComponents` map. Returns null when neither has the name —\n\t\t * the section then renders an inline error.\n\t\t *\n\t\t * @param {string} name The registry component name.\n\t\t * @return {object|null} The resolved Vue component, or null.\n\t\t */\n\t\tresolveSectionComponent(name) {\n\t\t\tif (typeof name !== 'string' || name === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst reg = (this.cnRegistry && this.cnRegistry[name]) || null\n\t\t\tif (reg && reg.component) {\n\t\t\t\treturn reg.component\n\t\t\t}\n\t\t\tconst legacy = this.cnCustomComponents && this.cnCustomComponents[name]\n\t\t\treturn legacy || null\n\t\t},\n\n\t\t/**\n\t\t * Resolve a section's `props` map: every value is passed through\n\t\t * resolveFilterValue with this page's context so `@objectId` /\n\t\t * `@object.<field>` / `@workspace.<key>` / time tokens resolve. A value\n\t\t * that resolves to an unset OPTIONAL token (`@…?` left as-is, or a\n\t\t * required `@object.<field>` whose field is absent) is DROPPED from the\n\t\t * props so the child sees `undefined` rather than a raw `@…` string.\n\t\t *\n\t\t * @param {object} section The section descriptor.\n\t\t * @return {object} The token-resolved props for the host component.\n\t\t */\n\t\tresolveSectionProps(section) {\n\t\t\tconst raw = (section && section.props && typeof section.props === 'object') ? section.props : {}\n\t\t\tconst ctx = this.context || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(raw)) {\n\t\t\t\tconst resolved = resolveFilterValue(value, ctx)\n\t\t\t\t// Drop a value that stayed an unresolved `@`-token (optional\n\t\t\t\t// workspace token, or an @object/@objectId with no context yet)\n\t\t\t\t// so the child receives undefined instead of a literal token.\n\t\t\t\tif (typeof resolved === 'string' && resolved.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Layout style for a section — a grid `grid-column: span N` when the\n\t\t * grid is active and the section declares a `colSpan`; otherwise none.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {object} An inline-style object.\n\t\t */\n\t\tsectionStyle(entry) {\n\t\t\tif (!this.useGrid || typeof entry.colSpan !== 'number') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst span = Math.max(1, Math.min(12, entry.colSpan))\n\t\t\treturn { gridColumn: `span ${span}` }\n\t\t},\n\n\t\t/**\n\t\t * Inline message for a section whose `component` name did not resolve.\n\t\t *\n\t\t * @param {object} entry A resolved section.\n\t\t * @return {string}\n\t\t */\n\t\tunresolvedLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'Section component \"{name}\" is not registered.', { name: entry.name || entry.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-body-sections {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-body-sections--grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(12, 1fr);\n\tgap: 16px;\n}\n\n.cn-body-sections__section {\n\tmin-width: 0;\n}\n\n/* Same card as the grid's widgets, so a section reads as one of them. */\n.cn-body-sections__section--card {\n\tpadding: 12px 16px 16px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-body-sections__section--card > .cn-body-sections__title {\n\tmargin: 0 -16px 12px;\n\tpadding: 0 16px 12px;\n\tfont-size: 14px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-body-sections__title {\n\tmargin: 0 0 8px;\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-body-sections__error {\n\tpadding: 12px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_normalizeStyle"],"mappings":";;;;;;;;;AAkDQ,EAAA,OAAA,CAAA,QAAA,CAAA,gBAAK,CAAA,MAAA,GAAA,CAAA;;;QACR,KAAE,EAAAA,cAAA,CAAA,CAAC,kBAAiB,EAAA,EAAA,wBAAA,EACD,QAAA,CAAA,OAAA,EAAA,CAAA,CAAA;qBACX,EAAC,kBAAkB;;0BAC7BC,8CACgB,QAAgB,CAAA,gBAAA,EAAA,CAAzB,KAAK,KAAA;+BADZA;YAEC,GAAE,EAAA,KAAA,CAAA,EAAA;iBACE,EAACD,cAAA,CAAA,CAAA,2BAAA,EACyB,EAAA,iCAAA,EAAA,KAAA,CAAA,IAAA,EAAA,CAAA,CAAA;YAC9B,KAAK,EAAAE,cAAA,CAAE,QAAY,CAAA,YAAA,CAAC,KAAK,CAAA,CAAA;YACzB,iBAAe,EAAE,KAAK,CAAC,EAAC;AACzB,YAAA,aAAK,EAAA,iBAAA;;AAEF,YAAA,CAAA,KAAA,CAAA,KAAA;8BADHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnCountdownWidget.vue2.js';
|
|
|
2
2
|
import { render } from './CnCountdownWidget.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-5546e71c";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -43,6 +43,18 @@ const VARIANT_COLORS = Object.freeze({
|
|
|
43
43
|
* negative number: "-3 days left" is a puzzle, and an overdue case is the one
|
|
44
44
|
* state a handler must not have to decode.
|
|
45
45
|
*
|
|
46
|
+
* ## A settled record
|
|
47
|
+
*
|
|
48
|
+
* `settledField` names a property on the record that is truthy once the work
|
|
49
|
+
* the date belongs to is done, such as a case in its final status. A settled
|
|
50
|
+
* record reads `settledLabel` (default "Closed") in no threshold colour, never
|
|
51
|
+
* "overdue": a closed case missed nothing that can still be acted on, and a red
|
|
52
|
+
* count of days on it sends a handler after a case that needs nobody.
|
|
53
|
+
*
|
|
54
|
+
* ```js
|
|
55
|
+
* content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
46
58
|
* ## An unset date
|
|
47
59
|
*
|
|
48
60
|
* Renders a dash and no threshold colour. A case with no deadline is not urgent
|
|
@@ -65,8 +77,10 @@ var script = {
|
|
|
65
77
|
* - `icon` — MDI icon name.
|
|
66
78
|
* - `thresholds` — `{ warn, danger }` in days remaining.
|
|
67
79
|
* - `showDate` — render the absolute date underneath (default true).
|
|
80
|
+
* - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.
|
|
81
|
+
* - `settledLabel` — the headline of a settled record, run through the host's translate function (default "Closed").
|
|
68
82
|
*
|
|
69
|
-
* @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}
|
|
83
|
+
* @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}
|
|
70
84
|
*/
|
|
71
85
|
content: {
|
|
72
86
|
type: Object,
|
|
@@ -89,6 +103,7 @@ var script = {
|
|
|
89
103
|
return {
|
|
90
104
|
cnObjectContextInjected: inject('cnObjectContext', null),
|
|
91
105
|
cnDetailObjectContextInjected: inject('cnDetailObjectContext', null),
|
|
106
|
+
cnTranslate: inject('cnTranslate', null),
|
|
92
107
|
}
|
|
93
108
|
},
|
|
94
109
|
|
|
@@ -140,17 +155,44 @@ var script = {
|
|
|
140
155
|
return Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)
|
|
141
156
|
},
|
|
142
157
|
|
|
143
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Whether the record says its work is done (`settledField` is truthy).
|
|
160
|
+
*
|
|
161
|
+
* @return {boolean} True for a settled record.
|
|
162
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
163
|
+
*/
|
|
164
|
+
isSettled() {
|
|
165
|
+
const field = this.content?.settledField;
|
|
166
|
+
if (!this.record || !field) {
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
return Boolean(getByPath(this.record, field))
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether the date has passed on a record still open.
|
|
174
|
+
*
|
|
175
|
+
* @return {boolean} True when overdue.
|
|
176
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
177
|
+
*/
|
|
144
178
|
isOverdue() {
|
|
145
|
-
return this.daysRemaining !== null && this.daysRemaining < 0
|
|
179
|
+
return !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0
|
|
146
180
|
},
|
|
147
181
|
|
|
148
182
|
/**
|
|
149
183
|
* The headline: the remaining time in words.
|
|
150
184
|
*
|
|
151
185
|
* @return {string} The headline text.
|
|
186
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
152
187
|
*/
|
|
153
188
|
headline() {
|
|
189
|
+
if (this.isSettled) {
|
|
190
|
+
const label = this.content?.settledLabel;
|
|
191
|
+
if (label) {
|
|
192
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label
|
|
193
|
+
}
|
|
194
|
+
return translate('nextcloud-vue', 'Closed')
|
|
195
|
+
}
|
|
154
196
|
const days = this.daysRemaining;
|
|
155
197
|
if (days === null) {
|
|
156
198
|
return '—'
|
|
@@ -187,10 +229,11 @@ var script = {
|
|
|
187
229
|
* The threshold band this date falls in.
|
|
188
230
|
*
|
|
189
231
|
* @return {string} One of default / success / warning / error.
|
|
232
|
+
* @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue
|
|
190
233
|
*/
|
|
191
234
|
variant() {
|
|
192
235
|
const days = this.daysRemaining;
|
|
193
|
-
if (days === null) {
|
|
236
|
+
if (days === null || this.isSettled) {
|
|
194
237
|
return 'default'
|
|
195
238
|
}
|
|
196
239
|
if (days < 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t */\n\t\theadline() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnCountdownWidget.vue2.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnWidgetIcon","t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=17598258&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/** @return {boolean} true once the date has passed. */\n\t\tisOverdue() {\n\t\t\treturn this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t */\n\t\theadline() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnCountdownWidget.vue3.js","sources":["../../../../src/components/CnCountdownWidget/CnCountdownWidget.vue?vue&type=template&id=5546e71c&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-kpi-card cn-countdown-widget\" :class=\"`cn-countdown-widget--${variant}`\">\n\t\t<div class=\"cn-kpi-card__icon cn-countdown-widget__icon\" :style=\"iconCircleStyle\">\n\t\t\t<CnWidgetIcon :name=\"resolvedIcon\" :size=\"24\" />\n\t\t</div>\n\n\t\t<div class=\"cn-kpi-card__body\">\n\t\t\t<div v-if=\"resolvedLabel\" class=\"cn-kpi-card__title\">\n\t\t\t\t{{ resolvedLabel }}\n\t\t\t</div>\n\t\t\t<div class=\"cn-kpi-card__value cn-countdown-widget__value\" :style=\"{ color: valueColor }\">\n\t\t\t\t{{ headline }}\n\t\t\t</div>\n\t\t\t<div v-if=\"subLabel\" class=\"cn-countdown-widget__sub\">\n\t\t\t\t{{ subLabel }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { inject } from 'vue'\nimport CnWidgetIcon from '../CnWidgetGrid/CnWidgetIcon.vue'\nimport { getByPath } from '../../composables/useEndpointSource.js'\nimport { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nconst VARIANT_COLORS = Object.freeze({\n\tdefault: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n})\n\n/**\n * CnCountdownWidget — a KPI tile for how long is left until a date on the\n * bound record.\n *\n * Every other tile in the catalog answers \"how many\" by aggregating or calling\n * an endpoint. This one answers \"how long\", and it needs neither: the record is\n * already loaded by the detail page, so the tile reads the date straight off it\n * and does the arithmetic. No request, and nothing to go stale.\n *\n * It headlines the remaining time and carries the date itself underneath,\n * because \"12 days left\" and \"5 October\" answer different questions and a\n * handler asks both.\n *\n * ## Thresholds\n *\n * `thresholds` recolours the tile as the date approaches, in days:\n *\n * ```js\n * content: {\n * label: 'Time left',\n * field: 'deadline',\n * icon: 'ClockAlertOutline',\n * thresholds: { warn: 14, danger: 5 },\n * }\n * ```\n *\n * A date in the past reads \"overdue by N days\" in the error colour, never a\n * negative number: \"-3 days left\" is a puzzle, and an overdue case is the one\n * state a handler must not have to decode.\n *\n * ## A settled record\n *\n * `settledField` names a property on the record that is truthy once the work\n * the date belongs to is done, such as a case in its final status. A settled\n * record reads `settledLabel` (default \"Closed\") in no threshold colour, never\n * \"overdue\": a closed case missed nothing that can still be acted on, and a red\n * count of days on it sends a handler after a case that needs nobody.\n *\n * ```js\n * content: { field: 'deadline', settledField: 'isFinalStatus', settledLabel: 'Handled' }\n * ```\n *\n * ## An unset date\n *\n * Renders a dash and no threshold colour. A case with no deadline is not urgent\n * and is not on time; it simply has no deadline, and colouring it green would\n * claim otherwise.\n */\nexport default {\n\tname: 'CnCountdownWidget',\n\n\tcomponents: {\n\t\tCnWidgetIcon,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The tile's config.\n\t\t *\n\t\t * - `field` — property on the bound record holding the target date.\n\t\t * - `label` — the tile's title.\n\t\t * - `icon` — MDI icon name.\n\t\t * - `thresholds` — `{ warn, danger }` in days remaining.\n\t\t * - `showDate` — render the absolute date underneath (default true).\n\t\t * - `settledField` — property on the record that is truthy once the work is done; the tile then reads `settledLabel` and never counts overdue.\n\t\t * - `settledLabel` — the headline of a settled record, run through the host's translate function (default \"Closed\").\n\t\t *\n\t\t * @type {{ field?: string, label?: string, icon?: string, thresholds?: {warn?: number, danger?: number}, showDate?: boolean, settledField?: string, settledLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The bound record, when the surface passes it explicitly. Falls back to\n\t\t * the injected `cnObjectContext` a detail page provides.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup() {\n\t\treturn {\n\t\t\tcnObjectContextInjected: inject('cnObjectContext', null),\n\t\t\tcnDetailObjectContextInjected: inject('cnDetailObjectContext', null),\n\t\t\tcnTranslate: inject('cnTranslate', null),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The record to read the date from.\n\t\t *\n\t\t * @return {object|null} The bound record, or null.\n\t\t */\n\t\trecord() {\n\t\t\tif (this.objectData) {\n\t\t\t\treturn this.objectData\n\t\t\t}\n\t\t\tconst ctx = resolveObjectTokenContext(this.cnObjectContextInjected, this.cnDetailObjectContextInjected)\n\t\t\treturn ctx?.object || null\n\t\t},\n\n\t\t/**\n\t\t * The target date, or null when unset or unparseable.\n\t\t *\n\t\t * @return {Date|null} The parsed date.\n\t\t */\n\t\ttargetDate() {\n\t\t\tconst field = this.content?.field\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = getByPath(this.record, field)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parsed = new Date(raw)\n\t\t\treturn Number.isNaN(parsed.getTime()) ? null : parsed\n\t\t},\n\n\t\t/**\n\t\t * Whole days from today until the target. Negative once it has passed.\n\t\t *\n\t\t * Both ends are floored to midnight first, so \"tomorrow\" is 1 day away\n\t\t * all day today rather than flipping to 0 at lunchtime.\n\t\t *\n\t\t * @return {number|null} Days remaining, or null.\n\t\t */\n\t\tdaysRemaining() {\n\t\t\tif (!this.targetDate) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst startOfDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n\t\t\treturn Math.round((startOfDay(this.targetDate) - startOfDay(new Date())) / MS_PER_DAY)\n\t\t},\n\n\t\t/**\n\t\t * Whether the record says its work is done (`settledField` is truthy).\n\t\t *\n\t\t * @return {boolean} True for a settled record.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisSettled() {\n\t\t\tconst field = this.content?.settledField\n\t\t\tif (!this.record || !field) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(getByPath(this.record, field))\n\t\t},\n\n\t\t/**\n\t\t * Whether the date has passed on a record still open.\n\t\t *\n\t\t * @return {boolean} True when overdue.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tisOverdue() {\n\t\t\treturn !this.isSettled && this.daysRemaining !== null && this.daysRemaining < 0\n\t\t},\n\n\t\t/**\n\t\t * The headline: the remaining time in words.\n\t\t *\n\t\t * @return {string} The headline text.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\theadline() {\n\t\t\tif (this.isSettled) {\n\t\t\t\tconst label = this.content?.settledLabel\n\t\t\t\tif (label) {\n\t\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(label) : label\n\t\t\t\t}\n\t\t\t\treturn t('nextcloud-vue', 'Closed')\n\t\t\t}\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\tconst overdue = Math.abs(days)\n\t\t\t\treturn overdue === 1\n\t\t\t\t\t? t('nextcloud-vue', '1 day overdue')\n\t\t\t\t\t: t('nextcloud-vue', '{count} days overdue', { count: overdue })\n\t\t\t}\n\t\t\tif (days === 0) {\n\t\t\t\treturn t('nextcloud-vue', 'Due today')\n\t\t\t}\n\t\t\tif (days === 1) {\n\t\t\t\treturn t('nextcloud-vue', '1 day left')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', '{count} days left', { count: days })\n\t\t},\n\n\t\t/**\n\t\t * The absolute date under the headline, so the tile answers \"how long\"\n\t\t * and \"when\" at once.\n\t\t *\n\t\t * @return {string} The formatted date, or ''.\n\t\t */\n\t\tsubLabel() {\n\t\t\tif (!this.targetDate || this.content?.showDate === false) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.targetDate.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' })\n\t\t},\n\n\t\t/**\n\t\t * The threshold band this date falls in.\n\t\t *\n\t\t * @return {string} One of default / success / warning / error.\n\t\t * @spec openspec/changes/countdown-settled-record/specs/countdown-settled-record/spec.md#requirement-a-settled-record-never-reads-as-overdue\n\t\t */\n\t\tvariant() {\n\t\t\tconst days = this.daysRemaining\n\t\t\tif (days === null || this.isSettled) {\n\t\t\t\treturn 'default'\n\t\t\t}\n\t\t\tif (days < 0) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tconst thresholds = this.content?.thresholds || {}\n\t\t\tif (Number.isFinite(thresholds.danger) && days <= thresholds.danger) {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (Number.isFinite(thresholds.warn) && days <= thresholds.warn) {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'default'\n\t\t},\n\n\t\t/** @return {string} The value colour for the current band. */\n\t\tvalueColor() {\n\t\t\treturn VARIANT_COLORS[this.variant] || VARIANT_COLORS.default\n\t\t},\n\n\t\t/** @return {string} The tile label. */\n\t\tresolvedLabel() {\n\t\t\treturn this.content?.label || ''\n\t\t},\n\n\t\t/** @return {string} The MDI icon name. */\n\t\tresolvedIcon() {\n\t\t\treturn this.content?.icon || 'ClockOutline'\n\t\t},\n\n\t\t/** @return {object} Inline style for the icon circle. */\n\t\ticonCircleStyle() {\n\t\t\treturn { color: this.valueColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-countdown-widget__value {\n\twhite-space: nowrap;\n}\n\n.cn-countdown-widget__sub {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_createVNode","_createElementBlock","_toDisplayString","_createElementVNode"],"mappings":";;AAgBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,mBAAI,GAAA;;;AACT,EAAA,KAAA,EAAA,oBAAA;;;;;;;;;;;IANH,KAAM,EAAAA,cAAA,CAAA,CAAC,iCAAoB,EAAA,CAAA,qBAAA,EAAA,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;sBAGxB,CAAA,KAAA,EAAA;AAFA,MAAA,KAAI,EAAC,6CAA0C;AAAA,MAAA,KAAA,iBAAA,QAAA,CAAA,eAAA,CAAA;;MACnDC,WAAkB,CAAA,uBAAA,EAAA;AAAJ,QAAA,IAAI,EAAA,QAAA,CAAA,YAAA;AAAA,QAAA,IAAA,EAAA,EAAA;;;;OAId,QAAA,CAAA,aAAA;wBAALC,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADD,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;MAEjBC,kBAEG,CAAA,KAAA,EAAA;AAFE,QAAA,KAAA,EAAA,+CAAA;AAAA,QAAA,KAAA,0BAAA,QAAA,CAAA,UAAA,EAAA,CAAA;yBACD,QAAA,CAAA,QAAA,CAAA,EAAA,CAAA,mBAAA;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnDashboardGrid.vue2.js';
|
|
|
2
2
|
import { render } from './CnDashboardGrid.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-a91998fc";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|