@conduction/nextcloud-vue 2.132.0 → 2.134.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +14 -0
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +5 -4
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js +9 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js +237 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js +103 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/conditionOperators.js +121 -0
- package/dist/esm/components/CnConditionListEditor/conditionOperators.js.map +1 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +12 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +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 +85 -10
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +60 -36
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue.js +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js +10 -0
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +258 -6
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +283 -131
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -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/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
- 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/dialogs/CnPartyAddDialog.vue.js +9 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue.js.map +1 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue2.js +184 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue2.js.map +1 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue3.js +168 -0
- package/dist/esm/dialogs/CnPartyAddDialog.vue3.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +27 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +27 -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 +353 -217
- package/dist/esm/store/plugins/liveUpdates.js +13 -4
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +28 -5
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +6 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/listLenses.js +20 -1
- package/dist/esm/utils/listLenses.js.map +1 -1
- package/dist/esm/utils/objectParties.js +79 -1
- package/dist/esm/utils/objectParties.js.map +1 -1
- package/dist/esm/utils/objectRows.js +59 -0
- package/dist/esm/utils/objectRows.js.map +1 -0
- 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 +7460 -5803
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +353 -217
- package/l10n/en.json +27 -1
- package/l10n/nl.json +27 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
- package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
- package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
- package/src/components/CnConditionListEditor/index.js +17 -0
- package/src/components/CnDetailPage/CnDetailPage.md +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +105 -12
- package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
- package/src/components/CnIntegrationWidget/CnIntegrationWidget.vue +10 -0
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +401 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
- package/src/components/index.js +1 -0
- package/src/css/look-board-index.css +6 -0
- package/src/css/look-board.css +35 -7
- package/src/dialogs/CnPartyAddDialog.vue +309 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +17 -1
- package/src/store/plugins/liveUpdates.js +13 -4
- package/src/store/useObjectStore.js +28 -5
- package/src/utils/detailActionModel.js +6 -0
- package/src/utils/listLenses.js +20 -1
- package/src/utils/objectParties.js +78 -0
- package/src/utils/objectRows.js +55 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -2,7 +2,7 @@ import script from './CnIntegrationWidget.vue2.js';
|
|
|
2
2
|
import { render } from './CnIntegrationWidget.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-1b6b243b";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -111,6 +111,15 @@ var script = {
|
|
|
111
111
|
*/
|
|
112
112
|
objectLabel: { type: String, default: '' },
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Extra props for the leaf, set by the placement (a manifest widget's
|
|
116
|
+
* `props.placementProps`), for example the kinds a finance leaf shows. The
|
|
117
|
+
* record's own identity always wins over a key of the same name.
|
|
118
|
+
*
|
|
119
|
+
* @type {object}
|
|
120
|
+
*/
|
|
121
|
+
placementProps: { type: Object, default: () => ({}) },
|
|
122
|
+
|
|
114
123
|
/**
|
|
115
124
|
* The surface the leaf itself is told it renders on, when it differs
|
|
116
125
|
* from `surface` (a leaf placed in a dialog that draws its sidebar-tab
|
|
@@ -386,6 +395,7 @@ var script = {
|
|
|
386
395
|
*/
|
|
387
396
|
leafProps(provider) {
|
|
388
397
|
return {
|
|
398
|
+
...this.placementProps,
|
|
389
399
|
integrationId: provider.id,
|
|
390
400
|
register: this.resolvedRegister,
|
|
391
401
|
schema: this.resolvedSchema,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnIntegrationWidget.vue2.js","sources":["../../../../src/components/CnIntegrationWidget/CnIntegrationWidget.vue"],"sourcesContent":["<!--\n CnIntegrationWidget — tabbed, app-faithful integration widget for\n OpenRegister object detail pages and dashboards.\n\n Supersedes the generic Phase E pair (CnIntegrationCard /\n CnIntegrationWidgetGrid) which rendered one one-size surface that\n erased each integrated app's visual identity. This widget gives every\n integration its own tab (app icon + label + accent), composing the\n existing bespoke per-leaf content component (`provider.tab`), PLUS a\n single-integration mode for manifest multi-placement.\n\n TWO MODES\n ---------\n - Tabbed (default): a tab strip with one tab per integration available\n for `surface` (optionally narrowed by `include`), sorted by `.order`.\n The active tab renders that leaf's content. Unavailable integrations\n still get a tab; clicking shows an NcEmptyContent set-up state.\n - Single (`:only=\"deck\"`): NO tab strip — just one integration's\n content under a compact header (app icon + label). This is what an\n app manifest places several times on one detail page (ADR-024).\n\n AVAILABILITY (Phase J-B)\n ------------------------\n Per-integration availability is sourced, in order:\n 1. descriptor `available` (boolean) when the descriptor carries it,\n 2. else the OCS capability\n `openregister.integrations.providers[].available`\n (getCapabilities()), distinguishing missing-app from\n not-configured via a `reason` field when present,\n 3. else `isAppInstalled(requiredApp)` (OC.appswebroots / capabilities).\n When unavailable, the leaf renders NcEmptyContent: app icon, a\n \"{App} not available\" / \"not configured\" name, a short description, and\n an NcButton linking to the leaf's setup docs\n (`https://openregister.conduction.nl/docs/Integrations/{id}/`).\n\n DESCRIPTOR CONTRACT (filled by the per-app waves)\n -------------------------------------------------\n In addition to the registry's existing fields, the widget reads:\n - icon (MDI name string) — the app icon, rendered via CnIcon.\n - accentColor (hex) — per-app brand tint for the active tab / header.\n - appName (string) — backing-app display name for empty copy\n (defaults to `label`).\n - docsUrl (string) — setup-docs link (defaults to the\n openregister.conduction.nl path derived from `id`).\n - surfaces (string[]) — optional surface allowlist (eligible\n everywhere when omitted).\n These are normalised in `src/integrations/registry.js`; the per-app\n fidelity waves refine accentColor/appName per leaf.\n\n MANIFEST PLACEMENT (ADR-024)\n ----------------------------\n An app manifest references this component by name and passes config as\n props, e.g. to place two single-integration widgets on a detail page:\n\n \"widgets\": [\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"deck\", \"surface\": \"detail-page\" } },\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"calendar\", \"surface\": \"detail-page\" } }\n ]\n\n The host wires `register` / `schema` / `objectId` from the object\n context. Omit `only` (and optionally pass `include`) for the tabbed\n surface.\n\n See ADR-019 (registry), ADR-022 (sidebar tab contract), ADR-024\n (manifest widget placement), ADR-004 (component composition).\n\n @example\n Tabbed, all detail-page integrations:\n <CnIntegrationWidget\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n\n @example\n Single-integration, manifest-placeable:\n <CnIntegrationWidget\n only=\"deck\"\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n-->\n<template>\n\t<div class=\"cn-integration-widget\" :data-surface=\"surface\" :data-mode=\"mode\">\n\t\t<!-- SINGLE MODE: one integration, no tab strip. -->\n\t\t<template v-if=\"mode === 'single'\">\n\t\t\t<div v-if=\"singleProvider\" class=\"cn-integration-widget__single\">\n\t\t\t\t<header class=\"cn-integration-widget__header\" :style=\"accentStyle(singleProvider)\">\n\t\t\t\t\t<CnIcon\n\t\t\t\t\t\tv-if=\"singleProvider.icon\"\n\t\t\t\t\t\t:name=\"singleProvider.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-integration-widget__header-icon\" />\n\t\t\t\t\t<span class=\"cn-integration-widget__header-label\">{{ singleProvider.label }}</span>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"singleProvider.label\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</header>\n\t\t\t\t<div class=\"cn-integration-widget__panel\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(singleProvider)\" />\n\t\t\t\t\t<CnLeafMountHost\n\t\t\t\t\t\tv-else-if=\"isMountLeaf(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:mountProps=\"leafMountProps(singleProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(singleProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(singleProvider)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- The leaf is not registered: its app is absent or did not load. -->\n\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\tv-else-if=\"appName\"\n\t\t\t\t:provider=\"absentProvider\"\n\t\t\t\treason=\"missing-app\"\n\t\t\t\tdata-testid=\"cn-integration-widget-absent\" />\n\t\t\t<NcEmptyContent v-else :name=\"unknownLeafLabel\" />\n\t\t</template>\n\n\t\t<!-- TABBED MODE: tab strip + active panel. -->\n\t\t<template v-else>\n\t\t\t<NcEmptyContent\n\t\t\t\tv-if=\"visibleProviders.length === 0\"\n\t\t\t\t:name=\"noIntegrationsLabel\" />\n\t\t\t<template v-else>\n\t\t\t\t<div class=\"cn-integration-widget__tabbar\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"cn-integration-widget__tabs\"\n\t\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t\t:aria-label=\"tabListLabel\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tv-for=\"(provider, idx) in visibleProviders\"\n\t\t\t\t\t\t\t:id=\"tabId(provider)\"\n\t\t\t\t\t\t\t:key=\"provider.id\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-integration-widget__tab--active': provider.id === activeId }\"\n\t\t\t\t\t\t\t:style=\"tabAccentStyle(provider)\"\n\t\t\t\t\t\t\t:aria-selected=\"provider.id === activeId ? 'true' : 'false'\"\n\t\t\t\t\t\t\t:tabindex=\"provider.id === activeId ? 0 : -1\"\n\t\t\t\t\t\t\t:aria-controls=\"panelId(provider)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-integration-widget-tab-${provider.id}`\"\n\t\t\t\t\t\t\t@click=\"selectTab(provider.id)\"\n\t\t\t\t\t\t\t@keydown=\"onTabKeydown($event, idx)\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"provider.icon\"\n\t\t\t\t\t\t\t\t:name=\"provider.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab-icon\" />\n\t\t\t\t\t\t\t<span class=\"cn-integration-widget__tab-label\">{{ provider.label }}</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"headerTitle\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"activeProvider\"\n\t\t\t\t\t:id=\"panelId(activeProvider)\"\n\t\t\t\t\tclass=\"cn-integration-widget__panel\"\n\t\t\t\t\trole=\"tabpanel\"\n\t\t\t\t\t:aria-labelledby=\"tabId(activeProvider)\"\n\t\t\t\t\ttabindex=\"0\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(activeProvider)\"\n\t\t\t\t\t\t:provider=\"activeProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(activeProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(activeProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(activeProvider)\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnLeafMountHost from '../CnLeafMountHost/CnLeafMountHost.vue'\nimport CnIntegrationWidgetEmpty from './CnIntegrationWidgetEmpty.vue'\nimport { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'\nimport { registerIntegrationIcons } from '../../integrations/icons.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\nimport { resolveProviderAvailability } from './availability.js'\n\n// Ensure every descriptor's MDI icon resolves through CnIcon regardless\n// of what the host app registered at bootstrap — otherwise tabs fall\n// back to the generic HelpCircleOutline glyph. Idempotent.\nregisterIntegrationIcons()\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnIntegrationWidget — tabbed integration widget + single-integration\n * mode. See the file-level docblock for the contract.\n */\nexport default {\n\tname: 'CnIntegrationWidget',\n\n\tcomponents: { NcEmptyContent, CnIcon, CnIntegrationWidgetEmpty, CnActionsMenu, CnLeafMountHost },\n\n\tprops: {\n\t\t/**\n\t\t * Optional registry override (used in tests so registrations\n\t\t * don't leak across cases). Defaults to the singleton.\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register id — a slug/uuid string, or the register\n\t\t * OBJECT a detail page injects. Accepts both for the same reason\n\t\t * `schema` does; see below.\n\t\t */\n\t\tregister: { type: [String, Object], default: '' },\n\t\t/**\n\t\t * OpenRegister schema — a slug/uuid string, or the schema OBJECT the\n\t\t * detail-page widget grid injects.\n\t\t *\n\t\t * This used to be `type: String`, and a detail page handed it the\n\t\t * resolved object instead. Vue reported the mismatch as a dev-only\n\t\t * warning (\"Expected String with value \\\"[object Object]\\\", got\n\t\t * Object\") and then passed the object through unchanged, so the leaf\n\t\t * built `/apps/openregister/api/schemas/[object Object]` and got a 404\n\t\t * — an unreadable URL rather than a named failure, and silent in\n\t\t * production where the warning is compiled out.\n\t\t *\n\t\t * `CnAuditTrailWidget` already accepted both shapes and collapsed the\n\t\t * object to its slug; this mirrors that so the two widgets cannot\n\t\t * disagree about what a detail page may hand them.\n\t\t */\n\t\tschema: { type: [String, Object], default: '' },\n\t\t/** Parent object id. Forwarded to each leaf. */\n\t\tobjectId: { type: [String, Number], default: '' },\n\t\t/**\n\t\t * Single-integration mode: render ONLY this leaf id with no tab\n\t\t * strip. The manifest multi-placement entry point (ADR-024).\n\t\t */\n\t\tonly: { type: String, default: '' },\n\t\t/**\n\t\t * Rendering surface — filters integrations by `surfaces.includes()`\n\t\t * when a descriptor declares a `surfaces` allowlist.\n\t\t */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/**\n\t\t * Explicit allowlist of leaf ids to show as tabs. When omitted,\n\t\t * all registered integrations eligible for `surface` are shown.\n\t\t *\n\t\t * @type {string[]|null}\n\t\t */\n\t\tinclude: { type: Array, default: null },\n\t\t/** Base API URL forwarded to each leaf content component. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Optional object type forwarded to each leaf content component. */\n\t\tobjectType: { type: String, default: '' },\n\t\t/**\n\t\t * Documentation link target for the header's shared Actions menu.\n\t\t * When non-empty a \"Documentation\" item opens it in a new tab;\n\t\t * empty (the default) hides the item.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: { type: String, default: '' },\n\t\t/**\n\t\t * Stable id carried on the Actions-menu refresh / request-feature\n\t\t * payloads and surface string. When empty the header title is\n\t\t * slugified as a fallback (see `resolvedWidgetId`).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: { type: String, default: '' },\n\n\t\t/**\n\t\t * The record's display name, handed to the leaf as `objectLabel` (a\n\t\t * leaf titles its panel or its dialog with it).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tobjectLabel: { type: String, default: '' },\n\n\t\t/**\n\t\t * The surface the leaf itself is told it renders on, when it differs\n\t\t * from `surface` (a leaf placed in a dialog that draws its sidebar-tab\n\t\t * look: `sidebar-tab`). Empty passes `surface`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tleafSurface: { type: String, default: '' },\n\n\t\t/**\n\t\t * The app that provides the `only` leaf, named in the empty state when\n\t\t * the leaf is not registered (\"Decidiq not available\"). Without it an\n\t\t * unregistered leaf shows the plain \"not available\" line.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: { type: String, default: '' },\n\n\t\t/** The icon of that empty state (an MDI name). */\n\t\tappIcon: { type: String, default: '' },\n\t},\n\n\tsetup(props) {\n\t\tconst { integrations, resolveTab } = useIntegrationRegistry(props.registry || undefined)\n\t\t// `resolveTab` swaps a lib-owned entry for THIS bundle's component\n\t\t// (openregister#1958); the entry's own `tab` may belong to another\n\t\t// bundle's Vue and then renders its nested components as literal\n\t\t// unknown elements. See `leafComponent`.\n\t\treturn { registryIntegrations: integrations, resolveRegistryTab: resolveTab }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Currently selected tab id (tabbed mode only). */\n\t\t\tactiveId: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** 'single' when `only` is set, else 'tabbed'. */\n\t\tmode() {\n\t\t\treturn this.only ? 'single' : 'tabbed'\n\t\t},\n\n\t\t/**\n\t\t * The register SLUG. A register object collapses to its slug/name/id,\n\t\t * mirroring `CnAuditTrailWidget.resolvedRegister`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedRegister() {\n\t\t\tconst r = this.register || ''\n\t\t\treturn typeof r === 'string' ? r : (r && (r.slug || r.name || r.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The schema SLUG. A schema object collapses to its slug/name/id.\n\t\t *\n\t\t * Leaves interpolate this straight into an OpenRegister URL, so an\n\t\t * object reaching them produces `/api/schemas/[object Object]` and a\n\t\t * 404 that names nothing. Collapsing here means every leaf receives\n\t\t * the string it already documents, and a caller that hands over the\n\t\t * whole schema object — which the detail-page widget grid does — is\n\t\t * served rather than silently broken.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedSchema() {\n\t\t\tconst s = this.schema || ''\n\t\t\treturn typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The provider for single mode (resolved from `only`), or null\n\t\t * when the id is unknown / not eligible for the surface.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tsingleProvider() {\n\t\t\tif (!this.only) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst all = this.registryIntegrations || []\n\t\t\tfor (const provider of all) {\n\t\t\t\tif (provider.id === this.only && this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Integrations shown as tabs: eligible for the surface, narrowed\n\t\t * by `include` when set, sorted by the registry's `order` (the\n\t\t * snapshot is already order-sorted, so we preserve that).\n\t\t *\n\t\t * @return {object[]}\n\t\t */\n\t\tvisibleProviders() {\n\t\t\tconst include = Array.isArray(this.include) ? this.include : null\n\t\t\treturn (this.registryIntegrations || []).filter((provider) => {\n\t\t\t\tif (!this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (include !== null && !include.includes(provider.id)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The currently active provider in tabbed mode.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveProvider() {\n\t\t\tconst list = this.visibleProviders\n\t\t\tfor (const provider of list) {\n\t\t\t\tif (provider.id === this.activeId) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn list.length > 0 ? list[0] : null\n\t\t},\n\n\t\tnoIntegrationsLabel() {\n\t\t\treturn t('nextcloud-vue', 'No integrations available yet.')\n\t\t},\n\n\t\t/**\n\t\t * The stand-in descriptor the empty state draws for an `only` leaf\n\t\t * that is not registered.\n\t\t *\n\t\t * @return {{id: string, appName: string, icon: string}}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tabsentProvider() {\n\t\t\treturn { id: this.only, appName: this.appName, icon: this.appIcon }\n\t\t},\n\n\t\tunknownLeafLabel() {\n\t\t\treturn t('nextcloud-vue', 'This integration is not available.')\n\t\t},\n\n\t\ttabListLabel() {\n\t\t\treturn t('nextcloud-vue', 'Integrations')\n\t\t},\n\n\t\t/**\n\t\t * Label shown by the active header — the single-mode provider's\n\t\t * label, else the active tab's label, else the generic tab-list\n\t\t * label. Fed to the shared Actions menu as its `title`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\theaderTitle() {\n\t\t\tif (this.mode === 'single') {\n\t\t\t\treturn this.singleProvider ? this.singleProvider.label : this.unknownLeafLabel\n\t\t\t}\n\t\t\treturn this.activeProvider ? this.activeProvider.label : this.tabListLabel\n\t\t},\n\n\t\t/**\n\t\t * Stable widget id for the Actions menu: the explicit `widgetId`\n\t\t * prop when set, else a slugified `headerTitle` (lowercase,\n\t\t * non-alphanumerics → '-'), falling back to 'integration'.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\tconst slug = String(this.headerTitle || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t\treturn slug || 'integration'\n\t\t},\n\t},\n\n\twatch: {\n\t\t/**\n\t\t * Keep the active tab valid when the provider set changes (late\n\t\t * registration, surface/include change). Re-anchor to the first\n\t\t * provider if the current active id disappears.\n\t\t */\n\t\tvisibleProviders: {\n\t\t\timmediate: true,\n\t\t\thandler(list) {\n\t\t\t\tif (this.mode !== 'tabbed') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ids = (list || []).map((p) => p.id)\n\t\t\t\tif (ids.length === 0) {\n\t\t\t\t\tthis.activeId = ''\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!ids.includes(this.activeId)) {\n\t\t\t\t\tthis.activeId = ids[0]\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Whether a provider is eligible for the current surface. A\n\t\t * descriptor with a `surfaces` allowlist must include it; without\n\t\t * one, every provider is eligible (matches CnIntegrationWidgetGrid).\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\teligibleForSurface(provider) {\n\t\t\tconst surfaces = provider.surfaces\n\t\t\tif (Array.isArray(surfaces) && surfaces.length > 0) {\n\t\t\t\treturn surfaces.includes(this.surface)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Resolve whether the backing app for a provider is available +\n\t\t * configured. See file docblock for the source order.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\tisAvailable(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).available\n\t\t},\n\n\t\t/**\n\t\t * The unavailable reason ('missing-app' | 'not-configured' |\n\t\t * 'unknown') for empty-state copy.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {string}\n\t\t */\n\t\tunavailableReason(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).reason\n\t\t},\n\n\t\t/**\n\t\t * The content component for a leaf panel. Resolved through the\n\t\t * registry composable so a lib-owned id renders THIS bundle's copy\n\t\t * of its `tab`; the entry's stored object is only used for a\n\t\t * consumer-custom id, which lives in the consumer's own bundle.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object|null} Vue component, or null.\n\t\t */\n\t\tleafComponent(provider) {\n\t\t\tif (!provider || typeof provider.id !== 'string') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst local = typeof this.resolveRegistryTab === 'function'\n\t\t\t\t? this.resolveRegistryTab(provider.id)\n\t\t\t\t: null\n\t\t\treturn local || provider.tab || null\n\t\t},\n\n\t\t/**\n\t\t * Props forwarded to a leaf's content component. Matches the\n\t\t * shared shape consumed by the bespoke `Cn<X>Tab` components.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object}\n\t\t */\n\t\tleafProps(provider) {\n\t\t\treturn {\n\t\t\t\tintegrationId: provider.id,\n\t\t\t\tregister: this.resolvedRegister,\n\t\t\t\tschema: this.resolvedSchema,\n\t\t\t\tobjectId: this.objectId !== '' && this.objectId !== null && this.objectId !== undefined\n\t\t\t\t\t? String(this.objectId)\n\t\t\t\t\t: '',\n\n\t\t\t\tobjectType: this.objectType,\n\t\t\t\tapiBase: this.apiBase,\n\t\t\t\tsurface: this.leafSurface || this.surface,\n\t\t\t\t...(this.objectLabel ? { objectLabel: this.objectLabel } : {}),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a leaf is a mount-mode leaf (ADR-066): it mounts its own\n\t\t * framework into a host element instead of being a component.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tisMountLeaf(provider) {\n\t\t\treturn Boolean(provider)\n\t\t\t\t&& provider.renderMode === 'mount'\n\t\t\t\t&& typeof provider.mount === 'function'\n\t\t\t\t&& typeof provider.unmount === 'function'\n\t\t},\n\n\t\t/**\n\t\t * What a mount-mode leaf is mounted with: the record context, also as\n\t\t * `integrationContext`, and the record's label.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {object} The mount props.\n\t\t */\n\t\tleafMountProps(provider) {\n\t\t\tconst props = this.leafProps(provider)\n\t\t\treturn {\n\t\t\t\t...props,\n\t\t\t\tintegrationContext: { register: props.register, schema: props.schema, objectId: props.objectId },\n\t\t\t}\n\t\t},\n\n\t\tselectTab(id) {\n\t\t\tthis.activeId = id\n\t\t},\n\n\t\t/**\n\t\t * Roving-tabindex keyboard nav per the ARIA tablist pattern:\n\t\t * Left/Right move + activate, Home/End jump to the ends.\n\t\t *\n\t\t * @param {KeyboardEvent} event Key event.\n\t\t * @param {number} index Index of the focused tab.\n\t\t * @return {void}\n\t\t */\n\t\tonTabKeydown(event, index) {\n\t\t\tconst list = this.visibleProviders\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet next = -1\n\t\t\tif (event.key === 'ArrowRight' || event.key === 'ArrowDown') {\n\t\t\t\tnext = (index + 1) % list.length\n\t\t\t} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {\n\t\t\t\tnext = (index - 1 + list.length) % list.length\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = list.length - 1\n\t\t\t}\n\t\t\tif (next === -1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tconst provider = list[next]\n\t\t\tthis.activeId = provider.id\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst el = this.$el && this.$el.querySelector(`#${this.tabId(provider)}`)\n\t\t\t\tif (el && typeof el.focus === 'function') {\n\t\t\t\t\tel.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\ttabId(provider) {\n\t\t\treturn `cn-iw-tab-${provider.id}`\n\t\t},\n\n\t\tpanelId(provider) {\n\t\t\treturn `cn-iw-panel-${provider.id}`\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the active tab — a left border + subtle bg in\n\t\t * the app's brand colour. Inactive tabs get no accent var.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\ttabAccentStyle(provider) {\n\t\t\tif (provider.id !== this.activeId || !provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the single-mode header.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\taccentStyle(provider) {\n\t\t\tif (!provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-integration-widget {\n\twidth: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__tabbar {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tgap: 8px;\n\tborder-bottom: 1px solid var(--color-border);\n\tmargin-bottom: 12px;\n}\n\n.cn-integration-widget__tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 2px;\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.cn-integration-widget__tab {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 8px 12px;\n\tbackground: transparent;\n\tborder: none;\n\tborder-bottom: 2px solid transparent;\n\tborder-radius: var(--border-radius) var(--border-radius) 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.95em;\n\tcursor: pointer;\n}\n\n.cn-integration-widget__tab:hover {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__tab:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-integration-widget__tab--active {\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n\tborder-bottom-color: var(--cn-iw-accent, var(--color-primary-element));\n}\n\n.cn-integration-widget__tab-icon {\n\tcolor: var(--cn-iw-accent, currentColor);\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__tab-label {\n\twhite-space: nowrap;\n}\n\n.cn-integration-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 4px;\n\tmargin-bottom: 8px;\n\tborder-bottom: 2px solid var(--cn-iw-accent, var(--color-border));\n}\n\n.cn-integration-widget__header-icon {\n\tcolor: var(--cn-iw-accent, var(--color-main-text));\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__header-label {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__header-actions {\n\tmargin-inline-start: auto;\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__panel {\n\tflex: 1;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__panel:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n</style>\n"],"names":["CnIcon","CnIntegrationWidgetEmpty","CnActionsMenu","CnLeafMountHost","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnIntegrationWidget.vue2.js","sources":["../../../../src/components/CnIntegrationWidget/CnIntegrationWidget.vue"],"sourcesContent":["<!--\n CnIntegrationWidget — tabbed, app-faithful integration widget for\n OpenRegister object detail pages and dashboards.\n\n Supersedes the generic Phase E pair (CnIntegrationCard /\n CnIntegrationWidgetGrid) which rendered one one-size surface that\n erased each integrated app's visual identity. This widget gives every\n integration its own tab (app icon + label + accent), composing the\n existing bespoke per-leaf content component (`provider.tab`), PLUS a\n single-integration mode for manifest multi-placement.\n\n TWO MODES\n ---------\n - Tabbed (default): a tab strip with one tab per integration available\n for `surface` (optionally narrowed by `include`), sorted by `.order`.\n The active tab renders that leaf's content. Unavailable integrations\n still get a tab; clicking shows an NcEmptyContent set-up state.\n - Single (`:only=\"deck\"`): NO tab strip — just one integration's\n content under a compact header (app icon + label). This is what an\n app manifest places several times on one detail page (ADR-024).\n\n AVAILABILITY (Phase J-B)\n ------------------------\n Per-integration availability is sourced, in order:\n 1. descriptor `available` (boolean) when the descriptor carries it,\n 2. else the OCS capability\n `openregister.integrations.providers[].available`\n (getCapabilities()), distinguishing missing-app from\n not-configured via a `reason` field when present,\n 3. else `isAppInstalled(requiredApp)` (OC.appswebroots / capabilities).\n When unavailable, the leaf renders NcEmptyContent: app icon, a\n \"{App} not available\" / \"not configured\" name, a short description, and\n an NcButton linking to the leaf's setup docs\n (`https://openregister.conduction.nl/docs/Integrations/{id}/`).\n\n DESCRIPTOR CONTRACT (filled by the per-app waves)\n -------------------------------------------------\n In addition to the registry's existing fields, the widget reads:\n - icon (MDI name string) — the app icon, rendered via CnIcon.\n - accentColor (hex) — per-app brand tint for the active tab / header.\n - appName (string) — backing-app display name for empty copy\n (defaults to `label`).\n - docsUrl (string) — setup-docs link (defaults to the\n openregister.conduction.nl path derived from `id`).\n - surfaces (string[]) — optional surface allowlist (eligible\n everywhere when omitted).\n These are normalised in `src/integrations/registry.js`; the per-app\n fidelity waves refine accentColor/appName per leaf.\n\n MANIFEST PLACEMENT (ADR-024)\n ----------------------------\n An app manifest references this component by name and passes config as\n props, e.g. to place two single-integration widgets on a detail page:\n\n \"widgets\": [\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"deck\", \"surface\": \"detail-page\" } },\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"calendar\", \"surface\": \"detail-page\" } }\n ]\n\n The host wires `register` / `schema` / `objectId` from the object\n context. Omit `only` (and optionally pass `include`) for the tabbed\n surface.\n\n See ADR-019 (registry), ADR-022 (sidebar tab contract), ADR-024\n (manifest widget placement), ADR-004 (component composition).\n\n @example\n Tabbed, all detail-page integrations:\n <CnIntegrationWidget\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n\n @example\n Single-integration, manifest-placeable:\n <CnIntegrationWidget\n only=\"deck\"\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n-->\n<template>\n\t<div class=\"cn-integration-widget\" :data-surface=\"surface\" :data-mode=\"mode\">\n\t\t<!-- SINGLE MODE: one integration, no tab strip. -->\n\t\t<template v-if=\"mode === 'single'\">\n\t\t\t<div v-if=\"singleProvider\" class=\"cn-integration-widget__single\">\n\t\t\t\t<header class=\"cn-integration-widget__header\" :style=\"accentStyle(singleProvider)\">\n\t\t\t\t\t<CnIcon\n\t\t\t\t\t\tv-if=\"singleProvider.icon\"\n\t\t\t\t\t\t:name=\"singleProvider.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-integration-widget__header-icon\" />\n\t\t\t\t\t<span class=\"cn-integration-widget__header-label\">{{ singleProvider.label }}</span>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"singleProvider.label\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</header>\n\t\t\t\t<div class=\"cn-integration-widget__panel\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(singleProvider)\" />\n\t\t\t\t\t<CnLeafMountHost\n\t\t\t\t\t\tv-else-if=\"isMountLeaf(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:mountProps=\"leafMountProps(singleProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(singleProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(singleProvider)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- The leaf is not registered: its app is absent or did not load. -->\n\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\tv-else-if=\"appName\"\n\t\t\t\t:provider=\"absentProvider\"\n\t\t\t\treason=\"missing-app\"\n\t\t\t\tdata-testid=\"cn-integration-widget-absent\" />\n\t\t\t<NcEmptyContent v-else :name=\"unknownLeafLabel\" />\n\t\t</template>\n\n\t\t<!-- TABBED MODE: tab strip + active panel. -->\n\t\t<template v-else>\n\t\t\t<NcEmptyContent\n\t\t\t\tv-if=\"visibleProviders.length === 0\"\n\t\t\t\t:name=\"noIntegrationsLabel\" />\n\t\t\t<template v-else>\n\t\t\t\t<div class=\"cn-integration-widget__tabbar\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"cn-integration-widget__tabs\"\n\t\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t\t:aria-label=\"tabListLabel\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tv-for=\"(provider, idx) in visibleProviders\"\n\t\t\t\t\t\t\t:id=\"tabId(provider)\"\n\t\t\t\t\t\t\t:key=\"provider.id\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-integration-widget__tab--active': provider.id === activeId }\"\n\t\t\t\t\t\t\t:style=\"tabAccentStyle(provider)\"\n\t\t\t\t\t\t\t:aria-selected=\"provider.id === activeId ? 'true' : 'false'\"\n\t\t\t\t\t\t\t:tabindex=\"provider.id === activeId ? 0 : -1\"\n\t\t\t\t\t\t\t:aria-controls=\"panelId(provider)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-integration-widget-tab-${provider.id}`\"\n\t\t\t\t\t\t\t@click=\"selectTab(provider.id)\"\n\t\t\t\t\t\t\t@keydown=\"onTabKeydown($event, idx)\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"provider.icon\"\n\t\t\t\t\t\t\t\t:name=\"provider.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab-icon\" />\n\t\t\t\t\t\t\t<span class=\"cn-integration-widget__tab-label\">{{ provider.label }}</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"headerTitle\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"activeProvider\"\n\t\t\t\t\t:id=\"panelId(activeProvider)\"\n\t\t\t\t\tclass=\"cn-integration-widget__panel\"\n\t\t\t\t\trole=\"tabpanel\"\n\t\t\t\t\t:aria-labelledby=\"tabId(activeProvider)\"\n\t\t\t\t\ttabindex=\"0\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(activeProvider)\"\n\t\t\t\t\t\t:provider=\"activeProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(activeProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(activeProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(activeProvider)\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnLeafMountHost from '../CnLeafMountHost/CnLeafMountHost.vue'\nimport CnIntegrationWidgetEmpty from './CnIntegrationWidgetEmpty.vue'\nimport { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'\nimport { registerIntegrationIcons } from '../../integrations/icons.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\nimport { resolveProviderAvailability } from './availability.js'\n\n// Ensure every descriptor's MDI icon resolves through CnIcon regardless\n// of what the host app registered at bootstrap — otherwise tabs fall\n// back to the generic HelpCircleOutline glyph. Idempotent.\nregisterIntegrationIcons()\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnIntegrationWidget — tabbed integration widget + single-integration\n * mode. See the file-level docblock for the contract.\n */\nexport default {\n\tname: 'CnIntegrationWidget',\n\n\tcomponents: { NcEmptyContent, CnIcon, CnIntegrationWidgetEmpty, CnActionsMenu, CnLeafMountHost },\n\n\tprops: {\n\t\t/**\n\t\t * Optional registry override (used in tests so registrations\n\t\t * don't leak across cases). Defaults to the singleton.\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register id — a slug/uuid string, or the register\n\t\t * OBJECT a detail page injects. Accepts both for the same reason\n\t\t * `schema` does; see below.\n\t\t */\n\t\tregister: { type: [String, Object], default: '' },\n\t\t/**\n\t\t * OpenRegister schema — a slug/uuid string, or the schema OBJECT the\n\t\t * detail-page widget grid injects.\n\t\t *\n\t\t * This used to be `type: String`, and a detail page handed it the\n\t\t * resolved object instead. Vue reported the mismatch as a dev-only\n\t\t * warning (\"Expected String with value \\\"[object Object]\\\", got\n\t\t * Object\") and then passed the object through unchanged, so the leaf\n\t\t * built `/apps/openregister/api/schemas/[object Object]` and got a 404\n\t\t * — an unreadable URL rather than a named failure, and silent in\n\t\t * production where the warning is compiled out.\n\t\t *\n\t\t * `CnAuditTrailWidget` already accepted both shapes and collapsed the\n\t\t * object to its slug; this mirrors that so the two widgets cannot\n\t\t * disagree about what a detail page may hand them.\n\t\t */\n\t\tschema: { type: [String, Object], default: '' },\n\t\t/** Parent object id. Forwarded to each leaf. */\n\t\tobjectId: { type: [String, Number], default: '' },\n\t\t/**\n\t\t * Single-integration mode: render ONLY this leaf id with no tab\n\t\t * strip. The manifest multi-placement entry point (ADR-024).\n\t\t */\n\t\tonly: { type: String, default: '' },\n\t\t/**\n\t\t * Rendering surface — filters integrations by `surfaces.includes()`\n\t\t * when a descriptor declares a `surfaces` allowlist.\n\t\t */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/**\n\t\t * Explicit allowlist of leaf ids to show as tabs. When omitted,\n\t\t * all registered integrations eligible for `surface` are shown.\n\t\t *\n\t\t * @type {string[]|null}\n\t\t */\n\t\tinclude: { type: Array, default: null },\n\t\t/** Base API URL forwarded to each leaf content component. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Optional object type forwarded to each leaf content component. */\n\t\tobjectType: { type: String, default: '' },\n\t\t/**\n\t\t * Documentation link target for the header's shared Actions menu.\n\t\t * When non-empty a \"Documentation\" item opens it in a new tab;\n\t\t * empty (the default) hides the item.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: { type: String, default: '' },\n\t\t/**\n\t\t * Stable id carried on the Actions-menu refresh / request-feature\n\t\t * payloads and surface string. When empty the header title is\n\t\t * slugified as a fallback (see `resolvedWidgetId`).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: { type: String, default: '' },\n\n\t\t/**\n\t\t * The record's display name, handed to the leaf as `objectLabel` (a\n\t\t * leaf titles its panel or its dialog with it).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tobjectLabel: { type: String, default: '' },\n\n\t\t/**\n\t\t * Extra props for the leaf, set by the placement (a manifest widget's\n\t\t * `props.placementProps`), for example the kinds a finance leaf shows. The\n\t\t * record's own identity always wins over a key of the same name.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tplacementProps: { type: Object, default: () => ({}) },\n\n\t\t/**\n\t\t * The surface the leaf itself is told it renders on, when it differs\n\t\t * from `surface` (a leaf placed in a dialog that draws its sidebar-tab\n\t\t * look: `sidebar-tab`). Empty passes `surface`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tleafSurface: { type: String, default: '' },\n\n\t\t/**\n\t\t * The app that provides the `only` leaf, named in the empty state when\n\t\t * the leaf is not registered (\"Decidiq not available\"). Without it an\n\t\t * unregistered leaf shows the plain \"not available\" line.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: { type: String, default: '' },\n\n\t\t/** The icon of that empty state (an MDI name). */\n\t\tappIcon: { type: String, default: '' },\n\t},\n\n\tsetup(props) {\n\t\tconst { integrations, resolveTab } = useIntegrationRegistry(props.registry || undefined)\n\t\t// `resolveTab` swaps a lib-owned entry for THIS bundle's component\n\t\t// (openregister#1958); the entry's own `tab` may belong to another\n\t\t// bundle's Vue and then renders its nested components as literal\n\t\t// unknown elements. See `leafComponent`.\n\t\treturn { registryIntegrations: integrations, resolveRegistryTab: resolveTab }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Currently selected tab id (tabbed mode only). */\n\t\t\tactiveId: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** 'single' when `only` is set, else 'tabbed'. */\n\t\tmode() {\n\t\t\treturn this.only ? 'single' : 'tabbed'\n\t\t},\n\n\t\t/**\n\t\t * The register SLUG. A register object collapses to its slug/name/id,\n\t\t * mirroring `CnAuditTrailWidget.resolvedRegister`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedRegister() {\n\t\t\tconst r = this.register || ''\n\t\t\treturn typeof r === 'string' ? r : (r && (r.slug || r.name || r.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The schema SLUG. A schema object collapses to its slug/name/id.\n\t\t *\n\t\t * Leaves interpolate this straight into an OpenRegister URL, so an\n\t\t * object reaching them produces `/api/schemas/[object Object]` and a\n\t\t * 404 that names nothing. Collapsing here means every leaf receives\n\t\t * the string it already documents, and a caller that hands over the\n\t\t * whole schema object — which the detail-page widget grid does — is\n\t\t * served rather than silently broken.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedSchema() {\n\t\t\tconst s = this.schema || ''\n\t\t\treturn typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The provider for single mode (resolved from `only`), or null\n\t\t * when the id is unknown / not eligible for the surface.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tsingleProvider() {\n\t\t\tif (!this.only) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst all = this.registryIntegrations || []\n\t\t\tfor (const provider of all) {\n\t\t\t\tif (provider.id === this.only && this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Integrations shown as tabs: eligible for the surface, narrowed\n\t\t * by `include` when set, sorted by the registry's `order` (the\n\t\t * snapshot is already order-sorted, so we preserve that).\n\t\t *\n\t\t * @return {object[]}\n\t\t */\n\t\tvisibleProviders() {\n\t\t\tconst include = Array.isArray(this.include) ? this.include : null\n\t\t\treturn (this.registryIntegrations || []).filter((provider) => {\n\t\t\t\tif (!this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (include !== null && !include.includes(provider.id)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The currently active provider in tabbed mode.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveProvider() {\n\t\t\tconst list = this.visibleProviders\n\t\t\tfor (const provider of list) {\n\t\t\t\tif (provider.id === this.activeId) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn list.length > 0 ? list[0] : null\n\t\t},\n\n\t\tnoIntegrationsLabel() {\n\t\t\treturn t('nextcloud-vue', 'No integrations available yet.')\n\t\t},\n\n\t\t/**\n\t\t * The stand-in descriptor the empty state draws for an `only` leaf\n\t\t * that is not registered.\n\t\t *\n\t\t * @return {{id: string, appName: string, icon: string}}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tabsentProvider() {\n\t\t\treturn { id: this.only, appName: this.appName, icon: this.appIcon }\n\t\t},\n\n\t\tunknownLeafLabel() {\n\t\t\treturn t('nextcloud-vue', 'This integration is not available.')\n\t\t},\n\n\t\ttabListLabel() {\n\t\t\treturn t('nextcloud-vue', 'Integrations')\n\t\t},\n\n\t\t/**\n\t\t * Label shown by the active header — the single-mode provider's\n\t\t * label, else the active tab's label, else the generic tab-list\n\t\t * label. Fed to the shared Actions menu as its `title`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\theaderTitle() {\n\t\t\tif (this.mode === 'single') {\n\t\t\t\treturn this.singleProvider ? this.singleProvider.label : this.unknownLeafLabel\n\t\t\t}\n\t\t\treturn this.activeProvider ? this.activeProvider.label : this.tabListLabel\n\t\t},\n\n\t\t/**\n\t\t * Stable widget id for the Actions menu: the explicit `widgetId`\n\t\t * prop when set, else a slugified `headerTitle` (lowercase,\n\t\t * non-alphanumerics → '-'), falling back to 'integration'.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\tconst slug = String(this.headerTitle || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t\treturn slug || 'integration'\n\t\t},\n\t},\n\n\twatch: {\n\t\t/**\n\t\t * Keep the active tab valid when the provider set changes (late\n\t\t * registration, surface/include change). Re-anchor to the first\n\t\t * provider if the current active id disappears.\n\t\t */\n\t\tvisibleProviders: {\n\t\t\timmediate: true,\n\t\t\thandler(list) {\n\t\t\t\tif (this.mode !== 'tabbed') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ids = (list || []).map((p) => p.id)\n\t\t\t\tif (ids.length === 0) {\n\t\t\t\t\tthis.activeId = ''\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!ids.includes(this.activeId)) {\n\t\t\t\t\tthis.activeId = ids[0]\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Whether a provider is eligible for the current surface. A\n\t\t * descriptor with a `surfaces` allowlist must include it; without\n\t\t * one, every provider is eligible (matches CnIntegrationWidgetGrid).\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\teligibleForSurface(provider) {\n\t\t\tconst surfaces = provider.surfaces\n\t\t\tif (Array.isArray(surfaces) && surfaces.length > 0) {\n\t\t\t\treturn surfaces.includes(this.surface)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Resolve whether the backing app for a provider is available +\n\t\t * configured. See file docblock for the source order.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\tisAvailable(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).available\n\t\t},\n\n\t\t/**\n\t\t * The unavailable reason ('missing-app' | 'not-configured' |\n\t\t * 'unknown') for empty-state copy.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {string}\n\t\t */\n\t\tunavailableReason(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).reason\n\t\t},\n\n\t\t/**\n\t\t * The content component for a leaf panel. Resolved through the\n\t\t * registry composable so a lib-owned id renders THIS bundle's copy\n\t\t * of its `tab`; the entry's stored object is only used for a\n\t\t * consumer-custom id, which lives in the consumer's own bundle.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object|null} Vue component, or null.\n\t\t */\n\t\tleafComponent(provider) {\n\t\t\tif (!provider || typeof provider.id !== 'string') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst local = typeof this.resolveRegistryTab === 'function'\n\t\t\t\t? this.resolveRegistryTab(provider.id)\n\t\t\t\t: null\n\t\t\treturn local || provider.tab || null\n\t\t},\n\n\t\t/**\n\t\t * Props forwarded to a leaf's content component. Matches the\n\t\t * shared shape consumed by the bespoke `Cn<X>Tab` components.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object}\n\t\t */\n\t\tleafProps(provider) {\n\t\t\treturn {\n\t\t\t\t...this.placementProps,\n\t\t\t\tintegrationId: provider.id,\n\t\t\t\tregister: this.resolvedRegister,\n\t\t\t\tschema: this.resolvedSchema,\n\t\t\t\tobjectId: this.objectId !== '' && this.objectId !== null && this.objectId !== undefined\n\t\t\t\t\t? String(this.objectId)\n\t\t\t\t\t: '',\n\n\t\t\t\tobjectType: this.objectType,\n\t\t\t\tapiBase: this.apiBase,\n\t\t\t\tsurface: this.leafSurface || this.surface,\n\t\t\t\t...(this.objectLabel ? { objectLabel: this.objectLabel } : {}),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a leaf is a mount-mode leaf (ADR-066): it mounts its own\n\t\t * framework into a host element instead of being a component.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tisMountLeaf(provider) {\n\t\t\treturn Boolean(provider)\n\t\t\t\t&& provider.renderMode === 'mount'\n\t\t\t\t&& typeof provider.mount === 'function'\n\t\t\t\t&& typeof provider.unmount === 'function'\n\t\t},\n\n\t\t/**\n\t\t * What a mount-mode leaf is mounted with: the record context, also as\n\t\t * `integrationContext`, and the record's label.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {object} The mount props.\n\t\t */\n\t\tleafMountProps(provider) {\n\t\t\tconst props = this.leafProps(provider)\n\t\t\treturn {\n\t\t\t\t...props,\n\t\t\t\tintegrationContext: { register: props.register, schema: props.schema, objectId: props.objectId },\n\t\t\t}\n\t\t},\n\n\t\tselectTab(id) {\n\t\t\tthis.activeId = id\n\t\t},\n\n\t\t/**\n\t\t * Roving-tabindex keyboard nav per the ARIA tablist pattern:\n\t\t * Left/Right move + activate, Home/End jump to the ends.\n\t\t *\n\t\t * @param {KeyboardEvent} event Key event.\n\t\t * @param {number} index Index of the focused tab.\n\t\t * @return {void}\n\t\t */\n\t\tonTabKeydown(event, index) {\n\t\t\tconst list = this.visibleProviders\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet next = -1\n\t\t\tif (event.key === 'ArrowRight' || event.key === 'ArrowDown') {\n\t\t\t\tnext = (index + 1) % list.length\n\t\t\t} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {\n\t\t\t\tnext = (index - 1 + list.length) % list.length\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = list.length - 1\n\t\t\t}\n\t\t\tif (next === -1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tconst provider = list[next]\n\t\t\tthis.activeId = provider.id\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst el = this.$el && this.$el.querySelector(`#${this.tabId(provider)}`)\n\t\t\t\tif (el && typeof el.focus === 'function') {\n\t\t\t\t\tel.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\ttabId(provider) {\n\t\t\treturn `cn-iw-tab-${provider.id}`\n\t\t},\n\n\t\tpanelId(provider) {\n\t\t\treturn `cn-iw-panel-${provider.id}`\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the active tab — a left border + subtle bg in\n\t\t * the app's brand colour. Inactive tabs get no accent var.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\ttabAccentStyle(provider) {\n\t\t\tif (provider.id !== this.activeId || !provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the single-mode header.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\taccentStyle(provider) {\n\t\t\tif (!provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-integration-widget {\n\twidth: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__tabbar {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tgap: 8px;\n\tborder-bottom: 1px solid var(--color-border);\n\tmargin-bottom: 12px;\n}\n\n.cn-integration-widget__tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 2px;\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.cn-integration-widget__tab {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 8px 12px;\n\tbackground: transparent;\n\tborder: none;\n\tborder-bottom: 2px solid transparent;\n\tborder-radius: var(--border-radius) var(--border-radius) 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.95em;\n\tcursor: pointer;\n}\n\n.cn-integration-widget__tab:hover {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__tab:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-integration-widget__tab--active {\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n\tborder-bottom-color: var(--cn-iw-accent, var(--color-primary-element));\n}\n\n.cn-integration-widget__tab-icon {\n\tcolor: var(--cn-iw-accent, currentColor);\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__tab-label {\n\twhite-space: nowrap;\n}\n\n.cn-integration-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 4px;\n\tmargin-bottom: 8px;\n\tborder-bottom: 2px solid var(--cn-iw-accent, var(--color-border));\n}\n\n.cn-integration-widget__header-icon {\n\tcolor: var(--cn-iw-accent, var(--color-main-text));\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__header-label {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__header-actions {\n\tmargin-inline-start: auto;\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__panel {\n\tflex: 1;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__panel:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n</style>\n"],"names":["CnIcon","CnIntegrationWidgetEmpty","CnActionsMenu","CnLeafMountHost","t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnIntegrationWidget.vue3.js","sources":["../../../../src/components/CnIntegrationWidget/CnIntegrationWidget.vue?vue&type=template&id=2d91b76f&lang.js"],"sourcesContent":["<!--\n CnIntegrationWidget — tabbed, app-faithful integration widget for\n OpenRegister object detail pages and dashboards.\n\n Supersedes the generic Phase E pair (CnIntegrationCard /\n CnIntegrationWidgetGrid) which rendered one one-size surface that\n erased each integrated app's visual identity. This widget gives every\n integration its own tab (app icon + label + accent), composing the\n existing bespoke per-leaf content component (`provider.tab`), PLUS a\n single-integration mode for manifest multi-placement.\n\n TWO MODES\n ---------\n - Tabbed (default): a tab strip with one tab per integration available\n for `surface` (optionally narrowed by `include`), sorted by `.order`.\n The active tab renders that leaf's content. Unavailable integrations\n still get a tab; clicking shows an NcEmptyContent set-up state.\n - Single (`:only=\"deck\"`): NO tab strip — just one integration's\n content under a compact header (app icon + label). This is what an\n app manifest places several times on one detail page (ADR-024).\n\n AVAILABILITY (Phase J-B)\n ------------------------\n Per-integration availability is sourced, in order:\n 1. descriptor `available` (boolean) when the descriptor carries it,\n 2. else the OCS capability\n `openregister.integrations.providers[].available`\n (getCapabilities()), distinguishing missing-app from\n not-configured via a `reason` field when present,\n 3. else `isAppInstalled(requiredApp)` (OC.appswebroots / capabilities).\n When unavailable, the leaf renders NcEmptyContent: app icon, a\n \"{App} not available\" / \"not configured\" name, a short description, and\n an NcButton linking to the leaf's setup docs\n (`https://openregister.conduction.nl/docs/Integrations/{id}/`).\n\n DESCRIPTOR CONTRACT (filled by the per-app waves)\n -------------------------------------------------\n In addition to the registry's existing fields, the widget reads:\n - icon (MDI name string) — the app icon, rendered via CnIcon.\n - accentColor (hex) — per-app brand tint for the active tab / header.\n - appName (string) — backing-app display name for empty copy\n (defaults to `label`).\n - docsUrl (string) — setup-docs link (defaults to the\n openregister.conduction.nl path derived from `id`).\n - surfaces (string[]) — optional surface allowlist (eligible\n everywhere when omitted).\n These are normalised in `src/integrations/registry.js`; the per-app\n fidelity waves refine accentColor/appName per leaf.\n\n MANIFEST PLACEMENT (ADR-024)\n ----------------------------\n An app manifest references this component by name and passes config as\n props, e.g. to place two single-integration widgets on a detail page:\n\n \"widgets\": [\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"deck\", \"surface\": \"detail-page\" } },\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"calendar\", \"surface\": \"detail-page\" } }\n ]\n\n The host wires `register` / `schema` / `objectId` from the object\n context. Omit `only` (and optionally pass `include`) for the tabbed\n surface.\n\n See ADR-019 (registry), ADR-022 (sidebar tab contract), ADR-024\n (manifest widget placement), ADR-004 (component composition).\n\n @example\n Tabbed, all detail-page integrations:\n <CnIntegrationWidget\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n\n @example\n Single-integration, manifest-placeable:\n <CnIntegrationWidget\n only=\"deck\"\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n-->\n<template>\n\t<div class=\"cn-integration-widget\" :data-surface=\"surface\" :data-mode=\"mode\">\n\t\t<!-- SINGLE MODE: one integration, no tab strip. -->\n\t\t<template v-if=\"mode === 'single'\">\n\t\t\t<div v-if=\"singleProvider\" class=\"cn-integration-widget__single\">\n\t\t\t\t<header class=\"cn-integration-widget__header\" :style=\"accentStyle(singleProvider)\">\n\t\t\t\t\t<CnIcon\n\t\t\t\t\t\tv-if=\"singleProvider.icon\"\n\t\t\t\t\t\t:name=\"singleProvider.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-integration-widget__header-icon\" />\n\t\t\t\t\t<span class=\"cn-integration-widget__header-label\">{{ singleProvider.label }}</span>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"singleProvider.label\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</header>\n\t\t\t\t<div class=\"cn-integration-widget__panel\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(singleProvider)\" />\n\t\t\t\t\t<CnLeafMountHost\n\t\t\t\t\t\tv-else-if=\"isMountLeaf(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:mountProps=\"leafMountProps(singleProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(singleProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(singleProvider)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- The leaf is not registered: its app is absent or did not load. -->\n\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\tv-else-if=\"appName\"\n\t\t\t\t:provider=\"absentProvider\"\n\t\t\t\treason=\"missing-app\"\n\t\t\t\tdata-testid=\"cn-integration-widget-absent\" />\n\t\t\t<NcEmptyContent v-else :name=\"unknownLeafLabel\" />\n\t\t</template>\n\n\t\t<!-- TABBED MODE: tab strip + active panel. -->\n\t\t<template v-else>\n\t\t\t<NcEmptyContent\n\t\t\t\tv-if=\"visibleProviders.length === 0\"\n\t\t\t\t:name=\"noIntegrationsLabel\" />\n\t\t\t<template v-else>\n\t\t\t\t<div class=\"cn-integration-widget__tabbar\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"cn-integration-widget__tabs\"\n\t\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t\t:aria-label=\"tabListLabel\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tv-for=\"(provider, idx) in visibleProviders\"\n\t\t\t\t\t\t\t:id=\"tabId(provider)\"\n\t\t\t\t\t\t\t:key=\"provider.id\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-integration-widget__tab--active': provider.id === activeId }\"\n\t\t\t\t\t\t\t:style=\"tabAccentStyle(provider)\"\n\t\t\t\t\t\t\t:aria-selected=\"provider.id === activeId ? 'true' : 'false'\"\n\t\t\t\t\t\t\t:tabindex=\"provider.id === activeId ? 0 : -1\"\n\t\t\t\t\t\t\t:aria-controls=\"panelId(provider)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-integration-widget-tab-${provider.id}`\"\n\t\t\t\t\t\t\t@click=\"selectTab(provider.id)\"\n\t\t\t\t\t\t\t@keydown=\"onTabKeydown($event, idx)\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"provider.icon\"\n\t\t\t\t\t\t\t\t:name=\"provider.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab-icon\" />\n\t\t\t\t\t\t\t<span class=\"cn-integration-widget__tab-label\">{{ provider.label }}</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"headerTitle\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"activeProvider\"\n\t\t\t\t\t:id=\"panelId(activeProvider)\"\n\t\t\t\t\tclass=\"cn-integration-widget__panel\"\n\t\t\t\t\trole=\"tabpanel\"\n\t\t\t\t\t:aria-labelledby=\"tabId(activeProvider)\"\n\t\t\t\t\ttabindex=\"0\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(activeProvider)\"\n\t\t\t\t\t\t:provider=\"activeProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(activeProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(activeProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(activeProvider)\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnLeafMountHost from '../CnLeafMountHost/CnLeafMountHost.vue'\nimport CnIntegrationWidgetEmpty from './CnIntegrationWidgetEmpty.vue'\nimport { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'\nimport { registerIntegrationIcons } from '../../integrations/icons.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\nimport { resolveProviderAvailability } from './availability.js'\n\n// Ensure every descriptor's MDI icon resolves through CnIcon regardless\n// of what the host app registered at bootstrap — otherwise tabs fall\n// back to the generic HelpCircleOutline glyph. Idempotent.\nregisterIntegrationIcons()\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnIntegrationWidget — tabbed integration widget + single-integration\n * mode. See the file-level docblock for the contract.\n */\nexport default {\n\tname: 'CnIntegrationWidget',\n\n\tcomponents: { NcEmptyContent, CnIcon, CnIntegrationWidgetEmpty, CnActionsMenu, CnLeafMountHost },\n\n\tprops: {\n\t\t/**\n\t\t * Optional registry override (used in tests so registrations\n\t\t * don't leak across cases). Defaults to the singleton.\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register id — a slug/uuid string, or the register\n\t\t * OBJECT a detail page injects. Accepts both for the same reason\n\t\t * `schema` does; see below.\n\t\t */\n\t\tregister: { type: [String, Object], default: '' },\n\t\t/**\n\t\t * OpenRegister schema — a slug/uuid string, or the schema OBJECT the\n\t\t * detail-page widget grid injects.\n\t\t *\n\t\t * This used to be `type: String`, and a detail page handed it the\n\t\t * resolved object instead. Vue reported the mismatch as a dev-only\n\t\t * warning (\"Expected String with value \\\"[object Object]\\\", got\n\t\t * Object\") and then passed the object through unchanged, so the leaf\n\t\t * built `/apps/openregister/api/schemas/[object Object]` and got a 404\n\t\t * — an unreadable URL rather than a named failure, and silent in\n\t\t * production where the warning is compiled out.\n\t\t *\n\t\t * `CnAuditTrailWidget` already accepted both shapes and collapsed the\n\t\t * object to its slug; this mirrors that so the two widgets cannot\n\t\t * disagree about what a detail page may hand them.\n\t\t */\n\t\tschema: { type: [String, Object], default: '' },\n\t\t/** Parent object id. Forwarded to each leaf. */\n\t\tobjectId: { type: [String, Number], default: '' },\n\t\t/**\n\t\t * Single-integration mode: render ONLY this leaf id with no tab\n\t\t * strip. The manifest multi-placement entry point (ADR-024).\n\t\t */\n\t\tonly: { type: String, default: '' },\n\t\t/**\n\t\t * Rendering surface — filters integrations by `surfaces.includes()`\n\t\t * when a descriptor declares a `surfaces` allowlist.\n\t\t */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/**\n\t\t * Explicit allowlist of leaf ids to show as tabs. When omitted,\n\t\t * all registered integrations eligible for `surface` are shown.\n\t\t *\n\t\t * @type {string[]|null}\n\t\t */\n\t\tinclude: { type: Array, default: null },\n\t\t/** Base API URL forwarded to each leaf content component. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Optional object type forwarded to each leaf content component. */\n\t\tobjectType: { type: String, default: '' },\n\t\t/**\n\t\t * Documentation link target for the header's shared Actions menu.\n\t\t * When non-empty a \"Documentation\" item opens it in a new tab;\n\t\t * empty (the default) hides the item.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: { type: String, default: '' },\n\t\t/**\n\t\t * Stable id carried on the Actions-menu refresh / request-feature\n\t\t * payloads and surface string. When empty the header title is\n\t\t * slugified as a fallback (see `resolvedWidgetId`).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: { type: String, default: '' },\n\n\t\t/**\n\t\t * The record's display name, handed to the leaf as `objectLabel` (a\n\t\t * leaf titles its panel or its dialog with it).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tobjectLabel: { type: String, default: '' },\n\n\t\t/**\n\t\t * The surface the leaf itself is told it renders on, when it differs\n\t\t * from `surface` (a leaf placed in a dialog that draws its sidebar-tab\n\t\t * look: `sidebar-tab`). Empty passes `surface`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tleafSurface: { type: String, default: '' },\n\n\t\t/**\n\t\t * The app that provides the `only` leaf, named in the empty state when\n\t\t * the leaf is not registered (\"Decidiq not available\"). Without it an\n\t\t * unregistered leaf shows the plain \"not available\" line.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: { type: String, default: '' },\n\n\t\t/** The icon of that empty state (an MDI name). */\n\t\tappIcon: { type: String, default: '' },\n\t},\n\n\tsetup(props) {\n\t\tconst { integrations, resolveTab } = useIntegrationRegistry(props.registry || undefined)\n\t\t// `resolveTab` swaps a lib-owned entry for THIS bundle's component\n\t\t// (openregister#1958); the entry's own `tab` may belong to another\n\t\t// bundle's Vue and then renders its nested components as literal\n\t\t// unknown elements. See `leafComponent`.\n\t\treturn { registryIntegrations: integrations, resolveRegistryTab: resolveTab }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Currently selected tab id (tabbed mode only). */\n\t\t\tactiveId: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** 'single' when `only` is set, else 'tabbed'. */\n\t\tmode() {\n\t\t\treturn this.only ? 'single' : 'tabbed'\n\t\t},\n\n\t\t/**\n\t\t * The register SLUG. A register object collapses to its slug/name/id,\n\t\t * mirroring `CnAuditTrailWidget.resolvedRegister`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedRegister() {\n\t\t\tconst r = this.register || ''\n\t\t\treturn typeof r === 'string' ? r : (r && (r.slug || r.name || r.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The schema SLUG. A schema object collapses to its slug/name/id.\n\t\t *\n\t\t * Leaves interpolate this straight into an OpenRegister URL, so an\n\t\t * object reaching them produces `/api/schemas/[object Object]` and a\n\t\t * 404 that names nothing. Collapsing here means every leaf receives\n\t\t * the string it already documents, and a caller that hands over the\n\t\t * whole schema object — which the detail-page widget grid does — is\n\t\t * served rather than silently broken.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedSchema() {\n\t\t\tconst s = this.schema || ''\n\t\t\treturn typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The provider for single mode (resolved from `only`), or null\n\t\t * when the id is unknown / not eligible for the surface.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tsingleProvider() {\n\t\t\tif (!this.only) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst all = this.registryIntegrations || []\n\t\t\tfor (const provider of all) {\n\t\t\t\tif (provider.id === this.only && this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Integrations shown as tabs: eligible for the surface, narrowed\n\t\t * by `include` when set, sorted by the registry's `order` (the\n\t\t * snapshot is already order-sorted, so we preserve that).\n\t\t *\n\t\t * @return {object[]}\n\t\t */\n\t\tvisibleProviders() {\n\t\t\tconst include = Array.isArray(this.include) ? this.include : null\n\t\t\treturn (this.registryIntegrations || []).filter((provider) => {\n\t\t\t\tif (!this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (include !== null && !include.includes(provider.id)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The currently active provider in tabbed mode.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveProvider() {\n\t\t\tconst list = this.visibleProviders\n\t\t\tfor (const provider of list) {\n\t\t\t\tif (provider.id === this.activeId) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn list.length > 0 ? list[0] : null\n\t\t},\n\n\t\tnoIntegrationsLabel() {\n\t\t\treturn t('nextcloud-vue', 'No integrations available yet.')\n\t\t},\n\n\t\t/**\n\t\t * The stand-in descriptor the empty state draws for an `only` leaf\n\t\t * that is not registered.\n\t\t *\n\t\t * @return {{id: string, appName: string, icon: string}}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tabsentProvider() {\n\t\t\treturn { id: this.only, appName: this.appName, icon: this.appIcon }\n\t\t},\n\n\t\tunknownLeafLabel() {\n\t\t\treturn t('nextcloud-vue', 'This integration is not available.')\n\t\t},\n\n\t\ttabListLabel() {\n\t\t\treturn t('nextcloud-vue', 'Integrations')\n\t\t},\n\n\t\t/**\n\t\t * Label shown by the active header — the single-mode provider's\n\t\t * label, else the active tab's label, else the generic tab-list\n\t\t * label. Fed to the shared Actions menu as its `title`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\theaderTitle() {\n\t\t\tif (this.mode === 'single') {\n\t\t\t\treturn this.singleProvider ? this.singleProvider.label : this.unknownLeafLabel\n\t\t\t}\n\t\t\treturn this.activeProvider ? this.activeProvider.label : this.tabListLabel\n\t\t},\n\n\t\t/**\n\t\t * Stable widget id for the Actions menu: the explicit `widgetId`\n\t\t * prop when set, else a slugified `headerTitle` (lowercase,\n\t\t * non-alphanumerics → '-'), falling back to 'integration'.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\tconst slug = String(this.headerTitle || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t\treturn slug || 'integration'\n\t\t},\n\t},\n\n\twatch: {\n\t\t/**\n\t\t * Keep the active tab valid when the provider set changes (late\n\t\t * registration, surface/include change). Re-anchor to the first\n\t\t * provider if the current active id disappears.\n\t\t */\n\t\tvisibleProviders: {\n\t\t\timmediate: true,\n\t\t\thandler(list) {\n\t\t\t\tif (this.mode !== 'tabbed') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ids = (list || []).map((p) => p.id)\n\t\t\t\tif (ids.length === 0) {\n\t\t\t\t\tthis.activeId = ''\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!ids.includes(this.activeId)) {\n\t\t\t\t\tthis.activeId = ids[0]\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Whether a provider is eligible for the current surface. A\n\t\t * descriptor with a `surfaces` allowlist must include it; without\n\t\t * one, every provider is eligible (matches CnIntegrationWidgetGrid).\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\teligibleForSurface(provider) {\n\t\t\tconst surfaces = provider.surfaces\n\t\t\tif (Array.isArray(surfaces) && surfaces.length > 0) {\n\t\t\t\treturn surfaces.includes(this.surface)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Resolve whether the backing app for a provider is available +\n\t\t * configured. See file docblock for the source order.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\tisAvailable(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).available\n\t\t},\n\n\t\t/**\n\t\t * The unavailable reason ('missing-app' | 'not-configured' |\n\t\t * 'unknown') for empty-state copy.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {string}\n\t\t */\n\t\tunavailableReason(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).reason\n\t\t},\n\n\t\t/**\n\t\t * The content component for a leaf panel. Resolved through the\n\t\t * registry composable so a lib-owned id renders THIS bundle's copy\n\t\t * of its `tab`; the entry's stored object is only used for a\n\t\t * consumer-custom id, which lives in the consumer's own bundle.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object|null} Vue component, or null.\n\t\t */\n\t\tleafComponent(provider) {\n\t\t\tif (!provider || typeof provider.id !== 'string') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst local = typeof this.resolveRegistryTab === 'function'\n\t\t\t\t? this.resolveRegistryTab(provider.id)\n\t\t\t\t: null\n\t\t\treturn local || provider.tab || null\n\t\t},\n\n\t\t/**\n\t\t * Props forwarded to a leaf's content component. Matches the\n\t\t * shared shape consumed by the bespoke `Cn<X>Tab` components.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object}\n\t\t */\n\t\tleafProps(provider) {\n\t\t\treturn {\n\t\t\t\tintegrationId: provider.id,\n\t\t\t\tregister: this.resolvedRegister,\n\t\t\t\tschema: this.resolvedSchema,\n\t\t\t\tobjectId: this.objectId !== '' && this.objectId !== null && this.objectId !== undefined\n\t\t\t\t\t? String(this.objectId)\n\t\t\t\t\t: '',\n\n\t\t\t\tobjectType: this.objectType,\n\t\t\t\tapiBase: this.apiBase,\n\t\t\t\tsurface: this.leafSurface || this.surface,\n\t\t\t\t...(this.objectLabel ? { objectLabel: this.objectLabel } : {}),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a leaf is a mount-mode leaf (ADR-066): it mounts its own\n\t\t * framework into a host element instead of being a component.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tisMountLeaf(provider) {\n\t\t\treturn Boolean(provider)\n\t\t\t\t&& provider.renderMode === 'mount'\n\t\t\t\t&& typeof provider.mount === 'function'\n\t\t\t\t&& typeof provider.unmount === 'function'\n\t\t},\n\n\t\t/**\n\t\t * What a mount-mode leaf is mounted with: the record context, also as\n\t\t * `integrationContext`, and the record's label.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {object} The mount props.\n\t\t */\n\t\tleafMountProps(provider) {\n\t\t\tconst props = this.leafProps(provider)\n\t\t\treturn {\n\t\t\t\t...props,\n\t\t\t\tintegrationContext: { register: props.register, schema: props.schema, objectId: props.objectId },\n\t\t\t}\n\t\t},\n\n\t\tselectTab(id) {\n\t\t\tthis.activeId = id\n\t\t},\n\n\t\t/**\n\t\t * Roving-tabindex keyboard nav per the ARIA tablist pattern:\n\t\t * Left/Right move + activate, Home/End jump to the ends.\n\t\t *\n\t\t * @param {KeyboardEvent} event Key event.\n\t\t * @param {number} index Index of the focused tab.\n\t\t * @return {void}\n\t\t */\n\t\tonTabKeydown(event, index) {\n\t\t\tconst list = this.visibleProviders\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet next = -1\n\t\t\tif (event.key === 'ArrowRight' || event.key === 'ArrowDown') {\n\t\t\t\tnext = (index + 1) % list.length\n\t\t\t} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {\n\t\t\t\tnext = (index - 1 + list.length) % list.length\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = list.length - 1\n\t\t\t}\n\t\t\tif (next === -1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tconst provider = list[next]\n\t\t\tthis.activeId = provider.id\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst el = this.$el && this.$el.querySelector(`#${this.tabId(provider)}`)\n\t\t\t\tif (el && typeof el.focus === 'function') {\n\t\t\t\t\tel.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\ttabId(provider) {\n\t\t\treturn `cn-iw-tab-${provider.id}`\n\t\t},\n\n\t\tpanelId(provider) {\n\t\t\treturn `cn-iw-panel-${provider.id}`\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the active tab — a left border + subtle bg in\n\t\t * the app's brand colour. Inactive tabs get no accent var.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\ttabAccentStyle(provider) {\n\t\t\tif (provider.id !== this.activeId || !provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the single-mode header.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\taccentStyle(provider) {\n\t\t\tif (!provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-integration-widget {\n\twidth: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__tabbar {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tgap: 8px;\n\tborder-bottom: 1px solid var(--color-border);\n\tmargin-bottom: 12px;\n}\n\n.cn-integration-widget__tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 2px;\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.cn-integration-widget__tab {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 8px 12px;\n\tbackground: transparent;\n\tborder: none;\n\tborder-bottom: 2px solid transparent;\n\tborder-radius: var(--border-radius) var(--border-radius) 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.95em;\n\tcursor: pointer;\n}\n\n.cn-integration-widget__tab:hover {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__tab:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-integration-widget__tab--active {\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n\tborder-bottom-color: var(--cn-iw-accent, var(--color-primary-element));\n}\n\n.cn-integration-widget__tab-icon {\n\tcolor: var(--cn-iw-accent, currentColor);\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__tab-label {\n\twhite-space: nowrap;\n}\n\n.cn-integration-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 4px;\n\tmargin-bottom: 8px;\n\tborder-bottom: 2px solid var(--cn-iw-accent, var(--color-border));\n}\n\n.cn-integration-widget__header-icon {\n\tcolor: var(--cn-iw-accent, var(--color-main-text));\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__header-label {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__header-actions {\n\tmargin-inline-start: auto;\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__panel {\n\tflex: 1;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__panel:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n</style>\n"],"names":["_createElementBlock","_createBlock","_createElementVNode","_toDisplayString","_createVNode"],"mappings":";;;;;AA0KS,EAAA,KAAA,EAAA,+BAAA;;AAOE,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qCAA2B,GAAA;AAClC,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uCAAC,GAAA;AASR,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,8BAAA,GAAA;;;;;;;;;;;;;;;uBApBFA;AAAA,IAAA,KAAI,EAAC,uBAAuB;AAAE,IAAA,cAAQ,EAAA,MAAA,CAAA,OAAA;AAAA,IAAA,WAAA,EAAA,QAAA,CAAA,IAAA;;KAEnC,QAAA,CAAA,IAAA,KAAA,QAAA;;WACD,QAAA,CAAA,cAAA;;kCAgBI,CAAA,QAAA,EAAA;;;;AAbD,kBAAA,CAAA,QAAA,CAAA,cAAa,CAAA,IAAA;oCADpBC,WAIc,CAAA,iBAAA,EAAA;;wBAFZ,IAAI,EAAE,QAAc,CAAA,cAAA,CAAC,IAAI;AACzB,wBAAA,IAAI,EAAE,EAAE;AACT,wBAAA,KAAK,EAAC,oCAAO;;;AACd,kBAAAC,kBAAA,CAAuC,MAAvC,EAAA,UAAuC,EAAAC,eAAA,CAAA,QAAA,CAAA,cAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;AACvC,kBAAAD,kBAAA,CAOM,OAPN,UAOM,EAAA;oBANLE,WAKkC,CAAA,wBAAA,EAAA;AAJhC,sBAAA,gBAAgB,EAAE,MAAc,CAAA,gBAAA;AAChC,sBAAA,QAAQ,EAAE,QAAa,CAAA,gBAAA;sBACvB,KAAK,EAAE,QAAc,CAAA,cAAA,CAAC,KAAK;AAC3B,sBAAA,OAAM,YAAA,QAAA,CAAA,gBAAA,CAAA,CAAA;AACP,sBAAA,UAAU,EAAC,uBAAsB;;;;AAGpC,gBAAAF,kBAAA,QAAA;AAEQ,kBAAA,CAAA,CAAA,QAAA,CAAA,WAAA,CAAA,QAAA,CAAA,cAAA,CAAA;oCADPD;;AAEC,wBAAA,QAAA,EAAA,QAAA,CAAA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnIntegrationWidget.vue3.js","sources":["../../../../src/components/CnIntegrationWidget/CnIntegrationWidget.vue?vue&type=template&id=1b6b243b&lang.js"],"sourcesContent":["<!--\n CnIntegrationWidget — tabbed, app-faithful integration widget for\n OpenRegister object detail pages and dashboards.\n\n Supersedes the generic Phase E pair (CnIntegrationCard /\n CnIntegrationWidgetGrid) which rendered one one-size surface that\n erased each integrated app's visual identity. This widget gives every\n integration its own tab (app icon + label + accent), composing the\n existing bespoke per-leaf content component (`provider.tab`), PLUS a\n single-integration mode for manifest multi-placement.\n\n TWO MODES\n ---------\n - Tabbed (default): a tab strip with one tab per integration available\n for `surface` (optionally narrowed by `include`), sorted by `.order`.\n The active tab renders that leaf's content. Unavailable integrations\n still get a tab; clicking shows an NcEmptyContent set-up state.\n - Single (`:only=\"deck\"`): NO tab strip — just one integration's\n content under a compact header (app icon + label). This is what an\n app manifest places several times on one detail page (ADR-024).\n\n AVAILABILITY (Phase J-B)\n ------------------------\n Per-integration availability is sourced, in order:\n 1. descriptor `available` (boolean) when the descriptor carries it,\n 2. else the OCS capability\n `openregister.integrations.providers[].available`\n (getCapabilities()), distinguishing missing-app from\n not-configured via a `reason` field when present,\n 3. else `isAppInstalled(requiredApp)` (OC.appswebroots / capabilities).\n When unavailable, the leaf renders NcEmptyContent: app icon, a\n \"{App} not available\" / \"not configured\" name, a short description, and\n an NcButton linking to the leaf's setup docs\n (`https://openregister.conduction.nl/docs/Integrations/{id}/`).\n\n DESCRIPTOR CONTRACT (filled by the per-app waves)\n -------------------------------------------------\n In addition to the registry's existing fields, the widget reads:\n - icon (MDI name string) — the app icon, rendered via CnIcon.\n - accentColor (hex) — per-app brand tint for the active tab / header.\n - appName (string) — backing-app display name for empty copy\n (defaults to `label`).\n - docsUrl (string) — setup-docs link (defaults to the\n openregister.conduction.nl path derived from `id`).\n - surfaces (string[]) — optional surface allowlist (eligible\n everywhere when omitted).\n These are normalised in `src/integrations/registry.js`; the per-app\n fidelity waves refine accentColor/appName per leaf.\n\n MANIFEST PLACEMENT (ADR-024)\n ----------------------------\n An app manifest references this component by name and passes config as\n props, e.g. to place two single-integration widgets on a detail page:\n\n \"widgets\": [\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"deck\", \"surface\": \"detail-page\" } },\n { \"component\": \"CnIntegrationWidget\",\n \"props\": { \"only\": \"calendar\", \"surface\": \"detail-page\" } }\n ]\n\n The host wires `register` / `schema` / `objectId` from the object\n context. Omit `only` (and optionally pass `include`) for the tabbed\n surface.\n\n See ADR-019 (registry), ADR-022 (sidebar tab contract), ADR-024\n (manifest widget placement), ADR-004 (component composition).\n\n @example\n Tabbed, all detail-page integrations:\n <CnIntegrationWidget\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n\n @example\n Single-integration, manifest-placeable:\n <CnIntegrationWidget\n only=\"deck\"\n register=\"decidesk\"\n schema=\"meeting\"\n object-id=\"obj-1\" />\n-->\n<template>\n\t<div class=\"cn-integration-widget\" :data-surface=\"surface\" :data-mode=\"mode\">\n\t\t<!-- SINGLE MODE: one integration, no tab strip. -->\n\t\t<template v-if=\"mode === 'single'\">\n\t\t\t<div v-if=\"singleProvider\" class=\"cn-integration-widget__single\">\n\t\t\t\t<header class=\"cn-integration-widget__header\" :style=\"accentStyle(singleProvider)\">\n\t\t\t\t\t<CnIcon\n\t\t\t\t\t\tv-if=\"singleProvider.icon\"\n\t\t\t\t\t\t:name=\"singleProvider.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-integration-widget__header-icon\" />\n\t\t\t\t\t<span class=\"cn-integration-widget__header-label\">{{ singleProvider.label }}</span>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"singleProvider.label\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</header>\n\t\t\t\t<div class=\"cn-integration-widget__panel\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(singleProvider)\" />\n\t\t\t\t\t<CnLeafMountHost\n\t\t\t\t\t\tv-else-if=\"isMountLeaf(singleProvider)\"\n\t\t\t\t\t\t:provider=\"singleProvider\"\n\t\t\t\t\t\t:mountProps=\"leafMountProps(singleProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(singleProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(singleProvider)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- The leaf is not registered: its app is absent or did not load. -->\n\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\tv-else-if=\"appName\"\n\t\t\t\t:provider=\"absentProvider\"\n\t\t\t\treason=\"missing-app\"\n\t\t\t\tdata-testid=\"cn-integration-widget-absent\" />\n\t\t\t<NcEmptyContent v-else :name=\"unknownLeafLabel\" />\n\t\t</template>\n\n\t\t<!-- TABBED MODE: tab strip + active panel. -->\n\t\t<template v-else>\n\t\t\t<NcEmptyContent\n\t\t\t\tv-if=\"visibleProviders.length === 0\"\n\t\t\t\t:name=\"noIntegrationsLabel\" />\n\t\t\t<template v-else>\n\t\t\t\t<div class=\"cn-integration-widget__tabbar\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"cn-integration-widget__tabs\"\n\t\t\t\t\t\trole=\"tablist\"\n\t\t\t\t\t\t:aria-label=\"tabListLabel\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tv-for=\"(provider, idx) in visibleProviders\"\n\t\t\t\t\t\t\t:id=\"tabId(provider)\"\n\t\t\t\t\t\t\t:key=\"provider.id\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"tab\"\n\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-integration-widget__tab--active': provider.id === activeId }\"\n\t\t\t\t\t\t\t:style=\"tabAccentStyle(provider)\"\n\t\t\t\t\t\t\t:aria-selected=\"provider.id === activeId ? 'true' : 'false'\"\n\t\t\t\t\t\t\t:tabindex=\"provider.id === activeId ? 0 : -1\"\n\t\t\t\t\t\t\t:aria-controls=\"panelId(provider)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-integration-widget-tab-${provider.id}`\"\n\t\t\t\t\t\t\t@click=\"selectTab(provider.id)\"\n\t\t\t\t\t\t\t@keydown=\"onTabKeydown($event, idx)\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"provider.icon\"\n\t\t\t\t\t\t\t\t:name=\"provider.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\tclass=\"cn-integration-widget__tab-icon\" />\n\t\t\t\t\t\t\t<span class=\"cn-integration-widget__tab-label\">{{ provider.label }}</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-integration-widget__header-actions\">\n\t\t\t\t\t\t<CnActionsMenu\n\t\t\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t\t\t:title=\"headerTitle\"\n\t\t\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t\t\ttestidBase=\"cn-integration-widget\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"activeProvider\"\n\t\t\t\t\t:id=\"panelId(activeProvider)\"\n\t\t\t\t\tclass=\"cn-integration-widget__panel\"\n\t\t\t\t\trole=\"tabpanel\"\n\t\t\t\t\t:aria-labelledby=\"tabId(activeProvider)\"\n\t\t\t\t\ttabindex=\"0\">\n\t\t\t\t\t<CnIntegrationWidgetEmpty\n\t\t\t\t\t\tv-if=\"!isAvailable(activeProvider)\"\n\t\t\t\t\t\t:provider=\"activeProvider\"\n\t\t\t\t\t\t:reason=\"unavailableReason(activeProvider)\" />\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"leafComponent(activeProvider)\"\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\tv-bind=\"leafProps(activeProvider)\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnLeafMountHost from '../CnLeafMountHost/CnLeafMountHost.vue'\nimport CnIntegrationWidgetEmpty from './CnIntegrationWidgetEmpty.vue'\nimport { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'\nimport { registerIntegrationIcons } from '../../integrations/icons.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\nimport { resolveProviderAvailability } from './availability.js'\n\n// Ensure every descriptor's MDI icon resolves through CnIcon regardless\n// of what the host app registered at bootstrap — otherwise tabs fall\n// back to the generic HelpCircleOutline glyph. Idempotent.\nregisterIntegrationIcons()\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnIntegrationWidget — tabbed integration widget + single-integration\n * mode. See the file-level docblock for the contract.\n */\nexport default {\n\tname: 'CnIntegrationWidget',\n\n\tcomponents: { NcEmptyContent, CnIcon, CnIntegrationWidgetEmpty, CnActionsMenu, CnLeafMountHost },\n\n\tprops: {\n\t\t/**\n\t\t * Optional registry override (used in tests so registrations\n\t\t * don't leak across cases). Defaults to the singleton.\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register id — a slug/uuid string, or the register\n\t\t * OBJECT a detail page injects. Accepts both for the same reason\n\t\t * `schema` does; see below.\n\t\t */\n\t\tregister: { type: [String, Object], default: '' },\n\t\t/**\n\t\t * OpenRegister schema — a slug/uuid string, or the schema OBJECT the\n\t\t * detail-page widget grid injects.\n\t\t *\n\t\t * This used to be `type: String`, and a detail page handed it the\n\t\t * resolved object instead. Vue reported the mismatch as a dev-only\n\t\t * warning (\"Expected String with value \\\"[object Object]\\\", got\n\t\t * Object\") and then passed the object through unchanged, so the leaf\n\t\t * built `/apps/openregister/api/schemas/[object Object]` and got a 404\n\t\t * — an unreadable URL rather than a named failure, and silent in\n\t\t * production where the warning is compiled out.\n\t\t *\n\t\t * `CnAuditTrailWidget` already accepted both shapes and collapsed the\n\t\t * object to its slug; this mirrors that so the two widgets cannot\n\t\t * disagree about what a detail page may hand them.\n\t\t */\n\t\tschema: { type: [String, Object], default: '' },\n\t\t/** Parent object id. Forwarded to each leaf. */\n\t\tobjectId: { type: [String, Number], default: '' },\n\t\t/**\n\t\t * Single-integration mode: render ONLY this leaf id with no tab\n\t\t * strip. The manifest multi-placement entry point (ADR-024).\n\t\t */\n\t\tonly: { type: String, default: '' },\n\t\t/**\n\t\t * Rendering surface — filters integrations by `surfaces.includes()`\n\t\t * when a descriptor declares a `surfaces` allowlist.\n\t\t */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/**\n\t\t * Explicit allowlist of leaf ids to show as tabs. When omitted,\n\t\t * all registered integrations eligible for `surface` are shown.\n\t\t *\n\t\t * @type {string[]|null}\n\t\t */\n\t\tinclude: { type: Array, default: null },\n\t\t/** Base API URL forwarded to each leaf content component. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Optional object type forwarded to each leaf content component. */\n\t\tobjectType: { type: String, default: '' },\n\t\t/**\n\t\t * Documentation link target for the header's shared Actions menu.\n\t\t * When non-empty a \"Documentation\" item opens it in a new tab;\n\t\t * empty (the default) hides the item.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: { type: String, default: '' },\n\t\t/**\n\t\t * Stable id carried on the Actions-menu refresh / request-feature\n\t\t * payloads and surface string. When empty the header title is\n\t\t * slugified as a fallback (see `resolvedWidgetId`).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: { type: String, default: '' },\n\n\t\t/**\n\t\t * The record's display name, handed to the leaf as `objectLabel` (a\n\t\t * leaf titles its panel or its dialog with it).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tobjectLabel: { type: String, default: '' },\n\n\t\t/**\n\t\t * Extra props for the leaf, set by the placement (a manifest widget's\n\t\t * `props.placementProps`), for example the kinds a finance leaf shows. The\n\t\t * record's own identity always wins over a key of the same name.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tplacementProps: { type: Object, default: () => ({}) },\n\n\t\t/**\n\t\t * The surface the leaf itself is told it renders on, when it differs\n\t\t * from `surface` (a leaf placed in a dialog that draws its sidebar-tab\n\t\t * look: `sidebar-tab`). Empty passes `surface`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tleafSurface: { type: String, default: '' },\n\n\t\t/**\n\t\t * The app that provides the `only` leaf, named in the empty state when\n\t\t * the leaf is not registered (\"Decidiq not available\"). Without it an\n\t\t * unregistered leaf shows the plain \"not available\" line.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: { type: String, default: '' },\n\n\t\t/** The icon of that empty state (an MDI name). */\n\t\tappIcon: { type: String, default: '' },\n\t},\n\n\tsetup(props) {\n\t\tconst { integrations, resolveTab } = useIntegrationRegistry(props.registry || undefined)\n\t\t// `resolveTab` swaps a lib-owned entry for THIS bundle's component\n\t\t// (openregister#1958); the entry's own `tab` may belong to another\n\t\t// bundle's Vue and then renders its nested components as literal\n\t\t// unknown elements. See `leafComponent`.\n\t\treturn { registryIntegrations: integrations, resolveRegistryTab: resolveTab }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Currently selected tab id (tabbed mode only). */\n\t\t\tactiveId: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** 'single' when `only` is set, else 'tabbed'. */\n\t\tmode() {\n\t\t\treturn this.only ? 'single' : 'tabbed'\n\t\t},\n\n\t\t/**\n\t\t * The register SLUG. A register object collapses to its slug/name/id,\n\t\t * mirroring `CnAuditTrailWidget.resolvedRegister`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedRegister() {\n\t\t\tconst r = this.register || ''\n\t\t\treturn typeof r === 'string' ? r : (r && (r.slug || r.name || r.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The schema SLUG. A schema object collapses to its slug/name/id.\n\t\t *\n\t\t * Leaves interpolate this straight into an OpenRegister URL, so an\n\t\t * object reaching them produces `/api/schemas/[object Object]` and a\n\t\t * 404 that names nothing. Collapsing here means every leaf receives\n\t\t * the string it already documents, and a caller that hands over the\n\t\t * whole schema object — which the detail-page widget grid does — is\n\t\t * served rather than silently broken.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedSchema() {\n\t\t\tconst s = this.schema || ''\n\t\t\treturn typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The provider for single mode (resolved from `only`), or null\n\t\t * when the id is unknown / not eligible for the surface.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tsingleProvider() {\n\t\t\tif (!this.only) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst all = this.registryIntegrations || []\n\t\t\tfor (const provider of all) {\n\t\t\t\tif (provider.id === this.only && this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Integrations shown as tabs: eligible for the surface, narrowed\n\t\t * by `include` when set, sorted by the registry's `order` (the\n\t\t * snapshot is already order-sorted, so we preserve that).\n\t\t *\n\t\t * @return {object[]}\n\t\t */\n\t\tvisibleProviders() {\n\t\t\tconst include = Array.isArray(this.include) ? this.include : null\n\t\t\treturn (this.registryIntegrations || []).filter((provider) => {\n\t\t\t\tif (!this.eligibleForSurface(provider)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif (include !== null && !include.includes(provider.id)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The currently active provider in tabbed mode.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tactiveProvider() {\n\t\t\tconst list = this.visibleProviders\n\t\t\tfor (const provider of list) {\n\t\t\t\tif (provider.id === this.activeId) {\n\t\t\t\t\treturn provider\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn list.length > 0 ? list[0] : null\n\t\t},\n\n\t\tnoIntegrationsLabel() {\n\t\t\treturn t('nextcloud-vue', 'No integrations available yet.')\n\t\t},\n\n\t\t/**\n\t\t * The stand-in descriptor the empty state draws for an `only` leaf\n\t\t * that is not registered.\n\t\t *\n\t\t * @return {{id: string, appName: string, icon: string}}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tabsentProvider() {\n\t\t\treturn { id: this.only, appName: this.appName, icon: this.appIcon }\n\t\t},\n\n\t\tunknownLeafLabel() {\n\t\t\treturn t('nextcloud-vue', 'This integration is not available.')\n\t\t},\n\n\t\ttabListLabel() {\n\t\t\treturn t('nextcloud-vue', 'Integrations')\n\t\t},\n\n\t\t/**\n\t\t * Label shown by the active header — the single-mode provider's\n\t\t * label, else the active tab's label, else the generic tab-list\n\t\t * label. Fed to the shared Actions menu as its `title`.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\theaderTitle() {\n\t\t\tif (this.mode === 'single') {\n\t\t\t\treturn this.singleProvider ? this.singleProvider.label : this.unknownLeafLabel\n\t\t\t}\n\t\t\treturn this.activeProvider ? this.activeProvider.label : this.tabListLabel\n\t\t},\n\n\t\t/**\n\t\t * Stable widget id for the Actions menu: the explicit `widgetId`\n\t\t * prop when set, else a slugified `headerTitle` (lowercase,\n\t\t * non-alphanumerics → '-'), falling back to 'integration'.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\tconst slug = String(this.headerTitle || '')\n\t\t\t\t.toLowerCase()\n\t\t\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t\t\t.replace(/^-+|-+$/g, '')\n\t\t\treturn slug || 'integration'\n\t\t},\n\t},\n\n\twatch: {\n\t\t/**\n\t\t * Keep the active tab valid when the provider set changes (late\n\t\t * registration, surface/include change). Re-anchor to the first\n\t\t * provider if the current active id disappears.\n\t\t */\n\t\tvisibleProviders: {\n\t\t\timmediate: true,\n\t\t\thandler(list) {\n\t\t\t\tif (this.mode !== 'tabbed') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ids = (list || []).map((p) => p.id)\n\t\t\t\tif (ids.length === 0) {\n\t\t\t\t\tthis.activeId = ''\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!ids.includes(this.activeId)) {\n\t\t\t\t\tthis.activeId = ids[0]\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Whether a provider is eligible for the current surface. A\n\t\t * descriptor with a `surfaces` allowlist must include it; without\n\t\t * one, every provider is eligible (matches CnIntegrationWidgetGrid).\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\teligibleForSurface(provider) {\n\t\t\tconst surfaces = provider.surfaces\n\t\t\tif (Array.isArray(surfaces) && surfaces.length > 0) {\n\t\t\t\treturn surfaces.includes(this.surface)\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Resolve whether the backing app for a provider is available +\n\t\t * configured. See file docblock for the source order.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {boolean}\n\t\t */\n\t\tisAvailable(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).available\n\t\t},\n\n\t\t/**\n\t\t * The unavailable reason ('missing-app' | 'not-configured' |\n\t\t * 'unknown') for empty-state copy.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {string}\n\t\t */\n\t\tunavailableReason(provider) {\n\t\t\treturn resolveProviderAvailability(provider, { isAppInstalled }).reason\n\t\t},\n\n\t\t/**\n\t\t * The content component for a leaf panel. Resolved through the\n\t\t * registry composable so a lib-owned id renders THIS bundle's copy\n\t\t * of its `tab`; the entry's stored object is only used for a\n\t\t * consumer-custom id, which lives in the consumer's own bundle.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object|null} Vue component, or null.\n\t\t */\n\t\tleafComponent(provider) {\n\t\t\tif (!provider || typeof provider.id !== 'string') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst local = typeof this.resolveRegistryTab === 'function'\n\t\t\t\t? this.resolveRegistryTab(provider.id)\n\t\t\t\t: null\n\t\t\treturn local || provider.tab || null\n\t\t},\n\n\t\t/**\n\t\t * Props forwarded to a leaf's content component. Matches the\n\t\t * shared shape consumed by the bespoke `Cn<X>Tab` components.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object}\n\t\t */\n\t\tleafProps(provider) {\n\t\t\treturn {\n\t\t\t\t...this.placementProps,\n\t\t\t\tintegrationId: provider.id,\n\t\t\t\tregister: this.resolvedRegister,\n\t\t\t\tschema: this.resolvedSchema,\n\t\t\t\tobjectId: this.objectId !== '' && this.objectId !== null && this.objectId !== undefined\n\t\t\t\t\t? String(this.objectId)\n\t\t\t\t\t: '',\n\n\t\t\t\tobjectType: this.objectType,\n\t\t\t\tapiBase: this.apiBase,\n\t\t\t\tsurface: this.leafSurface || this.surface,\n\t\t\t\t...(this.objectLabel ? { objectLabel: this.objectLabel } : {}),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a leaf is a mount-mode leaf (ADR-066): it mounts its own\n\t\t * framework into a host element instead of being a component.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/integration-widget-mounts-leaves/specs/integration-widget-mounts-leaves/spec.md#requirement-a-single-integration-widget-mounts-a-mount-mode-leaf\n\t\t */\n\t\tisMountLeaf(provider) {\n\t\t\treturn Boolean(provider)\n\t\t\t\t&& provider.renderMode === 'mount'\n\t\t\t\t&& typeof provider.mount === 'function'\n\t\t\t\t&& typeof provider.unmount === 'function'\n\t\t},\n\n\t\t/**\n\t\t * What a mount-mode leaf is mounted with: the record context, also as\n\t\t * `integrationContext`, and the record's label.\n\t\t *\n\t\t * @param {object} provider The descriptor.\n\t\t * @return {object} The mount props.\n\t\t */\n\t\tleafMountProps(provider) {\n\t\t\tconst props = this.leafProps(provider)\n\t\t\treturn {\n\t\t\t\t...props,\n\t\t\t\tintegrationContext: { register: props.register, schema: props.schema, objectId: props.objectId },\n\t\t\t}\n\t\t},\n\n\t\tselectTab(id) {\n\t\t\tthis.activeId = id\n\t\t},\n\n\t\t/**\n\t\t * Roving-tabindex keyboard nav per the ARIA tablist pattern:\n\t\t * Left/Right move + activate, Home/End jump to the ends.\n\t\t *\n\t\t * @param {KeyboardEvent} event Key event.\n\t\t * @param {number} index Index of the focused tab.\n\t\t * @return {void}\n\t\t */\n\t\tonTabKeydown(event, index) {\n\t\t\tconst list = this.visibleProviders\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet next = -1\n\t\t\tif (event.key === 'ArrowRight' || event.key === 'ArrowDown') {\n\t\t\t\tnext = (index + 1) % list.length\n\t\t\t} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {\n\t\t\t\tnext = (index - 1 + list.length) % list.length\n\t\t\t} else if (event.key === 'Home') {\n\t\t\t\tnext = 0\n\t\t\t} else if (event.key === 'End') {\n\t\t\t\tnext = list.length - 1\n\t\t\t}\n\t\t\tif (next === -1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tconst provider = list[next]\n\t\t\tthis.activeId = provider.id\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst el = this.$el && this.$el.querySelector(`#${this.tabId(provider)}`)\n\t\t\t\tif (el && typeof el.focus === 'function') {\n\t\t\t\t\tel.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\ttabId(provider) {\n\t\t\treturn `cn-iw-tab-${provider.id}`\n\t\t},\n\n\t\tpanelId(provider) {\n\t\t\treturn `cn-iw-panel-${provider.id}`\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the active tab — a left border + subtle bg in\n\t\t * the app's brand colour. Inactive tabs get no accent var.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\ttabAccentStyle(provider) {\n\t\t\tif (provider.id !== this.activeId || !provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\n\t\t/**\n\t\t * Accent tint for the single-mode header.\n\t\t *\n\t\t * @param {object} provider Normalised registry entry.\n\t\t * @return {object} Style object.\n\t\t */\n\t\taccentStyle(provider) {\n\t\t\tif (!provider.accentColor) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn { '--cn-iw-accent': provider.accentColor }\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-integration-widget {\n\twidth: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__tabbar {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tgap: 8px;\n\tborder-bottom: 1px solid var(--color-border);\n\tmargin-bottom: 12px;\n}\n\n.cn-integration-widget__tabs {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 2px;\n\tflex: 1;\n\tmin-width: 0;\n}\n\n.cn-integration-widget__tab {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 8px 12px;\n\tbackground: transparent;\n\tborder: none;\n\tborder-bottom: 2px solid transparent;\n\tborder-radius: var(--border-radius) var(--border-radius) 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.95em;\n\tcursor: pointer;\n}\n\n.cn-integration-widget__tab:hover {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__tab:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-integration-widget__tab--active {\n\tcolor: var(--color-main-text);\n\tfont-weight: 600;\n\tborder-bottom-color: var(--cn-iw-accent, var(--color-primary-element));\n}\n\n.cn-integration-widget__tab-icon {\n\tcolor: var(--cn-iw-accent, currentColor);\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__tab-label {\n\twhite-space: nowrap;\n}\n\n.cn-integration-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 4px;\n\tmargin-bottom: 8px;\n\tborder-bottom: 2px solid var(--cn-iw-accent, var(--color-border));\n}\n\n.cn-integration-widget__header-icon {\n\tcolor: var(--cn-iw-accent, var(--color-main-text));\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__header-label {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-integration-widget__header-actions {\n\tmargin-inline-start: auto;\n\tflex-shrink: 0;\n}\n\n.cn-integration-widget__panel {\n\tflex: 1;\n\tmin-height: 0;\n}\n\n.cn-integration-widget__panel:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n</style>\n"],"names":["_createElementBlock","_createBlock","_createElementVNode","_toDisplayString","_createVNode"],"mappings":";;;;;AA0KS,EAAA,KAAA,EAAA,+BAAA;;AAOE,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qCAA2B,GAAA;AAClC,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uCAAC,GAAA;AASR,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,8BAAA,GAAA;;;;;;;;;;;;;;;uBApBFA;AAAA,IAAA,KAAI,EAAC,uBAAuB;AAAE,IAAA,cAAQ,EAAA,MAAA,CAAA,OAAA;AAAA,IAAA,WAAA,EAAA,QAAA,CAAA,IAAA;;KAEnC,QAAA,CAAA,IAAA,KAAA,QAAA;;WACD,QAAA,CAAA,cAAA;;kCAgBI,CAAA,QAAA,EAAA;;;;AAbD,kBAAA,CAAA,QAAA,CAAA,cAAa,CAAA,IAAA;oCADpBC,WAIc,CAAA,iBAAA,EAAA;;wBAFZ,IAAI,EAAE,QAAc,CAAA,cAAA,CAAC,IAAI;AACzB,wBAAA,IAAI,EAAE,EAAE;AACT,wBAAA,KAAK,EAAC,oCAAO;;;AACd,kBAAAC,kBAAA,CAAuC,MAAvC,EAAA,UAAuC,EAAAC,eAAA,CAAA,QAAA,CAAA,cAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;AACvC,kBAAAD,kBAAA,CAOM,OAPN,UAOM,EAAA;oBANLE,WAKkC,CAAA,wBAAA,EAAA;AAJhC,sBAAA,gBAAgB,EAAE,MAAc,CAAA,gBAAA;AAChC,sBAAA,QAAQ,EAAE,QAAa,CAAA,gBAAA;sBACvB,KAAK,EAAE,QAAc,CAAA,cAAA,CAAC,KAAK;AAC3B,sBAAA,OAAM,YAAA,QAAA,CAAA,gBAAA,CAAA,CAAA;AACP,sBAAA,UAAU,EAAC,uBAAsB;;;;AAGpC,gBAAAF,kBAAA,QAAA;AAEQ,kBAAA,CAAA,CAAA,QAAA,CAAA,WAAA,CAAA,QAAA,CAAA,cAAA,CAAA;oCADPD;;AAEC,wBAAA,QAAA,EAAA,QAAA,CAAA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnNcWidgetWidget.vue2.js';
|
|
|
2
2
|
import { render } from './CnNcWidgetWidget.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-24a18190";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -2,6 +2,9 @@ import axios__default from '@nextcloud/axios';
|
|
|
2
2
|
import { translate } from '@nextcloud/l10n';
|
|
3
3
|
import { generateOcsUrl } from '@nextcloud/router';
|
|
4
4
|
|
|
5
|
+
/** The tones `content.itemTones` may give an item. */
|
|
6
|
+
const ITEM_TONES = ['success', 'info', 'neutral', 'warning', 'error'];
|
|
7
|
+
|
|
5
8
|
const API_ITEM_LIMIT = 7;
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -46,7 +49,7 @@ var script = {
|
|
|
46
49
|
|
|
47
50
|
props: {
|
|
48
51
|
/**
|
|
49
|
-
* Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?}`.
|
|
52
|
+
* Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?, itemTones?}`.
|
|
50
53
|
* `excludeApps` (`["systemtags"]`) and `excludeTypes` leave out the
|
|
51
54
|
* items of those apps or types (an item's own `app` / `type`, else the
|
|
52
55
|
* app in its link or icon URL); the list then comes from the items API.
|
|
@@ -274,18 +277,54 @@ var script = {
|
|
|
274
277
|
return false
|
|
275
278
|
}
|
|
276
279
|
const { apps, types } = this.exclusions;
|
|
280
|
+
const kinds = this.kindsOf(item);
|
|
281
|
+
if (apps.length > 0 && kinds.apps.some((app) => apps.includes(app))) {
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
return types.length > 0 && kinds.types.some((type) => types.includes(type))
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* What an item is, lower case: its own `type` / `objectType`, and its
|
|
289
|
+
* own `app` or the app in an `/apps/<app>/` part of its icons or link.
|
|
290
|
+
*
|
|
291
|
+
* @param {object} item A widget item.
|
|
292
|
+
* @return {{apps: string[], types: string[]}}
|
|
293
|
+
*/
|
|
294
|
+
kindsOf(item) {
|
|
277
295
|
const own = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
|
|
278
|
-
|
|
279
|
-
|
|
296
|
+
// The icons first: the activity app sets them from the activity's own app.
|
|
297
|
+
const apps = [own(item.app)];
|
|
298
|
+
for (const url of [item.iconUrl, item.overlayIconUrl, item.link]) {
|
|
280
299
|
const match = typeof url === 'string' ? url.match(/\/apps\/([a-z0-9_-]+)\//i) : null;
|
|
281
300
|
if (match) {
|
|
282
|
-
|
|
301
|
+
apps.push(match[1].toLowerCase());
|
|
283
302
|
}
|
|
284
303
|
}
|
|
285
|
-
|
|
286
|
-
|
|
304
|
+
return { apps: apps.filter((app) => app !== ''), types: [own(item.type), own(item.objectType)].filter((type) => type !== '') }
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The item's class for its tone (`content.itemTones`), keyed by its
|
|
309
|
+
* type first, then its app; '' without a match.
|
|
310
|
+
*
|
|
311
|
+
* @spec openspec/changes/nc-widget-item-tones/specs/nc-widget-item-tones/spec.md#requirement-a-nextcloud-widget-item-can-take-a-tone-from-its-app-or-type
|
|
312
|
+
* @param {object} item A widget item.
|
|
313
|
+
* @return {string}
|
|
314
|
+
*/
|
|
315
|
+
toneClass(item) {
|
|
316
|
+
const map = this.content && this.content.itemTones;
|
|
317
|
+
if (!map || typeof map !== 'object' || !item || typeof item !== 'object') {
|
|
318
|
+
return ''
|
|
319
|
+
}
|
|
320
|
+
const kinds = this.kindsOf(item);
|
|
321
|
+
for (const key of [...kinds.types, ...kinds.apps]) {
|
|
322
|
+
const tone = Object.keys(map).find((name) => name.toLowerCase() === key);
|
|
323
|
+
if (tone && ITEM_TONES.includes(map[tone])) {
|
|
324
|
+
return `cn-nc-widget-widget__item--tone-${map[tone]}`
|
|
325
|
+
}
|
|
287
326
|
}
|
|
288
|
-
return
|
|
327
|
+
return ''
|
|
289
328
|
},
|
|
290
329
|
|
|
291
330
|
t: translate,
|