@conduction/nextcloud-vue 2.61.0 → 2.62.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.
Files changed (61) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +128 -0
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  4. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  5. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  6. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  7. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  9. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  11. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +40 -15
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -2
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  22. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  23. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  24. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  25. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  31. package/dist/esm/l10n/en.json.js +5 -0
  32. package/dist/esm/l10n/en.json.js.map +1 -1
  33. package/dist/esm/l10n/nl.json.js +5 -0
  34. package/dist/esm/l10n/nl.json.js.map +1 -1
  35. package/dist/esm/nextcloud-vue.css +108 -9
  36. package/dist/esm/utils/validateManifestV2.compiled.js +1 -1
  37. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  38. package/dist/nextcloud-vue.cjs.js +857 -490
  39. package/dist/nextcloud-vue.cjs.js.map +2 -2
  40. package/dist/nextcloud-vue.css +108 -9
  41. package/l10n/en.json +5 -0
  42. package/l10n/nl.json +5 -0
  43. package/package.json +3 -2
  44. package/scripts/manifest-schema-hash.js +41 -0
  45. package/scripts/record-manifest-schema-hash.js +52 -0
  46. package/src/components/CnAppNav/CnAppNav.vue +128 -0
  47. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  48. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  49. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  50. package/src/components/CnDashboardPage/CnDashboardPage.vue +30 -6
  51. package/src/components/CnDataTable/CnDataTable.md +1 -1
  52. package/src/components/CnDataTable/CnDataTable.vue +42 -17
  53. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  54. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  55. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  56. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  57. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  58. package/src/css/table.css +1 -2
  59. package/src/public/index.js +5 -0
  60. package/src/schemas/app-manifest-v2.schema.json +1 -1
  61. package/src/utils/validateManifestV2.compiled.js +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CnReportsPage.vue2.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div v-if=\"categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tinject: {\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"CnReportsPage.vue2.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div v-if=\"categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tinject: {\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnReportsPage.vue3.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue?vue&type=template&id=41525752&lang.js"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div v-if=\"categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tinject: {\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString"],"mappings":";;AAmEK,MAAA,UAAA,GAAA,EAAA,KAAM,EAAC,iBAAiB,GAAA;AACtB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAsB,GAAA;AAC5B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAmB,GAAA;;;AAGC,EAAA,KAAK,EAAC,8BAA8B;;;;AAKxC,EAAA,KAAA,EAAA,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AAV1B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;AACC,IAAAC,kBAAA,CAOI,OAPJ,UAOI,EAAA;wBAJA,CAAA,IAAA,YAAA,EAAAC,eAAA,CADD,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;OAER,QAAmB,CAAA,mBAAA;0CAExB,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAmB,CAAA,mBAAA,CAAA,EAAA,CAAA,YAAA;;;AAIb,IAAA,CAAA,QAAA,CAAA,eAAe,CAAC,MAAA,GAAA,CAAA;AAAvB,SAAAH,SAAA,EAAA,EAAAC,kBAAA,QAAA;UACDC,kBAEE,CAAA,OAAA,EAAA;AAFG,YAAA,KAAC,EAAA,+BAAA;AAAA,YAAA,GAAA,EAAA,QAAA,CAAA,QAAA;6BACJ,QAAA,CAAA,qBAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;yBAEJA;AAAG,YAAA,EAAA,EAAA,QAAA,CAAA,QAAA;yEACA,KAAA,CAAA,cAAA,IAAA,MAAA,CAAA,CAAA;;;;;;;;;;;2BAAA,KAAA,CAAA,cAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnReportsPage.vue3.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue?vue&type=template&id=41525752&lang.js"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div v-if=\"categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tinject: {\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString"],"mappings":";;AAmEK,MAAA,UAAA,GAAA,EAAA,KAAM,EAAC,iBAAiB,GAAA;AACtB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAsB,GAAA;AAC5B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAmB,GAAA;;;AAGC,EAAA,KAAK,EAAC,8BAA8B;;;;AAKxC,EAAA,KAAA,EAAA,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AAV1B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;AACC,IAAAC,kBAAA,CAOI,OAPJ,UAOI,EAAA;wBAJA,CAAA,IAAA,YAAA,EAAAC,eAAA,CADD,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;OAER,QAAmB,CAAA,mBAAA;0CAExB,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAmB,CAAA,mBAAA,CAAA,EAAA,CAAA,YAAA;;;AAIb,IAAA,CAAA,QAAA,CAAA,eAAe,CAAC,MAAA,GAAA,CAAA;AAAvB,SAAAH,SAAA,EAAA,EAAAC,kBAAA,QAAA;UACDC,kBAEE,CAAA,OAAA,EAAA;AAFG,YAAA,KAAC,EAAA,+BAAA;AAAA,YAAA,GAAA,EAAA,QAAA,CAAA,QAAA;6BACJ,QAAA,CAAA,qBAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;yBAEJA;AAAG,YAAA,EAAA,EAAA,QAAA,CAAA,QAAA;yEACA,KAAA,CAAA,cAAA,IAAA,MAAA,CAAA,CAAA;;;;;;;;;;;2BAAA,KAAA,CAAA,cAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnStorePage.vue2.js","sources":["../../../../src/components/CnStorePage/CnStorePage.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n SPDX-License-Identifier: EUPL-1.2\n\n CnStorePage — browse a remote OpenRegister registry for shareable content,\n and install an item into this instance.\n\n The `store` page type (ADR-080, ADR-114 Decision 4). Promoted out of dossiq's\n app-local StoreGallery, which was the fleet's only implementation and was\n about to be copied fifteen times (ADR-012).\n\n WHAT IS APP-SPECIFIC, AND WHAT IS NOT.\n --------------------------------------\n Everything on this page is generic except three things, and all three arrive\n as props rather than being written into the component:\n\n `app` the app id, which is the only thing in the request URL\n `builtIn` the items this app ships itself, for the no-registry case\n `description` the lead paragraph, because a store of case types and a\n store of connector adapters are not selling the same thing\n\n DISCOVERY IS NOT IMPLEMENTED HERE, ON PURPOSE.\n ----------------------------------------------\n This calls `/apps/<app>/api/store/items`, which is a thin action over\n OpenRegister's `GenericStoreService` (ADR-080 Decision 2). The SSRF guard,\n the redirect refusal, the timeouts and the registry token all stay\n server-side in the engine, and no leaf app ever builds a registry URL.\n\n Install is deliberately NOT generalised (ADR-080 Decision 3): cloning an\n application template, enabling a connector adapter and instantiating an agent\n template are different operations with different authorization. Each app owns\n `POST /api/store/items/{slug}/install`; this component only calls it and\n renders what it reports.\n\n WHO SEES INSTALL AND PUBLISH IS THE APP'S ANSWER, NOT THIS PAGE'S.\n -----------------------------------------------------------------\n `canInstall` and `canPublish` carry a boolean the app resolved (learniq reads\n its ADR-023 action matrix). `null`, the default, falls back to administrators\n only, which is what every store page did before the props existed. Publish\n appears only when the app also names a `publishRoute`: publishing is the\n app's own surface, like install. Both props hide a button and nothing more;\n the app's endpoints enforce the same rule.\n\n A LOCAL-ONLY CARD GRID IS NOT A STORE (ADR-080 Decision 4). With no registry\n configured the engine answers `not_configured` WITHOUT a network call, and\n this page then renders `builtIn`. That fallback is the only reason a surface\n may carry the word Store at all.\n-->\n<template>\n\t<div class=\"cn-store-page\" data-testid=\"store-page\">\n\t\t<div class=\"cn-store-page__header\">\n\t\t\t<div class=\"cn-store-page__heading\">\n\t\t\t\t<h2 class=\"cn-store-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"showPublish\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tdata-testid=\"store-publish\"\n\t\t\t\t\t:to=\"publishTarget\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Publish') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-store-page__intro\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div class=\"cn-store-page__controls\">\n\t\t\t<NcTextField\n\t\t\t\tv-model=\"query\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Search the store')\"\n\t\t\t\t:disabled=\"offline\"\n\t\t\t\tdata-testid=\"store-search\"\n\t\t\t\t@update:modelValue=\"onSearchInput\" />\n\n\t\t\t<div\n\t\t\t\tclass=\"cn-store-page__kinds\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Filter by kind')\">\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"option in kindOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:variant=\"option.value === kind ? 'primary' : 'secondary'\"\n\t\t\t\t\t:disabled=\"offline\"\n\t\t\t\t\t@click=\"selectKind(option.value)\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"44\" class=\"cn-store-page__loading\" />\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"offline\"\n\t\t\ttype=\"info\"\n\t\t\tdata-testid=\"store-not-configured\">\n\t\t\t{{ notConfiguredMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"unreachable\"\n\t\t\ttype=\"warning\"\n\t\t\tdata-testid=\"store-unreachable\">\n\t\t\t{{ unreachableMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"cards.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'Nothing matches that search')\"\n\t\t\t:description=\"\n\t\t\t\tt('nextcloud-vue', 'Try a different term, or clear the kind filter.')\n\t\t\t\" />\n\n\t\t<ul\n\t\t\tv-if=\"!loading && cards.length > 0\"\n\t\t\tclass=\"cn-store-page__grid\"\n\t\t\tdata-testid=\"store-results\">\n\t\t\t<li v-for=\"card in cards\" :key=\"card.slug\" class=\"cn-store-page__card\">\n\t\t\t\t<h3 class=\"cn-store-page__card-title\">\n\t\t\t\t\t{{ card.title || card.slug }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"card.typeName || card.kind\" class=\"cn-store-page__card-kind\">\n\t\t\t\t\t{{ card.typeName || card.kind }}\n\t\t\t\t</p>\n\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t{{ card.description }}\n\t\t\t\t</p>\n\t\t\t\t<div class=\"cn-store-page__card-footer\">\n\t\t\t\t\t<span v-if=\"card.publisher\" class=\"cn-store-page__card-publisher\">\n\t\t\t\t\t\t{{ card.publisher }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"card.version\" class=\"cn-store-page__card-version\">\n\t\t\t\t\t\t{{ card.version }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"showInstall\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"store-install\"\n\t\t\t\t\t\t:disabled=\"installing === card.slug\"\n\t\t\t\t\t\t@click=\"install(card)\">\n\t\t\t\t\t\t{{\n\t\t\t\t\t\t\tinstalling === card.slug\n\t\t\t\t\t\t\t\t? t('nextcloud-vue', 'Installing…')\n\t\t\t\t\t\t\t\t: t('nextcloud-vue', 'Install')\n\t\t\t\t\t\t}}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<div\n\t\t\tv-if=\"!loading && visibleBuiltIn.length > 0\"\n\t\t\tclass=\"cn-store-page__builtin\">\n\t\t\t<h3 class=\"cn-store-page__builtin-title\">\n\t\t\t\t{{ builtInHeading }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-store-page__grid\" data-testid=\"store-builtin\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"item in visibleBuiltIn\"\n\t\t\t\t\t:key=\"item.slug\"\n\t\t\t\t\tclass=\"cn-store-page__card\">\n\t\t\t\t\t<h4 class=\"cn-store-page__card-title\">\n\t\t\t\t\t\t{{ item.title || item.slug }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t\t{{ item.description }}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\n\t\t<NcNoteCard\n\t\t\tv-if=\"report\"\n\t\t\t:type=\"report.type\"\n\t\t\tdata-testid=\"store-install-report\">\n\t\t\t{{ report.message }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { showError, showSuccess } from '@nextcloud/dialogs'\nimport { t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport {\n\tNcButton,\n\tNcEmptyContent,\n\tNcLoadingIcon,\n\tNcNoteCard,\n\tNcTextField,\n} from '@nextcloud/vue'\n\n/**\n * The kind vocabulary from ADR-080 Decision 5. A `kind` names what installing\n * an item DOES, which is what lets one registry serve several apps from one\n * schema. An app whose items are a different shape passes its own `kinds`.\n */\nexport const DEFAULT_STORE_KINDS = [\n\t'app-template',\n\t'adapter',\n\t'source-template',\n\t'configuration-template',\n\t'agent-template',\n]\n\nexport default {\n\tname: 'CnStorePage',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tNcTextField,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"store\"` page arrives with the flattened\n\t\t * props below and this default `{}`. Reading `page.config` here would\n\t\t * render an empty store forever, which is exactly what happened to\n\t\t * CnReportsPage before #897. Kept only so a host rendering this\n\t\t * component directly can still hand it a whole page; the flattened\n\t\t * props win.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The app id. The ONLY app-specific value in the request URL, and the\n\t\t * reason this component is shared rather than copied.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tapp: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Page title, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph. A store of case types and a store of connector\n\t\t * adapters are not offering the same thing, so the app says what its\n\t\t * store is for.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Kind quick-filters. Defaults to the ADR-080 Decision 5 vocabulary.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tkinds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => DEFAULT_STORE_KINDS,\n\t\t},\n\n\t\t/**\n\t\t * The items this app ships itself, rendered when the engine reports\n\t\t * `not_configured` or the registry does not answer.\n\t\t *\n\t\t * ADR-080 Decision 4 makes this the price of the word Store: a surface\n\t\t * that goes blank without a registry was never a store, and a surface\n\t\t * that only ever shows these is a Templates page.\n\t\t *\n\t\t * @type {Array<{slug: string, title: string, description: string}>}\n\t\t */\n\t\tbuiltIn: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Install, as the app resolved it\n\t\t * (learniq: its ADR-023 matrix). `true` shows it on every remote\n\t\t * card, `false` hides it from everyone, administrators included.\n\t\t * `null` (the default) keeps the rule from before this prop:\n\t\t * administrators only. Presentation only: the app's install endpoint\n\t\t * must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanInstall: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Publish, as the app resolved it.\n\t\t * `null` (the default) falls back to administrators only. Publish\n\t\t * renders only when `publishRoute` is set as well. Presentation\n\t\t * only: the app's publish endpoint must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanPublish: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where Publish takes the user: a route name (string) or a route\n\t\t * location (object). Publishing is the app's own surface, so this\n\t\t * page never sends anything to a registry itself. Empty means no\n\t\t * Publish button, whatever `canPublish` says, so an app that never\n\t\t * sets it renders what it rendered before.\n\t\t *\n\t\t * @type {string|object}\n\t\t */\n\t\tpublishRoute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\toutcome: null,\n\t\t\tcards: [],\n\t\t\tquery: '',\n\t\t\tkind: '',\n\t\t\tinstalling: null,\n\t\t\treport: null,\n\t\t\tsearchTimer: null,\n\t\t\t// The kinds the ENGINE served, from the app's `store` manifest\n\t\t\t// block. Null until the first response, so \"not asked yet\" stays\n\t\t\t// distinguishable from \"the app declares none\".\n\t\t\tservedKinds: null,\n\t\t\t// The built-in items the ENGINE served, from the app's `store`\n\t\t\t// manifest block. Null until the first response, so \"not asked\n\t\t\t// yet\" stays distinguishable from \"the app declares none\".\n\t\t\tservedBuiltIn: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The page title, preferring the flattened prop over the page object.\n\t\t *\n\t\t * @return {string} The heading to render.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title || this.page?.title || t('nextcloud-vue', 'Store')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The intro copy, or an empty string.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.description || this.page?.config?.description || ''\n\t\t},\n\n\t\t/**\n\t\t * The app id, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The app id used to address the store endpoints.\n\t\t */\n\t\tresolvedApp() {\n\t\t\treturn this.app || this.page?.config?.app || ''\n\t\t},\n\n\t\t/**\n\t\t * No registry configured. The engine reported this WITHOUT making a\n\t\t * network call, which is the ADR-080 Decision 4 fallback.\n\t\t *\n\t\t * @return {boolean} True when no registry is connected.\n\t\t */\n\t\toffline() {\n\t\t\treturn this.outcome === 'not_configured'\n\t\t},\n\n\t\t/**\n\t\t * The registry is configured but did not answer usefully.\n\t\t *\n\t\t * @return {boolean} True when the registry errored.\n\t\t */\n\t\tunreachable() {\n\t\t\treturn (\n\t\t\t\tthis.outcome === 'store_unreachable'\n\t\t\t\t|| this.outcome === 'store_invalid_response'\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user is an administrator: the fallback for both\n\t\t * actions when the app did not say.\n\t\t *\n\t\t * @return {boolean} True when the current user is an administrator.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tisAdmin() {\n\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t},\n\n\t\t/**\n\t\t * Whether Install shows. The app's answer wins; without one only an\n\t\t * administrator installs, because what an install writes is the shape\n\t\t * of the work every handler then operates against. The server\n\t\t * enforces this too; hiding the button keeps a user from discovering\n\t\t * a refusal as a 403.\n\t\t *\n\t\t * @return {boolean} True when the Install buttons render.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tshowInstall() {\n\t\t\tif (typeof this.canInstall === 'boolean') {\n\t\t\t\treturn this.canInstall\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * Where Publish navigates, or null when the app named no destination.\n\t\t * A string is a route NAME, the way manifest menu entries name routes.\n\t\t *\n\t\t * @return {object|null} A vue-router location, or null.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tpublishTarget() {\n\t\t\tif (typeof this.publishRoute === 'string') {\n\t\t\t\treturn this.publishRoute === '' ? null : { name: this.publishRoute }\n\t\t\t}\n\n\t\t\tif (this.publishRoute !== null && typeof this.publishRoute === 'object') {\n\t\t\t\treturn this.publishRoute\n\t\t\t}\n\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Whether Publish shows: a destination is named AND the app allows it\n\t\t * (or, without an answer, the user is an administrator). No\n\t\t * destination means no button, because a button that goes nowhere\n\t\t * is worse than none.\n\t\t *\n\t\t * @return {boolean} True when the Publish button renders.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tshowPublish() {\n\t\t\tif (this.publishTarget === null) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif (typeof this.canPublish === 'boolean') {\n\t\t\t\treturn this.canPublish\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * The kind quick-filters, with an \"all\" entry first.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} The filter options.\n\t\t */\n\t\tkindOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: '', label: t('nextcloud-vue', 'All kinds') },\n\t\t\t\t...this.effectiveKinds.map((value) => ({ value, label: value })),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The kinds to offer, preferring what the ENGINE served.\n\t\t *\n\t\t * An app declares its kinds in the `store` block of its manifest, next\n\t\t * to the schema allowlist, and the engine returns them with the cards.\n\t\t * That is the single place an app says what its store sells; the\n\t\t * `kinds` PROP remains for a host rendering this component directly and\n\t\t * as the pre-response value, and DEFAULT_STORE_KINDS is the floor.\n\t\t *\n\t\t * A served EMPTY list means the app declared none, which is not the\n\t\t * same as not having asked yet — hence `servedKinds` starts null.\n\t\t *\n\t\t * @return {Array<string>} The kind filter values.\n\t\t */\n\t\teffectiveKinds() {\n\t\t\tif (Array.isArray(this.servedKinds) && this.servedKinds.length > 0) {\n\t\t\t\treturn this.servedKinds\n\t\t\t}\n\n\t\t\treturn this.kinds\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, shown whenever the remote list is not carrying\n\t\t * the page. Not a network call, and not a Store on their own.\n\t\t *\n\t\t * 🔴 AN EMPTY ANSWER IS STILL AN EMPTY PAGE. This used to hide the\n\t\t * built-ins whenever the engine answered at all, checking only\n\t\t * `not_configured` and the error outcomes. A federated store that\n\t\t * answers `ok` with zero cards — a configured registry that has\n\t\t * published nothing yet — then rendered a heading, a search box and\n\t\t * nothing else, while the app shipped four items it could have shown.\n\t\t * Measured on decidiq, whose store returns `ok` with 0 cards and 4\n\t\t * built-ins declared.\n\t\t *\n\t\t * ADR-080 Decision 4 is explicit that a surface which goes blank was\n\t\t * never a store, so the test is what the reader can SEE, not which\n\t\t * outcome the engine reported.\n\t\t *\n\t\t * @return {Array<object>} The built-in items to render.\n\t\t */\n\t\tvisibleBuiltIn() {\n\t\t\tif (this.offline === false && this.unreachable === false && this.cards.length > 0) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn this.effectiveBuiltIn\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, preferring what the ENGINE served over the page\n\t\t * config.\n\t\t *\n\t\t * An app declares these ONCE, in its `store` manifest block beside the\n\t\t * allowlist, which is where the schema documents them. The `builtIn`\n\t\t * PROP is a second place to write the same list, and an app that\n\t\t * declared them only in the manifest used to render nothing: the\n\t\t * engine parsed them and never sent them.\n\t\t *\n\t\t * Mirrors `effectiveKinds` exactly, for the same reason.\n\t\t *\n\t\t * @return {Array<object>} The items to render.\n\t\t */\n\t\teffectiveBuiltIn() {\n\t\t\tif (Array.isArray(this.servedBuiltIn) && this.servedBuiltIn.length > 0) {\n\t\t\t\treturn this.servedBuiltIn\n\t\t\t}\n\n\t\t\treturn this.builtIn\n\t\t},\n\n\t\t/**\n\t\t * Heading above the app's own items.\n\t\t *\n\t\t * @return {string} The built-in section heading.\n\t\t */\n\t\tbuiltInHeading() {\n\t\t\treturn this.resolvedApp\n\t\t\t\t? t('nextcloud-vue', 'Included with {app}', { app: this.resolvedApp })\n\t\t\t\t: t('nextcloud-vue', 'Included with this app')\n\t\t},\n\n\t\t/**\n\t\t * The no-registry message, which names the built-in fallback only when\n\t\t * there is one. Promising templates below when the list is empty is\n\t\t * how an empty page reads as a broken one.\n\t\t *\n\t\t * @return {string} The not-configured note.\n\t\t */\n\t\tnotConfiguredMessage() {\n\t\t\tconst base = t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'No store registry is configured, so nothing was requested from the network. An administrator can connect one under Administration settings.',\n\t\t\t)\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\n\t\t/**\n\t\t * The registry-error message, on the same rule as above.\n\t\t *\n\t\t * @return {string} The unreachable note.\n\t\t */\n\t\tunreachableMessage() {\n\t\t\tconst base = t('nextcloud-vue', 'The store registry did not answer.')\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.search()\n\t},\n\n\t/**\n\t * Cancel a pending debounced search.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\tif (this.searchTimer !== null) {\n\t\t\tclearTimeout(this.searchTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Debounce the search so typing does not fire one remote request per\n\t\t * keystroke against somebody else's registry.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonSearchInput() {\n\t\t\tif (this.searchTimer !== null) {\n\t\t\t\tclearTimeout(this.searchTimer)\n\t\t\t}\n\n\t\t\tthis.searchTimer = setTimeout(() => {\n\t\t\t\tthis.search()\n\t\t\t}, 400)\n\t\t},\n\n\t\t/**\n\t\t * Select a kind filter and re-search.\n\t\t *\n\t\t * @param {string} value The kind, or an empty string for all kinds.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tselectKind(value) {\n\t\t\tthis.kind = value\n\t\t\tthis.search()\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current page of store cards.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search() {\n\t\t\t// An unconfigured `app` cannot address any endpoint. Reporting\n\t\t\t// not_configured is the honest outcome and costs no request; the\n\t\t\t// alternative is a fetch to /apps//api/... which 404s and reads to\n\t\t\t// the user as a registry that is down.\n\t\t\tif (this.resolvedApp === '') {\n\t\t\t\tthis.outcome = 'not_configured'\n\t\t\t\tthis.cards = []\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams()\n\t\t\t\tif (this.query) {\n\t\t\t\t\tparams.set('q', this.query)\n\t\t\t\t}\n\t\t\t\tif (this.kind) {\n\t\t\t\t\tparams.set('kind', this.kind)\n\t\t\t\t}\n\n\t\t\t\tconst suffix = params.toString() ? `?${params.toString()}` : ''\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items${suffix}`),\n\t\t\t\t\t{\n\t\t\t\t\t\theaders: { requesttoken: window.OC?.requestToken },\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tthis.outcome = body.outcome ?? 'store_invalid_response'\n\t\t\t\tthis.cards = Array.isArray(body.cards) ? body.cards : []\n\t\t\t\t// Present on every arm the engine answers, including\n\t\t\t\t// not_configured and store_unreachable, so the filters survive\n\t\t\t\t// a registry that is down.\n\t\t\t\tthis.servedKinds = Array.isArray(body.kinds) ? body.kinds : null\n\t\t\t\t// Same contract as `kinds`: present on every arm the engine\n\t\t\t\t// answers, so an app's own items survive a registry that is\n\t\t\t\t// down — which is exactly when they are all there is to show.\n\t\t\t\tthis.servedBuiltIn = Array.isArray(body.builtIn) ? body.builtIn : null\n\t\t\t} catch {\n\t\t\t\tthis.outcome = 'store_unreachable'\n\t\t\t\tthis.cards = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Install one item, and report per component.\n\t\t *\n\t\t * A partial install is a real outcome rather than a failure: an item\n\t\t * carrying both configuration and records installs the configuration\n\t\t * and names the records it refused.\n\t\t *\n\t\t * @param {{slug: string, title: string}} card The card to install.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync install(card) {\n\t\t\tthis.installing = card.slug\n\t\t\tthis.report = null\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items/${encodeURIComponent(card.slug)}/install`),\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\trequesttoken: window.OC?.requestToken,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tif (response.ok !== true) {\n\t\t\t\t\tshowError(body.message\n\t\t\t\t\t\t?? t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst refused = (body.components ?? []).filter((c) => c.status !== 'installed')\n\t\t\t\tif (refused.length === 0) {\n\t\t\t\t\tshowSuccess(t('nextcloud-vue', 'Installed.'))\n\t\t\t\t\tthis.report = null\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.report = {\n\t\t\t\t\ttype: body.success === true ? 'info' : 'warning',\n\t\t\t\t\tmessage: t(\n\t\t\t\t\t\t'nextcloud-vue',\n\t\t\t\t\t\t'Some parts were not installed: {schemas}',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschemas: refused.map((c) => c.schema).join(', '),\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tshowError(t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t} finally {\n\t\t\t\tthis.installing = null\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-store-page {\n\tpadding: 16px;\n\tmax-width: 1200px;\n}\n\n.cn-store-page__heading {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 0 0 4px;\n}\n\n.cn-store-page__title {\n\tmargin: 0;\n}\n\n.cn-store-page__intro {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0 0 16px;\n}\n\n.cn-store-page__controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 12px;\n\talign-items: flex-end;\n\tmargin-bottom: 16px;\n}\n\n.cn-store-page__kinds {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n.cn-store-page__loading {\n\tmargin: 32px auto;\n}\n\n.cn-store-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n\tgap: 16px;\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 16px 0 0;\n}\n\n.cn-store-page__card {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-main-background);\n\tpadding: 16px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-store-page__card-title {\n\tmargin: 0;\n\tfont-size: 1rem;\n}\n\n.cn-store-page__card-kind {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n.cn-store-page__card-description {\n\tmargin: 0;\n\tflex: 1;\n}\n\n.cn-store-page__card-footer {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-store-page__card-version {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n/* Who published a configuration set matters more than its version does, since\n installing one applies somebody else's schemas and flows to this instance. */\n.cn-store-page__card-publisher {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n\tmargin-inline-end: auto;\n}\n\n.cn-store-page__builtin-title {\n\tmargin: 24px 0 0;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnStorePage.vue2.js","sources":["../../../../src/components/CnStorePage/CnStorePage.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n SPDX-License-Identifier: EUPL-1.2\n\n CnStorePage — browse a remote OpenRegister registry for shareable content,\n and install an item into this instance.\n\n The `store` page type (ADR-080, ADR-114 Decision 4). Promoted out of dossiq's\n app-local StoreGallery, which was the fleet's only implementation and was\n about to be copied fifteen times (ADR-012).\n\n WHAT IS APP-SPECIFIC, AND WHAT IS NOT.\n --------------------------------------\n Everything on this page is generic except three things, and all three arrive\n as props rather than being written into the component:\n\n `app` the app id, which is the only thing in the request URL\n `builtIn` the items this app ships itself, for the no-registry case\n `description` the lead paragraph, because a store of case types and a\n store of connector adapters are not selling the same thing\n\n DISCOVERY IS NOT IMPLEMENTED HERE, ON PURPOSE.\n ----------------------------------------------\n This calls `/apps/<app>/api/store/items`, which is a thin action over\n OpenRegister's `GenericStoreService` (ADR-080 Decision 2). The SSRF guard,\n the redirect refusal, the timeouts and the registry token all stay\n server-side in the engine, and no leaf app ever builds a registry URL.\n\n Install is deliberately NOT generalised (ADR-080 Decision 3): cloning an\n application template, enabling a connector adapter and instantiating an agent\n template are different operations with different authorization. Each app owns\n `POST /api/store/items/{slug}/install`; this component only calls it and\n renders what it reports.\n\n WHO SEES INSTALL AND PUBLISH IS THE APP'S ANSWER, NOT THIS PAGE'S.\n -----------------------------------------------------------------\n `canInstall` and `canPublish` carry a boolean the app resolved (learniq reads\n its ADR-023 action matrix). `null`, the default, falls back to administrators\n only, which is what every store page did before the props existed. Publish\n appears only when the app also names a `publishRoute`: publishing is the\n app's own surface, like install. Both props hide a button and nothing more;\n the app's endpoints enforce the same rule.\n\n A LOCAL-ONLY CARD GRID IS NOT A STORE (ADR-080 Decision 4). With no registry\n configured the engine answers `not_configured` WITHOUT a network call, and\n this page then renders `builtIn`. That fallback is the only reason a surface\n may carry the word Store at all.\n-->\n<template>\n\t<div class=\"cn-store-page\" data-testid=\"store-page\">\n\t\t<div class=\"cn-store-page__header\">\n\t\t\t<div class=\"cn-store-page__heading\">\n\t\t\t\t<h2 class=\"cn-store-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"showPublish\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tdata-testid=\"store-publish\"\n\t\t\t\t\t:to=\"publishTarget\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Publish') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-store-page__intro\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div class=\"cn-store-page__controls\">\n\t\t\t<NcTextField\n\t\t\t\tv-model=\"query\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Search the store')\"\n\t\t\t\t:disabled=\"offline\"\n\t\t\t\tdata-testid=\"store-search\"\n\t\t\t\t@update:modelValue=\"onSearchInput\" />\n\n\t\t\t<div\n\t\t\t\tclass=\"cn-store-page__kinds\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Filter by kind')\">\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"option in kindOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:variant=\"option.value === kind ? 'primary' : 'secondary'\"\n\t\t\t\t\t:disabled=\"offline\"\n\t\t\t\t\t@click=\"selectKind(option.value)\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"44\" class=\"cn-store-page__loading\" />\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"offline\"\n\t\t\ttype=\"info\"\n\t\t\tdata-testid=\"store-not-configured\">\n\t\t\t{{ notConfiguredMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"unreachable\"\n\t\t\ttype=\"warning\"\n\t\t\tdata-testid=\"store-unreachable\">\n\t\t\t{{ unreachableMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"cards.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'Nothing matches that search')\"\n\t\t\t:description=\"\n\t\t\t\tt('nextcloud-vue', 'Try a different term, or clear the kind filter.')\n\t\t\t\" />\n\n\t\t<ul\n\t\t\tv-if=\"!loading && cards.length > 0\"\n\t\t\tclass=\"cn-store-page__grid\"\n\t\t\tdata-testid=\"store-results\">\n\t\t\t<li v-for=\"card in cards\" :key=\"card.slug\" class=\"cn-store-page__card\">\n\t\t\t\t<h3 class=\"cn-store-page__card-title\">\n\t\t\t\t\t{{ card.title || card.slug }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"card.typeName || card.kind\" class=\"cn-store-page__card-kind\">\n\t\t\t\t\t{{ card.typeName || card.kind }}\n\t\t\t\t</p>\n\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t{{ card.description }}\n\t\t\t\t</p>\n\t\t\t\t<div class=\"cn-store-page__card-footer\">\n\t\t\t\t\t<span v-if=\"card.publisher\" class=\"cn-store-page__card-publisher\">\n\t\t\t\t\t\t{{ card.publisher }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"card.version\" class=\"cn-store-page__card-version\">\n\t\t\t\t\t\t{{ card.version }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"showInstall\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"store-install\"\n\t\t\t\t\t\t:disabled=\"installing === card.slug\"\n\t\t\t\t\t\t@click=\"install(card)\">\n\t\t\t\t\t\t{{\n\t\t\t\t\t\t\tinstalling === card.slug\n\t\t\t\t\t\t\t\t? t('nextcloud-vue', 'Installing…')\n\t\t\t\t\t\t\t\t: t('nextcloud-vue', 'Install')\n\t\t\t\t\t\t}}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<div\n\t\t\tv-if=\"!loading && visibleBuiltIn.length > 0\"\n\t\t\tclass=\"cn-store-page__builtin\">\n\t\t\t<h3 class=\"cn-store-page__builtin-title\">\n\t\t\t\t{{ builtInHeading }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-store-page__grid\" data-testid=\"store-builtin\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"item in visibleBuiltIn\"\n\t\t\t\t\t:key=\"item.slug\"\n\t\t\t\t\tclass=\"cn-store-page__card\">\n\t\t\t\t\t<h4 class=\"cn-store-page__card-title\">\n\t\t\t\t\t\t{{ item.title || item.slug }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t\t{{ item.description }}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\n\t\t<NcNoteCard\n\t\t\tv-if=\"report\"\n\t\t\t:type=\"report.type\"\n\t\t\tdata-testid=\"store-install-report\">\n\t\t\t{{ report.message }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { showError, showSuccess } from '@nextcloud/dialogs'\nimport { t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport {\n\tNcButton,\n\tNcEmptyContent,\n\tNcLoadingIcon,\n\tNcNoteCard,\n\tNcTextField,\n} from '@nextcloud/vue'\n\n/**\n * The kind vocabulary from ADR-080 Decision 5. A `kind` names what installing\n * an item DOES, which is what lets one registry serve several apps from one\n * schema. An app whose items are a different shape passes its own `kinds`.\n */\nexport const DEFAULT_STORE_KINDS = [\n\t'app-template',\n\t'adapter',\n\t'source-template',\n\t'configuration-template',\n\t'agent-template',\n]\n\nexport default {\n\tname: 'CnStorePage',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tNcTextField,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"store\"` page arrives with the flattened\n\t\t * props below and this default `{}`. Reading `page.config` here would\n\t\t * render an empty store forever, which is exactly what happened to\n\t\t * CnReportsPage before #897. Kept only so a host rendering this\n\t\t * component directly can still hand it a whole page; the flattened\n\t\t * props win.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The app id. The ONLY app-specific value in the request URL, and the\n\t\t * reason this component is shared rather than copied.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tapp: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Page title, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph. A store of case types and a store of connector\n\t\t * adapters are not offering the same thing, so the app says what its\n\t\t * store is for.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Kind quick-filters. Defaults to the ADR-080 Decision 5 vocabulary.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tkinds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => DEFAULT_STORE_KINDS,\n\t\t},\n\n\t\t/**\n\t\t * The items this app ships itself, rendered when the engine reports\n\t\t * `not_configured` or the registry does not answer.\n\t\t *\n\t\t * ADR-080 Decision 4 makes this the price of the word Store: a surface\n\t\t * that goes blank without a registry was never a store, and a surface\n\t\t * that only ever shows these is a Templates page.\n\t\t *\n\t\t * @type {Array<{slug: string, title: string, description: string}>}\n\t\t */\n\t\tbuiltIn: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Install, as the app resolved it\n\t\t * (learniq: its ADR-023 matrix). `true` shows it on every remote\n\t\t * card, `false` hides it from everyone, administrators included.\n\t\t * `null` (the default) keeps the rule from before this prop:\n\t\t * administrators only. Presentation only: the app's install endpoint\n\t\t * must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanInstall: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Publish, as the app resolved it.\n\t\t * `null` (the default) falls back to administrators only. Publish\n\t\t * renders only when `publishRoute` is set as well. Presentation\n\t\t * only: the app's publish endpoint must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanPublish: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where Publish takes the user: a route name (string) or a route\n\t\t * location (object). Publishing is the app's own surface, so this\n\t\t * page never sends anything to a registry itself. Empty means no\n\t\t * Publish button, whatever `canPublish` says, so an app that never\n\t\t * sets it renders what it rendered before.\n\t\t *\n\t\t * @type {string|object}\n\t\t */\n\t\tpublishRoute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\toutcome: null,\n\t\t\tcards: [],\n\t\t\tquery: '',\n\t\t\tkind: '',\n\t\t\tinstalling: null,\n\t\t\treport: null,\n\t\t\tsearchTimer: null,\n\t\t\t// The kinds the ENGINE served, from the app's `store` manifest\n\t\t\t// block. Null until the first response, so \"not asked yet\" stays\n\t\t\t// distinguishable from \"the app declares none\".\n\t\t\tservedKinds: null,\n\t\t\t// The built-in items the ENGINE served, from the app's `store`\n\t\t\t// manifest block. Null until the first response, so \"not asked\n\t\t\t// yet\" stays distinguishable from \"the app declares none\".\n\t\t\tservedBuiltIn: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The page title, preferring the flattened prop over the page object.\n\t\t *\n\t\t * @return {string} The heading to render.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title || this.page?.title || t('nextcloud-vue', 'Store')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The intro copy, or an empty string.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.description || this.page?.config?.description || ''\n\t\t},\n\n\t\t/**\n\t\t * The app id, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The app id used to address the store endpoints.\n\t\t */\n\t\tresolvedApp() {\n\t\t\treturn this.app || this.page?.config?.app || ''\n\t\t},\n\n\t\t/**\n\t\t * No registry configured. The engine reported this WITHOUT making a\n\t\t * network call, which is the ADR-080 Decision 4 fallback.\n\t\t *\n\t\t * @return {boolean} True when no registry is connected.\n\t\t */\n\t\toffline() {\n\t\t\treturn this.outcome === 'not_configured'\n\t\t},\n\n\t\t/**\n\t\t * The registry is configured but did not answer usefully.\n\t\t *\n\t\t * @return {boolean} True when the registry errored.\n\t\t */\n\t\tunreachable() {\n\t\t\treturn (\n\t\t\t\tthis.outcome === 'store_unreachable'\n\t\t\t\t|| this.outcome === 'store_invalid_response'\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user is an administrator: the fallback for both\n\t\t * actions when the app did not say.\n\t\t *\n\t\t * @return {boolean} True when the current user is an administrator.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tisAdmin() {\n\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t},\n\n\t\t/**\n\t\t * Whether Install shows. The app's answer wins; without one only an\n\t\t * administrator installs, because what an install writes is the shape\n\t\t * of the work every handler then operates against. The server\n\t\t * enforces this too; hiding the button keeps a user from discovering\n\t\t * a refusal as a 403.\n\t\t *\n\t\t * @return {boolean} True when the Install buttons render.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tshowInstall() {\n\t\t\tif (typeof this.canInstall === 'boolean') {\n\t\t\t\treturn this.canInstall\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * Where Publish navigates, or null when the app named no destination.\n\t\t * A string is a route NAME, the way manifest menu entries name routes.\n\t\t *\n\t\t * @return {object|null} A vue-router location, or null.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tpublishTarget() {\n\t\t\tif (typeof this.publishRoute === 'string') {\n\t\t\t\treturn this.publishRoute === '' ? null : { name: this.publishRoute }\n\t\t\t}\n\n\t\t\tif (this.publishRoute !== null && typeof this.publishRoute === 'object') {\n\t\t\t\treturn this.publishRoute\n\t\t\t}\n\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Whether Publish shows: a destination is named AND the app allows it\n\t\t * (or, without an answer, the user is an administrator). No\n\t\t * destination means no button, because a button that goes nowhere\n\t\t * is worse than none.\n\t\t *\n\t\t * @return {boolean} True when the Publish button renders.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tshowPublish() {\n\t\t\tif (this.publishTarget === null) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif (typeof this.canPublish === 'boolean') {\n\t\t\t\treturn this.canPublish\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * The kind quick-filters, with an \"all\" entry first.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} The filter options.\n\t\t */\n\t\tkindOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: '', label: t('nextcloud-vue', 'All kinds') },\n\t\t\t\t...this.effectiveKinds.map((value) => ({ value, label: value })),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The kinds to offer, preferring what the ENGINE served.\n\t\t *\n\t\t * An app declares its kinds in the `store` block of its manifest, next\n\t\t * to the schema allowlist, and the engine returns them with the cards.\n\t\t * That is the single place an app says what its store sells; the\n\t\t * `kinds` PROP remains for a host rendering this component directly and\n\t\t * as the pre-response value, and DEFAULT_STORE_KINDS is the floor.\n\t\t *\n\t\t * A served EMPTY list means the app declared none, which is not the\n\t\t * same as not having asked yet — hence `servedKinds` starts null.\n\t\t *\n\t\t * @return {Array<string>} The kind filter values.\n\t\t */\n\t\teffectiveKinds() {\n\t\t\tif (Array.isArray(this.servedKinds) && this.servedKinds.length > 0) {\n\t\t\t\treturn this.servedKinds\n\t\t\t}\n\n\t\t\treturn this.kinds\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, shown whenever the remote list is not carrying\n\t\t * the page. Not a network call, and not a Store on their own.\n\t\t *\n\t\t * 🔴 AN EMPTY ANSWER IS STILL AN EMPTY PAGE. This used to hide the\n\t\t * built-ins whenever the engine answered at all, checking only\n\t\t * `not_configured` and the error outcomes. A federated store that\n\t\t * answers `ok` with zero cards — a configured registry that has\n\t\t * published nothing yet — then rendered a heading, a search box and\n\t\t * nothing else, while the app shipped four items it could have shown.\n\t\t * Measured on decidiq, whose store returns `ok` with 0 cards and 4\n\t\t * built-ins declared.\n\t\t *\n\t\t * ADR-080 Decision 4 is explicit that a surface which goes blank was\n\t\t * never a store, so the test is what the reader can SEE, not which\n\t\t * outcome the engine reported.\n\t\t *\n\t\t * @return {Array<object>} The built-in items to render.\n\t\t */\n\t\tvisibleBuiltIn() {\n\t\t\tif (this.offline === false && this.unreachable === false && this.cards.length > 0) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn this.effectiveBuiltIn\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, preferring what the ENGINE served over the page\n\t\t * config.\n\t\t *\n\t\t * An app declares these ONCE, in its `store` manifest block beside the\n\t\t * allowlist, which is where the schema documents them. The `builtIn`\n\t\t * PROP is a second place to write the same list, and an app that\n\t\t * declared them only in the manifest used to render nothing: the\n\t\t * engine parsed them and never sent them.\n\t\t *\n\t\t * Mirrors `effectiveKinds` exactly, for the same reason.\n\t\t *\n\t\t * @return {Array<object>} The items to render.\n\t\t */\n\t\teffectiveBuiltIn() {\n\t\t\tif (Array.isArray(this.servedBuiltIn) && this.servedBuiltIn.length > 0) {\n\t\t\t\treturn this.servedBuiltIn\n\t\t\t}\n\n\t\t\treturn this.builtIn\n\t\t},\n\n\t\t/**\n\t\t * Heading above the app's own items.\n\t\t *\n\t\t * @return {string} The built-in section heading.\n\t\t */\n\t\tbuiltInHeading() {\n\t\t\treturn this.resolvedApp\n\t\t\t\t? t('nextcloud-vue', 'Included with {app}', { app: this.resolvedApp })\n\t\t\t\t: t('nextcloud-vue', 'Included with this app')\n\t\t},\n\n\t\t/**\n\t\t * The no-registry message, which names the built-in fallback only when\n\t\t * there is one. Promising templates below when the list is empty is\n\t\t * how an empty page reads as a broken one.\n\t\t *\n\t\t * @return {string} The not-configured note.\n\t\t */\n\t\tnotConfiguredMessage() {\n\t\t\tconst base = t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'No store registry is configured, so nothing was requested from the network. An administrator can connect one under Administration settings.',\n\t\t\t)\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\n\t\t/**\n\t\t * The registry-error message, on the same rule as above.\n\t\t *\n\t\t * @return {string} The unreachable note.\n\t\t */\n\t\tunreachableMessage() {\n\t\t\tconst base = t('nextcloud-vue', 'The store registry did not answer.')\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.search()\n\t},\n\n\t/**\n\t * Cancel a pending debounced search.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\tif (this.searchTimer !== null) {\n\t\t\tclearTimeout(this.searchTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Debounce the search so typing does not fire one remote request per\n\t\t * keystroke against somebody else's registry.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonSearchInput() {\n\t\t\tif (this.searchTimer !== null) {\n\t\t\t\tclearTimeout(this.searchTimer)\n\t\t\t}\n\n\t\t\tthis.searchTimer = setTimeout(() => {\n\t\t\t\tthis.search()\n\t\t\t}, 400)\n\t\t},\n\n\t\t/**\n\t\t * Select a kind filter and re-search.\n\t\t *\n\t\t * @param {string} value The kind, or an empty string for all kinds.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tselectKind(value) {\n\t\t\tthis.kind = value\n\t\t\tthis.search()\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current page of store cards.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search() {\n\t\t\t// An unconfigured `app` cannot address any endpoint. Reporting\n\t\t\t// not_configured is the honest outcome and costs no request; the\n\t\t\t// alternative is a fetch to /apps//api/... which 404s and reads to\n\t\t\t// the user as a registry that is down.\n\t\t\tif (this.resolvedApp === '') {\n\t\t\t\tthis.outcome = 'not_configured'\n\t\t\t\tthis.cards = []\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams()\n\t\t\t\tif (this.query) {\n\t\t\t\t\tparams.set('q', this.query)\n\t\t\t\t}\n\t\t\t\tif (this.kind) {\n\t\t\t\t\tparams.set('kind', this.kind)\n\t\t\t\t}\n\n\t\t\t\tconst suffix = params.toString() ? `?${params.toString()}` : ''\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items${suffix}`),\n\t\t\t\t\t{\n\t\t\t\t\t\theaders: { requesttoken: window.OC?.requestToken },\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tthis.outcome = body.outcome ?? 'store_invalid_response'\n\t\t\t\tthis.cards = Array.isArray(body.cards) ? body.cards : []\n\t\t\t\t// Present on every arm the engine answers, including\n\t\t\t\t// not_configured and store_unreachable, so the filters survive\n\t\t\t\t// a registry that is down.\n\t\t\t\tthis.servedKinds = Array.isArray(body.kinds) ? body.kinds : null\n\t\t\t\t// Same contract as `kinds`: present on every arm the engine\n\t\t\t\t// answers, so an app's own items survive a registry that is\n\t\t\t\t// down — which is exactly when they are all there is to show.\n\t\t\t\tthis.servedBuiltIn = Array.isArray(body.builtIn) ? body.builtIn : null\n\t\t\t} catch {\n\t\t\t\tthis.outcome = 'store_unreachable'\n\t\t\t\tthis.cards = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Install one item, and report per component.\n\t\t *\n\t\t * A partial install is a real outcome rather than a failure: an item\n\t\t * carrying both configuration and records installs the configuration\n\t\t * and names the records it refused.\n\t\t *\n\t\t * @param {{slug: string, title: string}} card The card to install.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync install(card) {\n\t\t\tthis.installing = card.slug\n\t\t\tthis.report = null\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items/${encodeURIComponent(card.slug)}/install`),\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\trequesttoken: window.OC?.requestToken,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tif (response.ok !== true) {\n\t\t\t\t\tshowError(body.message\n\t\t\t\t\t\t?? t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst refused = (body.components ?? []).filter((c) => c.status !== 'installed')\n\t\t\t\tif (refused.length === 0) {\n\t\t\t\t\tshowSuccess(t('nextcloud-vue', 'Installed.'))\n\t\t\t\t\tthis.report = null\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.report = {\n\t\t\t\t\ttype: body.success === true ? 'info' : 'warning',\n\t\t\t\t\tmessage: t(\n\t\t\t\t\t\t'nextcloud-vue',\n\t\t\t\t\t\t'Some parts were not installed: {schemas}',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschemas: refused.map((c) => c.schema).join(', '),\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tshowError(t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t} finally {\n\t\t\t\tthis.installing = null\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-store-page {\n\tpadding: 16px;\n\tmax-width: 1200px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-store-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-store-page__heading {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 0 0 4px;\n}\n\n.cn-store-page__title {\n\tmargin: 0;\n}\n\n.cn-store-page__intro {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0 0 16px;\n}\n\n.cn-store-page__controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 12px;\n\talign-items: flex-end;\n\tmargin-bottom: 16px;\n}\n\n.cn-store-page__kinds {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n.cn-store-page__loading {\n\tmargin: 32px auto;\n}\n\n.cn-store-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n\tgap: 16px;\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 16px 0 0;\n}\n\n.cn-store-page__card {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-main-background);\n\tpadding: 16px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-store-page__card-title {\n\tmargin: 0;\n\tfont-size: 1rem;\n}\n\n.cn-store-page__card-kind {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n.cn-store-page__card-description {\n\tmargin: 0;\n\tflex: 1;\n}\n\n.cn-store-page__card-footer {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-store-page__card-version {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n/* Who published a configuration set matters more than its version does, since\n installing one applies somebody else's schemas and flows to this instance. */\n.cn-store-page__card-publisher {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n\tmargin-inline-end: auto;\n}\n\n.cn-store-page__builtin-title {\n\tmargin: 24px 0 0;\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnStorePage.vue3.js","sources":["../../../../src/components/CnStorePage/CnStorePage.vue?vue&type=template&id=25c8171b&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n SPDX-License-Identifier: EUPL-1.2\n\n CnStorePage — browse a remote OpenRegister registry for shareable content,\n and install an item into this instance.\n\n The `store` page type (ADR-080, ADR-114 Decision 4). Promoted out of dossiq's\n app-local StoreGallery, which was the fleet's only implementation and was\n about to be copied fifteen times (ADR-012).\n\n WHAT IS APP-SPECIFIC, AND WHAT IS NOT.\n --------------------------------------\n Everything on this page is generic except three things, and all three arrive\n as props rather than being written into the component:\n\n `app` the app id, which is the only thing in the request URL\n `builtIn` the items this app ships itself, for the no-registry case\n `description` the lead paragraph, because a store of case types and a\n store of connector adapters are not selling the same thing\n\n DISCOVERY IS NOT IMPLEMENTED HERE, ON PURPOSE.\n ----------------------------------------------\n This calls `/apps/<app>/api/store/items`, which is a thin action over\n OpenRegister's `GenericStoreService` (ADR-080 Decision 2). The SSRF guard,\n the redirect refusal, the timeouts and the registry token all stay\n server-side in the engine, and no leaf app ever builds a registry URL.\n\n Install is deliberately NOT generalised (ADR-080 Decision 3): cloning an\n application template, enabling a connector adapter and instantiating an agent\n template are different operations with different authorization. Each app owns\n `POST /api/store/items/{slug}/install`; this component only calls it and\n renders what it reports.\n\n WHO SEES INSTALL AND PUBLISH IS THE APP'S ANSWER, NOT THIS PAGE'S.\n -----------------------------------------------------------------\n `canInstall` and `canPublish` carry a boolean the app resolved (learniq reads\n its ADR-023 action matrix). `null`, the default, falls back to administrators\n only, which is what every store page did before the props existed. Publish\n appears only when the app also names a `publishRoute`: publishing is the\n app's own surface, like install. Both props hide a button and nothing more;\n the app's endpoints enforce the same rule.\n\n A LOCAL-ONLY CARD GRID IS NOT A STORE (ADR-080 Decision 4). With no registry\n configured the engine answers `not_configured` WITHOUT a network call, and\n this page then renders `builtIn`. That fallback is the only reason a surface\n may carry the word Store at all.\n-->\n<template>\n\t<div class=\"cn-store-page\" data-testid=\"store-page\">\n\t\t<div class=\"cn-store-page__header\">\n\t\t\t<div class=\"cn-store-page__heading\">\n\t\t\t\t<h2 class=\"cn-store-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"showPublish\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tdata-testid=\"store-publish\"\n\t\t\t\t\t:to=\"publishTarget\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Publish') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-store-page__intro\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div class=\"cn-store-page__controls\">\n\t\t\t<NcTextField\n\t\t\t\tv-model=\"query\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Search the store')\"\n\t\t\t\t:disabled=\"offline\"\n\t\t\t\tdata-testid=\"store-search\"\n\t\t\t\t@update:modelValue=\"onSearchInput\" />\n\n\t\t\t<div\n\t\t\t\tclass=\"cn-store-page__kinds\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Filter by kind')\">\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"option in kindOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:variant=\"option.value === kind ? 'primary' : 'secondary'\"\n\t\t\t\t\t:disabled=\"offline\"\n\t\t\t\t\t@click=\"selectKind(option.value)\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"44\" class=\"cn-store-page__loading\" />\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"offline\"\n\t\t\ttype=\"info\"\n\t\t\tdata-testid=\"store-not-configured\">\n\t\t\t{{ notConfiguredMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"unreachable\"\n\t\t\ttype=\"warning\"\n\t\t\tdata-testid=\"store-unreachable\">\n\t\t\t{{ unreachableMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"cards.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'Nothing matches that search')\"\n\t\t\t:description=\"\n\t\t\t\tt('nextcloud-vue', 'Try a different term, or clear the kind filter.')\n\t\t\t\" />\n\n\t\t<ul\n\t\t\tv-if=\"!loading && cards.length > 0\"\n\t\t\tclass=\"cn-store-page__grid\"\n\t\t\tdata-testid=\"store-results\">\n\t\t\t<li v-for=\"card in cards\" :key=\"card.slug\" class=\"cn-store-page__card\">\n\t\t\t\t<h3 class=\"cn-store-page__card-title\">\n\t\t\t\t\t{{ card.title || card.slug }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"card.typeName || card.kind\" class=\"cn-store-page__card-kind\">\n\t\t\t\t\t{{ card.typeName || card.kind }}\n\t\t\t\t</p>\n\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t{{ card.description }}\n\t\t\t\t</p>\n\t\t\t\t<div class=\"cn-store-page__card-footer\">\n\t\t\t\t\t<span v-if=\"card.publisher\" class=\"cn-store-page__card-publisher\">\n\t\t\t\t\t\t{{ card.publisher }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"card.version\" class=\"cn-store-page__card-version\">\n\t\t\t\t\t\t{{ card.version }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"showInstall\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"store-install\"\n\t\t\t\t\t\t:disabled=\"installing === card.slug\"\n\t\t\t\t\t\t@click=\"install(card)\">\n\t\t\t\t\t\t{{\n\t\t\t\t\t\t\tinstalling === card.slug\n\t\t\t\t\t\t\t\t? t('nextcloud-vue', 'Installing…')\n\t\t\t\t\t\t\t\t: t('nextcloud-vue', 'Install')\n\t\t\t\t\t\t}}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<div\n\t\t\tv-if=\"!loading && visibleBuiltIn.length > 0\"\n\t\t\tclass=\"cn-store-page__builtin\">\n\t\t\t<h3 class=\"cn-store-page__builtin-title\">\n\t\t\t\t{{ builtInHeading }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-store-page__grid\" data-testid=\"store-builtin\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"item in visibleBuiltIn\"\n\t\t\t\t\t:key=\"item.slug\"\n\t\t\t\t\tclass=\"cn-store-page__card\">\n\t\t\t\t\t<h4 class=\"cn-store-page__card-title\">\n\t\t\t\t\t\t{{ item.title || item.slug }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t\t{{ item.description }}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\n\t\t<NcNoteCard\n\t\t\tv-if=\"report\"\n\t\t\t:type=\"report.type\"\n\t\t\tdata-testid=\"store-install-report\">\n\t\t\t{{ report.message }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { showError, showSuccess } from '@nextcloud/dialogs'\nimport { t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport {\n\tNcButton,\n\tNcEmptyContent,\n\tNcLoadingIcon,\n\tNcNoteCard,\n\tNcTextField,\n} from '@nextcloud/vue'\n\n/**\n * The kind vocabulary from ADR-080 Decision 5. A `kind` names what installing\n * an item DOES, which is what lets one registry serve several apps from one\n * schema. An app whose items are a different shape passes its own `kinds`.\n */\nexport const DEFAULT_STORE_KINDS = [\n\t'app-template',\n\t'adapter',\n\t'source-template',\n\t'configuration-template',\n\t'agent-template',\n]\n\nexport default {\n\tname: 'CnStorePage',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tNcTextField,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"store\"` page arrives with the flattened\n\t\t * props below and this default `{}`. Reading `page.config` here would\n\t\t * render an empty store forever, which is exactly what happened to\n\t\t * CnReportsPage before #897. Kept only so a host rendering this\n\t\t * component directly can still hand it a whole page; the flattened\n\t\t * props win.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The app id. The ONLY app-specific value in the request URL, and the\n\t\t * reason this component is shared rather than copied.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tapp: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Page title, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph. A store of case types and a store of connector\n\t\t * adapters are not offering the same thing, so the app says what its\n\t\t * store is for.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Kind quick-filters. Defaults to the ADR-080 Decision 5 vocabulary.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tkinds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => DEFAULT_STORE_KINDS,\n\t\t},\n\n\t\t/**\n\t\t * The items this app ships itself, rendered when the engine reports\n\t\t * `not_configured` or the registry does not answer.\n\t\t *\n\t\t * ADR-080 Decision 4 makes this the price of the word Store: a surface\n\t\t * that goes blank without a registry was never a store, and a surface\n\t\t * that only ever shows these is a Templates page.\n\t\t *\n\t\t * @type {Array<{slug: string, title: string, description: string}>}\n\t\t */\n\t\tbuiltIn: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Install, as the app resolved it\n\t\t * (learniq: its ADR-023 matrix). `true` shows it on every remote\n\t\t * card, `false` hides it from everyone, administrators included.\n\t\t * `null` (the default) keeps the rule from before this prop:\n\t\t * administrators only. Presentation only: the app's install endpoint\n\t\t * must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanInstall: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Publish, as the app resolved it.\n\t\t * `null` (the default) falls back to administrators only. Publish\n\t\t * renders only when `publishRoute` is set as well. Presentation\n\t\t * only: the app's publish endpoint must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanPublish: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where Publish takes the user: a route name (string) or a route\n\t\t * location (object). Publishing is the app's own surface, so this\n\t\t * page never sends anything to a registry itself. Empty means no\n\t\t * Publish button, whatever `canPublish` says, so an app that never\n\t\t * sets it renders what it rendered before.\n\t\t *\n\t\t * @type {string|object}\n\t\t */\n\t\tpublishRoute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\toutcome: null,\n\t\t\tcards: [],\n\t\t\tquery: '',\n\t\t\tkind: '',\n\t\t\tinstalling: null,\n\t\t\treport: null,\n\t\t\tsearchTimer: null,\n\t\t\t// The kinds the ENGINE served, from the app's `store` manifest\n\t\t\t// block. Null until the first response, so \"not asked yet\" stays\n\t\t\t// distinguishable from \"the app declares none\".\n\t\t\tservedKinds: null,\n\t\t\t// The built-in items the ENGINE served, from the app's `store`\n\t\t\t// manifest block. Null until the first response, so \"not asked\n\t\t\t// yet\" stays distinguishable from \"the app declares none\".\n\t\t\tservedBuiltIn: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The page title, preferring the flattened prop over the page object.\n\t\t *\n\t\t * @return {string} The heading to render.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title || this.page?.title || t('nextcloud-vue', 'Store')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The intro copy, or an empty string.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.description || this.page?.config?.description || ''\n\t\t},\n\n\t\t/**\n\t\t * The app id, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The app id used to address the store endpoints.\n\t\t */\n\t\tresolvedApp() {\n\t\t\treturn this.app || this.page?.config?.app || ''\n\t\t},\n\n\t\t/**\n\t\t * No registry configured. The engine reported this WITHOUT making a\n\t\t * network call, which is the ADR-080 Decision 4 fallback.\n\t\t *\n\t\t * @return {boolean} True when no registry is connected.\n\t\t */\n\t\toffline() {\n\t\t\treturn this.outcome === 'not_configured'\n\t\t},\n\n\t\t/**\n\t\t * The registry is configured but did not answer usefully.\n\t\t *\n\t\t * @return {boolean} True when the registry errored.\n\t\t */\n\t\tunreachable() {\n\t\t\treturn (\n\t\t\t\tthis.outcome === 'store_unreachable'\n\t\t\t\t|| this.outcome === 'store_invalid_response'\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user is an administrator: the fallback for both\n\t\t * actions when the app did not say.\n\t\t *\n\t\t * @return {boolean} True when the current user is an administrator.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tisAdmin() {\n\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t},\n\n\t\t/**\n\t\t * Whether Install shows. The app's answer wins; without one only an\n\t\t * administrator installs, because what an install writes is the shape\n\t\t * of the work every handler then operates against. The server\n\t\t * enforces this too; hiding the button keeps a user from discovering\n\t\t * a refusal as a 403.\n\t\t *\n\t\t * @return {boolean} True when the Install buttons render.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tshowInstall() {\n\t\t\tif (typeof this.canInstall === 'boolean') {\n\t\t\t\treturn this.canInstall\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * Where Publish navigates, or null when the app named no destination.\n\t\t * A string is a route NAME, the way manifest menu entries name routes.\n\t\t *\n\t\t * @return {object|null} A vue-router location, or null.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tpublishTarget() {\n\t\t\tif (typeof this.publishRoute === 'string') {\n\t\t\t\treturn this.publishRoute === '' ? null : { name: this.publishRoute }\n\t\t\t}\n\n\t\t\tif (this.publishRoute !== null && typeof this.publishRoute === 'object') {\n\t\t\t\treturn this.publishRoute\n\t\t\t}\n\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Whether Publish shows: a destination is named AND the app allows it\n\t\t * (or, without an answer, the user is an administrator). No\n\t\t * destination means no button, because a button that goes nowhere\n\t\t * is worse than none.\n\t\t *\n\t\t * @return {boolean} True when the Publish button renders.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tshowPublish() {\n\t\t\tif (this.publishTarget === null) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif (typeof this.canPublish === 'boolean') {\n\t\t\t\treturn this.canPublish\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * The kind quick-filters, with an \"all\" entry first.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} The filter options.\n\t\t */\n\t\tkindOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: '', label: t('nextcloud-vue', 'All kinds') },\n\t\t\t\t...this.effectiveKinds.map((value) => ({ value, label: value })),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The kinds to offer, preferring what the ENGINE served.\n\t\t *\n\t\t * An app declares its kinds in the `store` block of its manifest, next\n\t\t * to the schema allowlist, and the engine returns them with the cards.\n\t\t * That is the single place an app says what its store sells; the\n\t\t * `kinds` PROP remains for a host rendering this component directly and\n\t\t * as the pre-response value, and DEFAULT_STORE_KINDS is the floor.\n\t\t *\n\t\t * A served EMPTY list means the app declared none, which is not the\n\t\t * same as not having asked yet — hence `servedKinds` starts null.\n\t\t *\n\t\t * @return {Array<string>} The kind filter values.\n\t\t */\n\t\teffectiveKinds() {\n\t\t\tif (Array.isArray(this.servedKinds) && this.servedKinds.length > 0) {\n\t\t\t\treturn this.servedKinds\n\t\t\t}\n\n\t\t\treturn this.kinds\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, shown whenever the remote list is not carrying\n\t\t * the page. Not a network call, and not a Store on their own.\n\t\t *\n\t\t * 🔴 AN EMPTY ANSWER IS STILL AN EMPTY PAGE. This used to hide the\n\t\t * built-ins whenever the engine answered at all, checking only\n\t\t * `not_configured` and the error outcomes. A federated store that\n\t\t * answers `ok` with zero cards — a configured registry that has\n\t\t * published nothing yet — then rendered a heading, a search box and\n\t\t * nothing else, while the app shipped four items it could have shown.\n\t\t * Measured on decidiq, whose store returns `ok` with 0 cards and 4\n\t\t * built-ins declared.\n\t\t *\n\t\t * ADR-080 Decision 4 is explicit that a surface which goes blank was\n\t\t * never a store, so the test is what the reader can SEE, not which\n\t\t * outcome the engine reported.\n\t\t *\n\t\t * @return {Array<object>} The built-in items to render.\n\t\t */\n\t\tvisibleBuiltIn() {\n\t\t\tif (this.offline === false && this.unreachable === false && this.cards.length > 0) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn this.effectiveBuiltIn\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, preferring what the ENGINE served over the page\n\t\t * config.\n\t\t *\n\t\t * An app declares these ONCE, in its `store` manifest block beside the\n\t\t * allowlist, which is where the schema documents them. The `builtIn`\n\t\t * PROP is a second place to write the same list, and an app that\n\t\t * declared them only in the manifest used to render nothing: the\n\t\t * engine parsed them and never sent them.\n\t\t *\n\t\t * Mirrors `effectiveKinds` exactly, for the same reason.\n\t\t *\n\t\t * @return {Array<object>} The items to render.\n\t\t */\n\t\teffectiveBuiltIn() {\n\t\t\tif (Array.isArray(this.servedBuiltIn) && this.servedBuiltIn.length > 0) {\n\t\t\t\treturn this.servedBuiltIn\n\t\t\t}\n\n\t\t\treturn this.builtIn\n\t\t},\n\n\t\t/**\n\t\t * Heading above the app's own items.\n\t\t *\n\t\t * @return {string} The built-in section heading.\n\t\t */\n\t\tbuiltInHeading() {\n\t\t\treturn this.resolvedApp\n\t\t\t\t? t('nextcloud-vue', 'Included with {app}', { app: this.resolvedApp })\n\t\t\t\t: t('nextcloud-vue', 'Included with this app')\n\t\t},\n\n\t\t/**\n\t\t * The no-registry message, which names the built-in fallback only when\n\t\t * there is one. Promising templates below when the list is empty is\n\t\t * how an empty page reads as a broken one.\n\t\t *\n\t\t * @return {string} The not-configured note.\n\t\t */\n\t\tnotConfiguredMessage() {\n\t\t\tconst base = t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'No store registry is configured, so nothing was requested from the network. An administrator can connect one under Administration settings.',\n\t\t\t)\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\n\t\t/**\n\t\t * The registry-error message, on the same rule as above.\n\t\t *\n\t\t * @return {string} The unreachable note.\n\t\t */\n\t\tunreachableMessage() {\n\t\t\tconst base = t('nextcloud-vue', 'The store registry did not answer.')\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.search()\n\t},\n\n\t/**\n\t * Cancel a pending debounced search.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\tif (this.searchTimer !== null) {\n\t\t\tclearTimeout(this.searchTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Debounce the search so typing does not fire one remote request per\n\t\t * keystroke against somebody else's registry.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonSearchInput() {\n\t\t\tif (this.searchTimer !== null) {\n\t\t\t\tclearTimeout(this.searchTimer)\n\t\t\t}\n\n\t\t\tthis.searchTimer = setTimeout(() => {\n\t\t\t\tthis.search()\n\t\t\t}, 400)\n\t\t},\n\n\t\t/**\n\t\t * Select a kind filter and re-search.\n\t\t *\n\t\t * @param {string} value The kind, or an empty string for all kinds.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tselectKind(value) {\n\t\t\tthis.kind = value\n\t\t\tthis.search()\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current page of store cards.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search() {\n\t\t\t// An unconfigured `app` cannot address any endpoint. Reporting\n\t\t\t// not_configured is the honest outcome and costs no request; the\n\t\t\t// alternative is a fetch to /apps//api/... which 404s and reads to\n\t\t\t// the user as a registry that is down.\n\t\t\tif (this.resolvedApp === '') {\n\t\t\t\tthis.outcome = 'not_configured'\n\t\t\t\tthis.cards = []\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams()\n\t\t\t\tif (this.query) {\n\t\t\t\t\tparams.set('q', this.query)\n\t\t\t\t}\n\t\t\t\tif (this.kind) {\n\t\t\t\t\tparams.set('kind', this.kind)\n\t\t\t\t}\n\n\t\t\t\tconst suffix = params.toString() ? `?${params.toString()}` : ''\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items${suffix}`),\n\t\t\t\t\t{\n\t\t\t\t\t\theaders: { requesttoken: window.OC?.requestToken },\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tthis.outcome = body.outcome ?? 'store_invalid_response'\n\t\t\t\tthis.cards = Array.isArray(body.cards) ? body.cards : []\n\t\t\t\t// Present on every arm the engine answers, including\n\t\t\t\t// not_configured and store_unreachable, so the filters survive\n\t\t\t\t// a registry that is down.\n\t\t\t\tthis.servedKinds = Array.isArray(body.kinds) ? body.kinds : null\n\t\t\t\t// Same contract as `kinds`: present on every arm the engine\n\t\t\t\t// answers, so an app's own items survive a registry that is\n\t\t\t\t// down — which is exactly when they are all there is to show.\n\t\t\t\tthis.servedBuiltIn = Array.isArray(body.builtIn) ? body.builtIn : null\n\t\t\t} catch {\n\t\t\t\tthis.outcome = 'store_unreachable'\n\t\t\t\tthis.cards = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Install one item, and report per component.\n\t\t *\n\t\t * A partial install is a real outcome rather than a failure: an item\n\t\t * carrying both configuration and records installs the configuration\n\t\t * and names the records it refused.\n\t\t *\n\t\t * @param {{slug: string, title: string}} card The card to install.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync install(card) {\n\t\t\tthis.installing = card.slug\n\t\t\tthis.report = null\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items/${encodeURIComponent(card.slug)}/install`),\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\trequesttoken: window.OC?.requestToken,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tif (response.ok !== true) {\n\t\t\t\t\tshowError(body.message\n\t\t\t\t\t\t?? t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst refused = (body.components ?? []).filter((c) => c.status !== 'installed')\n\t\t\t\tif (refused.length === 0) {\n\t\t\t\t\tshowSuccess(t('nextcloud-vue', 'Installed.'))\n\t\t\t\t\tthis.report = null\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.report = {\n\t\t\t\t\ttype: body.success === true ? 'info' : 'warning',\n\t\t\t\t\tmessage: t(\n\t\t\t\t\t\t'nextcloud-vue',\n\t\t\t\t\t\t'Some parts were not installed: {schemas}',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschemas: refused.map((c) => c.schema).join(', '),\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tshowError(t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t} finally {\n\t\t\t\tthis.installing = null\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-store-page {\n\tpadding: 16px;\n\tmax-width: 1200px;\n}\n\n.cn-store-page__heading {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 0 0 4px;\n}\n\n.cn-store-page__title {\n\tmargin: 0;\n}\n\n.cn-store-page__intro {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0 0 16px;\n}\n\n.cn-store-page__controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 12px;\n\talign-items: flex-end;\n\tmargin-bottom: 16px;\n}\n\n.cn-store-page__kinds {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n.cn-store-page__loading {\n\tmargin: 32px auto;\n}\n\n.cn-store-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n\tgap: 16px;\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 16px 0 0;\n}\n\n.cn-store-page__card {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-main-background);\n\tpadding: 16px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-store-page__card-title {\n\tmargin: 0;\n\tfont-size: 1rem;\n}\n\n.cn-store-page__card-kind {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n.cn-store-page__card-description {\n\tmargin: 0;\n\tflex: 1;\n}\n\n.cn-store-page__card-footer {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-store-page__card-version {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n/* Who published a configuration set matters more than its version does, since\n installing one applies somebody else's schemas and flows to this instance. */\n.cn-store-page__card-publisher {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n\tmargin-inline-end: auto;\n}\n\n.cn-store-page__builtin-title {\n\tmargin: 24px 0 0;\n}\n</style>\n"],"names":["_createElementVNode","_toDisplayString","_createBlock","_createVNode","_createElementBlock","_Fragment","_renderList","_createTextVNode","_openBlock"],"mappings":";;;AAiGM,EAAA,KAAK,EAAC,eAAe;AAAC,EAAA,aAAA,EAAA,YAAA;;AACrB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAC,GAAA;;AAEN,MAAA,UAAA,GAAA,EAAA,KAAI,EAAA,sBAAA,GAAA;;;AAWqB,EAAA,KAAK,EAAA,sBAAuB;;AAKtD,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAgB,GAAA;;;;OAgDrB,EAAC,qBAAA;AACJ,EAAA,aAAS,EAAC,eAAe;;AAEtB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,2BAAA,GAAA;;;AAGD,EAAA,KAAA,EAAA,0BAAA;;AAGA,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,iCAAE,GAAA;AAGN,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAAO,GAAA;;;AACR,EAAA,KAAA,EAAA,+BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AA/EZ,IAAAA,kBAAA,CAgBE,OAhBF,UAgBE,EAAA;yBAJK,iBAAA,EAAA;QAVLA,kBAEK,CAAA,IAAA,EAFL,UAEK,EAAAC,eAAA,CADD,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;SAGV,QAAW,CAAA,WAAA;0BADlBC,WAMW,CAAA,mBAAA,EAAA;;AAJV,cAAA,OAAO,EAAC,WAAC;;AAER,cAAA,EAAE,EAAE,QAAM,CAAA,aAAA;;+BACX,MAA2B;gDAAxB,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,SAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;OAGG,QAAmB,CAAA,mBAAA;4EACzB,QAAA,CAAA,mBAAA,CAAA,EAAA,CAAA,YAAA;;;uBAyBE,iBAAA,EAAA;MApBLC,WAKsC,CAAA,sBAAA,EAAA;oBAJ5B,KAAM,CAAA,KAAA;;gDAAN,KAAM,CAAA,KAAA,IAAA,MAAA,CAAA,CAAA;UAIK,QAAa,CAAA,aAAA;;AAHhC,QAAA,KAAK,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,kBAAA,CAAA;AACR,QAAA,QAAQ,EAAE,QAAO,CAAA,OAAA;AAClB,QAAA,aAAI,EAAA,cAAA;;MAGJH,kBAYK,CAAA,KAAA,EAAA;AAXL,QAAA,KAAK,EAAC,sBAAsB;AAC3B,QAAA,IAAG,EAAC,OAAO;AACX,QAAA,YAAE,EAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,gBAAA,CAAA;;0BACHI,kBAOS,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CANS,QAAW,CAAA,WAAA,EAAA,CAArB,MAAM,KAAA;+BADdJ,WAOS,CAAA,mBAAA,EAAA;YALP,GAAG,EAAE,MAAM,CAAC;AACZ,YAAA,OAAK,EAAA,MAAA,CAAA,KAAA,KAAA,KAAA,CAAA,IAAA,GAAA,SAAA,GAAA,WAAA;AACL,YAAA,QAAQ,EAAE,QAAO,CAAA,OAAA;AACjB,YAAA,OAAK,EAAE,MAAA,KAAA,QAAA,CAAA,UAAU,CAAA,MAAA,CAAA,KAAA,CAAA,CAAA;;6BAClB,MAAM;AAAH,cAAAK,eAAA,CAAAN,eAAA,CAAA,MAAG,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;KAKY,KAAO,CAAA,OAAA;sBAAxBC,WAAmC,CAAA,wBAAA,EAAA;;AAAR,UAAA,IAAI,EAAE,EAAE;AAAA,UAAA,KAAA,EAAA,wBAAA;;SAG3B,QAAM,CAAA,OAAA;mCAIL,CAAA,qBAAA,EAAA;;AAHP,YAAA,IAAA,EAAA,MAAM;yBACA,EAAC,sBAAsB;;mCAC9B;8CAAD,QAAC,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA;;;;WAID,QAAA,CAAA,WAAA;qCAIS,CAAA,qBAAA,EAAA;;;AAFZ,cAAA,aAAE,EAAA,mBAAA;;+BACF,MAAwB;gDAArB,QAAkB,CAAA,kBAAA,CAAA,EAAA,CAAA,YAAA;;;;AAIjB,YAAA,CAAA,KAAA,CAAA,KAAA,CAAA,MAAA,KAAA,CAAA;4BADHA,WAKG,CAAA,yBAAA,EAAA;;AAHH,gBAAA,IAAI,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,6BAAA,CAAA;gBACP,WAAE;IAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,iDAAA,CAAA;;;;MAKI,KAAO,CAAA,OAAA,IAAI,KAAK,CAAA,KAAA,CAAC,MAAM,GAAA,CAAA;AAD5B,SAAAM,SAAA,EAAA,EAAAJ,kBAAA,OAAA;4BAICA,8CAAgB,KAAK,CAAA,KAAA,EAAA,CAAb,IAAI,KAAA;iCAAZA;cAAqB,GAAA,EAAA,IAAA,CAAA,IAAA;AAAA,cAAA,KAAA,EAAA,qBAAA;;cACtBJ,kBAEE,CAAA,IAAA,EAFF,UAEE,EADCC,eAAA,CAAA,IAAA,CAAA,KAAA,IAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;AAED,cAAA,CAAA,IAAA,CAAA,QAAA,IAAA,IAAA,CAAA,IAAA;gCAAHG,kBAEI,CAAA,GAAA,EAFJ,WAEI;;AACJ,cAAAJ,kBAAA,CAEI,GAFJ,EAAA,WAEI,EADAC,eAAA,CAAA,IAAI,CAAC,WAAQ,CAAA,EAAA,CAAA,YAAA;AAEjB,cAAAD,kBAAA,QAAA;AACU,gBAAA,CAAA,IAAA,CAAA,SAAA;AAAT,qBAAAQ,SAAA,EAAA,EAAAJ,kBAAA,OAAA,EAAA,aACCH,eAAA,CAAA,IAAA,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnStorePage.vue3.js","sources":["../../../../src/components/CnStorePage/CnStorePage.vue?vue&type=template&id=25c8171b&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n SPDX-License-Identifier: EUPL-1.2\n\n CnStorePage — browse a remote OpenRegister registry for shareable content,\n and install an item into this instance.\n\n The `store` page type (ADR-080, ADR-114 Decision 4). Promoted out of dossiq's\n app-local StoreGallery, which was the fleet's only implementation and was\n about to be copied fifteen times (ADR-012).\n\n WHAT IS APP-SPECIFIC, AND WHAT IS NOT.\n --------------------------------------\n Everything on this page is generic except three things, and all three arrive\n as props rather than being written into the component:\n\n `app` the app id, which is the only thing in the request URL\n `builtIn` the items this app ships itself, for the no-registry case\n `description` the lead paragraph, because a store of case types and a\n store of connector adapters are not selling the same thing\n\n DISCOVERY IS NOT IMPLEMENTED HERE, ON PURPOSE.\n ----------------------------------------------\n This calls `/apps/<app>/api/store/items`, which is a thin action over\n OpenRegister's `GenericStoreService` (ADR-080 Decision 2). The SSRF guard,\n the redirect refusal, the timeouts and the registry token all stay\n server-side in the engine, and no leaf app ever builds a registry URL.\n\n Install is deliberately NOT generalised (ADR-080 Decision 3): cloning an\n application template, enabling a connector adapter and instantiating an agent\n template are different operations with different authorization. Each app owns\n `POST /api/store/items/{slug}/install`; this component only calls it and\n renders what it reports.\n\n WHO SEES INSTALL AND PUBLISH IS THE APP'S ANSWER, NOT THIS PAGE'S.\n -----------------------------------------------------------------\n `canInstall` and `canPublish` carry a boolean the app resolved (learniq reads\n its ADR-023 action matrix). `null`, the default, falls back to administrators\n only, which is what every store page did before the props existed. Publish\n appears only when the app also names a `publishRoute`: publishing is the\n app's own surface, like install. Both props hide a button and nothing more;\n the app's endpoints enforce the same rule.\n\n A LOCAL-ONLY CARD GRID IS NOT A STORE (ADR-080 Decision 4). With no registry\n configured the engine answers `not_configured` WITHOUT a network call, and\n this page then renders `builtIn`. That fallback is the only reason a surface\n may carry the word Store at all.\n-->\n<template>\n\t<div class=\"cn-store-page\" data-testid=\"store-page\">\n\t\t<div class=\"cn-store-page__header\">\n\t\t\t<div class=\"cn-store-page__heading\">\n\t\t\t\t<h2 class=\"cn-store-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"showPublish\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tdata-testid=\"store-publish\"\n\t\t\t\t\t:to=\"publishTarget\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Publish') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-store-page__intro\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<div class=\"cn-store-page__controls\">\n\t\t\t<NcTextField\n\t\t\t\tv-model=\"query\"\n\t\t\t\t:label=\"t('nextcloud-vue', 'Search the store')\"\n\t\t\t\t:disabled=\"offline\"\n\t\t\t\tdata-testid=\"store-search\"\n\t\t\t\t@update:modelValue=\"onSearchInput\" />\n\n\t\t\t<div\n\t\t\t\tclass=\"cn-store-page__kinds\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Filter by kind')\">\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"option in kindOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:variant=\"option.value === kind ? 'primary' : 'secondary'\"\n\t\t\t\t\t:disabled=\"offline\"\n\t\t\t\t\t@click=\"selectKind(option.value)\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"44\" class=\"cn-store-page__loading\" />\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"offline\"\n\t\t\ttype=\"info\"\n\t\t\tdata-testid=\"store-not-configured\">\n\t\t\t{{ notConfiguredMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"unreachable\"\n\t\t\ttype=\"warning\"\n\t\t\tdata-testid=\"store-unreachable\">\n\t\t\t{{ unreachableMessage }}\n\t\t</NcNoteCard>\n\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"cards.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'Nothing matches that search')\"\n\t\t\t:description=\"\n\t\t\t\tt('nextcloud-vue', 'Try a different term, or clear the kind filter.')\n\t\t\t\" />\n\n\t\t<ul\n\t\t\tv-if=\"!loading && cards.length > 0\"\n\t\t\tclass=\"cn-store-page__grid\"\n\t\t\tdata-testid=\"store-results\">\n\t\t\t<li v-for=\"card in cards\" :key=\"card.slug\" class=\"cn-store-page__card\">\n\t\t\t\t<h3 class=\"cn-store-page__card-title\">\n\t\t\t\t\t{{ card.title || card.slug }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"card.typeName || card.kind\" class=\"cn-store-page__card-kind\">\n\t\t\t\t\t{{ card.typeName || card.kind }}\n\t\t\t\t</p>\n\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t{{ card.description }}\n\t\t\t\t</p>\n\t\t\t\t<div class=\"cn-store-page__card-footer\">\n\t\t\t\t\t<span v-if=\"card.publisher\" class=\"cn-store-page__card-publisher\">\n\t\t\t\t\t\t{{ card.publisher }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"card.version\" class=\"cn-store-page__card-version\">\n\t\t\t\t\t\t{{ card.version }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"showInstall\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"store-install\"\n\t\t\t\t\t\t:disabled=\"installing === card.slug\"\n\t\t\t\t\t\t@click=\"install(card)\">\n\t\t\t\t\t\t{{\n\t\t\t\t\t\t\tinstalling === card.slug\n\t\t\t\t\t\t\t\t? t('nextcloud-vue', 'Installing…')\n\t\t\t\t\t\t\t\t: t('nextcloud-vue', 'Install')\n\t\t\t\t\t\t}}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<div\n\t\t\tv-if=\"!loading && visibleBuiltIn.length > 0\"\n\t\t\tclass=\"cn-store-page__builtin\">\n\t\t\t<h3 class=\"cn-store-page__builtin-title\">\n\t\t\t\t{{ builtInHeading }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-store-page__grid\" data-testid=\"store-builtin\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"item in visibleBuiltIn\"\n\t\t\t\t\t:key=\"item.slug\"\n\t\t\t\t\tclass=\"cn-store-page__card\">\n\t\t\t\t\t<h4 class=\"cn-store-page__card-title\">\n\t\t\t\t\t\t{{ item.title || item.slug }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p class=\"cn-store-page__card-description\">\n\t\t\t\t\t\t{{ item.description }}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\n\t\t<NcNoteCard\n\t\t\tv-if=\"report\"\n\t\t\t:type=\"report.type\"\n\t\t\tdata-testid=\"store-install-report\">\n\t\t\t{{ report.message }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { showError, showSuccess } from '@nextcloud/dialogs'\nimport { t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport {\n\tNcButton,\n\tNcEmptyContent,\n\tNcLoadingIcon,\n\tNcNoteCard,\n\tNcTextField,\n} from '@nextcloud/vue'\n\n/**\n * The kind vocabulary from ADR-080 Decision 5. A `kind` names what installing\n * an item DOES, which is what lets one registry serve several apps from one\n * schema. An app whose items are a different shape passes its own `kinds`.\n */\nexport const DEFAULT_STORE_KINDS = [\n\t'app-template',\n\t'adapter',\n\t'source-template',\n\t'configuration-template',\n\t'agent-template',\n]\n\nexport default {\n\tname: 'CnStorePage',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tNcTextField,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"store\"` page arrives with the flattened\n\t\t * props below and this default `{}`. Reading `page.config` here would\n\t\t * render an empty store forever, which is exactly what happened to\n\t\t * CnReportsPage before #897. Kept only so a host rendering this\n\t\t * component directly can still hand it a whole page; the flattened\n\t\t * props win.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The app id. The ONLY app-specific value in the request URL, and the\n\t\t * reason this component is shared rather than copied.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tapp: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Page title, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph. A store of case types and a store of connector\n\t\t * adapters are not offering the same thing, so the app says what its\n\t\t * store is for.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Kind quick-filters. Defaults to the ADR-080 Decision 5 vocabulary.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tkinds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => DEFAULT_STORE_KINDS,\n\t\t},\n\n\t\t/**\n\t\t * The items this app ships itself, rendered when the engine reports\n\t\t * `not_configured` or the registry does not answer.\n\t\t *\n\t\t * ADR-080 Decision 4 makes this the price of the word Store: a surface\n\t\t * that goes blank without a registry was never a store, and a surface\n\t\t * that only ever shows these is a Templates page.\n\t\t *\n\t\t * @type {Array<{slug: string, title: string, description: string}>}\n\t\t */\n\t\tbuiltIn: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Install, as the app resolved it\n\t\t * (learniq: its ADR-023 matrix). `true` shows it on every remote\n\t\t * card, `false` hides it from everyone, administrators included.\n\t\t * `null` (the default) keeps the rule from before this prop:\n\t\t * administrators only. Presentation only: the app's install endpoint\n\t\t * must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanInstall: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the signed-in user sees Publish, as the app resolved it.\n\t\t * `null` (the default) falls back to administrators only. Publish\n\t\t * renders only when `publishRoute` is set as well. Presentation\n\t\t * only: the app's publish endpoint must enforce the same rule.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tcanPublish: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where Publish takes the user: a route name (string) or a route\n\t\t * location (object). Publishing is the app's own surface, so this\n\t\t * page never sends anything to a registry itself. Empty means no\n\t\t * Publish button, whatever `canPublish` says, so an app that never\n\t\t * sets it renders what it rendered before.\n\t\t *\n\t\t * @type {string|object}\n\t\t */\n\t\tpublishRoute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\toutcome: null,\n\t\t\tcards: [],\n\t\t\tquery: '',\n\t\t\tkind: '',\n\t\t\tinstalling: null,\n\t\t\treport: null,\n\t\t\tsearchTimer: null,\n\t\t\t// The kinds the ENGINE served, from the app's `store` manifest\n\t\t\t// block. Null until the first response, so \"not asked yet\" stays\n\t\t\t// distinguishable from \"the app declares none\".\n\t\t\tservedKinds: null,\n\t\t\t// The built-in items the ENGINE served, from the app's `store`\n\t\t\t// manifest block. Null until the first response, so \"not asked\n\t\t\t// yet\" stays distinguishable from \"the app declares none\".\n\t\t\tservedBuiltIn: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The page title, preferring the flattened prop over the page object.\n\t\t *\n\t\t * @return {string} The heading to render.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title || this.page?.title || t('nextcloud-vue', 'Store')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The intro copy, or an empty string.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.description || this.page?.config?.description || ''\n\t\t},\n\n\t\t/**\n\t\t * The app id, preferring the flattened prop.\n\t\t *\n\t\t * @return {string} The app id used to address the store endpoints.\n\t\t */\n\t\tresolvedApp() {\n\t\t\treturn this.app || this.page?.config?.app || ''\n\t\t},\n\n\t\t/**\n\t\t * No registry configured. The engine reported this WITHOUT making a\n\t\t * network call, which is the ADR-080 Decision 4 fallback.\n\t\t *\n\t\t * @return {boolean} True when no registry is connected.\n\t\t */\n\t\toffline() {\n\t\t\treturn this.outcome === 'not_configured'\n\t\t},\n\n\t\t/**\n\t\t * The registry is configured but did not answer usefully.\n\t\t *\n\t\t * @return {boolean} True when the registry errored.\n\t\t */\n\t\tunreachable() {\n\t\t\treturn (\n\t\t\t\tthis.outcome === 'store_unreachable'\n\t\t\t\t|| this.outcome === 'store_invalid_response'\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user is an administrator: the fallback for both\n\t\t * actions when the app did not say.\n\t\t *\n\t\t * @return {boolean} True when the current user is an administrator.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tisAdmin() {\n\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t},\n\n\t\t/**\n\t\t * Whether Install shows. The app's answer wins; without one only an\n\t\t * administrator installs, because what an install writes is the shape\n\t\t * of the work every handler then operates against. The server\n\t\t * enforces this too; hiding the button keeps a user from discovering\n\t\t * a refusal as a 403.\n\t\t *\n\t\t * @return {boolean} True when the Install buttons render.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-1-the-consuming-app-decides-who-sees-install\n\t\t */\n\t\tshowInstall() {\n\t\t\tif (typeof this.canInstall === 'boolean') {\n\t\t\t\treturn this.canInstall\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * Where Publish navigates, or null when the app named no destination.\n\t\t * A string is a route NAME, the way manifest menu entries name routes.\n\t\t *\n\t\t * @return {object|null} A vue-router location, or null.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tpublishTarget() {\n\t\t\tif (typeof this.publishRoute === 'string') {\n\t\t\t\treturn this.publishRoute === '' ? null : { name: this.publishRoute }\n\t\t\t}\n\n\t\t\tif (this.publishRoute !== null && typeof this.publishRoute === 'object') {\n\t\t\t\treturn this.publishRoute\n\t\t\t}\n\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Whether Publish shows: a destination is named AND the app allows it\n\t\t * (or, without an answer, the user is an administrator). No\n\t\t * destination means no button, because a button that goes nowhere\n\t\t * is worse than none.\n\t\t *\n\t\t * @return {boolean} True when the Publish button renders.\n\t\t *\n\t\t * @spec openspec/changes/store-page-action-visibility/specs/store-page/spec.md#requirement-req-stp-2-the-consuming-app-decides-who-sees-publish-and-where-it-leads\n\t\t */\n\t\tshowPublish() {\n\t\t\tif (this.publishTarget === null) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif (typeof this.canPublish === 'boolean') {\n\t\t\t\treturn this.canPublish\n\t\t\t}\n\n\t\t\treturn this.isAdmin\n\t\t},\n\n\t\t/**\n\t\t * The kind quick-filters, with an \"all\" entry first.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} The filter options.\n\t\t */\n\t\tkindOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: '', label: t('nextcloud-vue', 'All kinds') },\n\t\t\t\t...this.effectiveKinds.map((value) => ({ value, label: value })),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The kinds to offer, preferring what the ENGINE served.\n\t\t *\n\t\t * An app declares its kinds in the `store` block of its manifest, next\n\t\t * to the schema allowlist, and the engine returns them with the cards.\n\t\t * That is the single place an app says what its store sells; the\n\t\t * `kinds` PROP remains for a host rendering this component directly and\n\t\t * as the pre-response value, and DEFAULT_STORE_KINDS is the floor.\n\t\t *\n\t\t * A served EMPTY list means the app declared none, which is not the\n\t\t * same as not having asked yet — hence `servedKinds` starts null.\n\t\t *\n\t\t * @return {Array<string>} The kind filter values.\n\t\t */\n\t\teffectiveKinds() {\n\t\t\tif (Array.isArray(this.servedKinds) && this.servedKinds.length > 0) {\n\t\t\t\treturn this.servedKinds\n\t\t\t}\n\n\t\t\treturn this.kinds\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, shown whenever the remote list is not carrying\n\t\t * the page. Not a network call, and not a Store on their own.\n\t\t *\n\t\t * 🔴 AN EMPTY ANSWER IS STILL AN EMPTY PAGE. This used to hide the\n\t\t * built-ins whenever the engine answered at all, checking only\n\t\t * `not_configured` and the error outcomes. A federated store that\n\t\t * answers `ok` with zero cards — a configured registry that has\n\t\t * published nothing yet — then rendered a heading, a search box and\n\t\t * nothing else, while the app shipped four items it could have shown.\n\t\t * Measured on decidiq, whose store returns `ok` with 0 cards and 4\n\t\t * built-ins declared.\n\t\t *\n\t\t * ADR-080 Decision 4 is explicit that a surface which goes blank was\n\t\t * never a store, so the test is what the reader can SEE, not which\n\t\t * outcome the engine reported.\n\t\t *\n\t\t * @return {Array<object>} The built-in items to render.\n\t\t */\n\t\tvisibleBuiltIn() {\n\t\t\tif (this.offline === false && this.unreachable === false && this.cards.length > 0) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn this.effectiveBuiltIn\n\t\t},\n\n\t\t/**\n\t\t * The app's own items, preferring what the ENGINE served over the page\n\t\t * config.\n\t\t *\n\t\t * An app declares these ONCE, in its `store` manifest block beside the\n\t\t * allowlist, which is where the schema documents them. The `builtIn`\n\t\t * PROP is a second place to write the same list, and an app that\n\t\t * declared them only in the manifest used to render nothing: the\n\t\t * engine parsed them and never sent them.\n\t\t *\n\t\t * Mirrors `effectiveKinds` exactly, for the same reason.\n\t\t *\n\t\t * @return {Array<object>} The items to render.\n\t\t */\n\t\teffectiveBuiltIn() {\n\t\t\tif (Array.isArray(this.servedBuiltIn) && this.servedBuiltIn.length > 0) {\n\t\t\t\treturn this.servedBuiltIn\n\t\t\t}\n\n\t\t\treturn this.builtIn\n\t\t},\n\n\t\t/**\n\t\t * Heading above the app's own items.\n\t\t *\n\t\t * @return {string} The built-in section heading.\n\t\t */\n\t\tbuiltInHeading() {\n\t\t\treturn this.resolvedApp\n\t\t\t\t? t('nextcloud-vue', 'Included with {app}', { app: this.resolvedApp })\n\t\t\t\t: t('nextcloud-vue', 'Included with this app')\n\t\t},\n\n\t\t/**\n\t\t * The no-registry message, which names the built-in fallback only when\n\t\t * there is one. Promising templates below when the list is empty is\n\t\t * how an empty page reads as a broken one.\n\t\t *\n\t\t * @return {string} The not-configured note.\n\t\t */\n\t\tnotConfiguredMessage() {\n\t\t\tconst base = t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'No store registry is configured, so nothing was requested from the network. An administrator can connect one under Administration settings.',\n\t\t\t)\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\n\t\t/**\n\t\t * The registry-error message, on the same rule as above.\n\t\t *\n\t\t * @return {string} The unreachable note.\n\t\t */\n\t\tunreachableMessage() {\n\t\t\tconst base = t('nextcloud-vue', 'The store registry did not answer.')\n\n\t\t\treturn this.builtIn.length > 0\n\t\t\t\t? `${base} ${t('nextcloud-vue', 'The items below ship with this app.')}`\n\t\t\t\t: base\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.search()\n\t},\n\n\t/**\n\t * Cancel a pending debounced search.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\tif (this.searchTimer !== null) {\n\t\t\tclearTimeout(this.searchTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Debounce the search so typing does not fire one remote request per\n\t\t * keystroke against somebody else's registry.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonSearchInput() {\n\t\t\tif (this.searchTimer !== null) {\n\t\t\t\tclearTimeout(this.searchTimer)\n\t\t\t}\n\n\t\t\tthis.searchTimer = setTimeout(() => {\n\t\t\t\tthis.search()\n\t\t\t}, 400)\n\t\t},\n\n\t\t/**\n\t\t * Select a kind filter and re-search.\n\t\t *\n\t\t * @param {string} value The kind, or an empty string for all kinds.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tselectKind(value) {\n\t\t\tthis.kind = value\n\t\t\tthis.search()\n\t\t},\n\n\t\t/**\n\t\t * Fetch the current page of store cards.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search() {\n\t\t\t// An unconfigured `app` cannot address any endpoint. Reporting\n\t\t\t// not_configured is the honest outcome and costs no request; the\n\t\t\t// alternative is a fetch to /apps//api/... which 404s and reads to\n\t\t\t// the user as a registry that is down.\n\t\t\tif (this.resolvedApp === '') {\n\t\t\t\tthis.outcome = 'not_configured'\n\t\t\t\tthis.cards = []\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst params = new URLSearchParams()\n\t\t\t\tif (this.query) {\n\t\t\t\t\tparams.set('q', this.query)\n\t\t\t\t}\n\t\t\t\tif (this.kind) {\n\t\t\t\t\tparams.set('kind', this.kind)\n\t\t\t\t}\n\n\t\t\t\tconst suffix = params.toString() ? `?${params.toString()}` : ''\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items${suffix}`),\n\t\t\t\t\t{\n\t\t\t\t\t\theaders: { requesttoken: window.OC?.requestToken },\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tthis.outcome = body.outcome ?? 'store_invalid_response'\n\t\t\t\tthis.cards = Array.isArray(body.cards) ? body.cards : []\n\t\t\t\t// Present on every arm the engine answers, including\n\t\t\t\t// not_configured and store_unreachable, so the filters survive\n\t\t\t\t// a registry that is down.\n\t\t\t\tthis.servedKinds = Array.isArray(body.kinds) ? body.kinds : null\n\t\t\t\t// Same contract as `kinds`: present on every arm the engine\n\t\t\t\t// answers, so an app's own items survive a registry that is\n\t\t\t\t// down — which is exactly when they are all there is to show.\n\t\t\t\tthis.servedBuiltIn = Array.isArray(body.builtIn) ? body.builtIn : null\n\t\t\t} catch {\n\t\t\t\tthis.outcome = 'store_unreachable'\n\t\t\t\tthis.cards = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Install one item, and report per component.\n\t\t *\n\t\t * A partial install is a real outcome rather than a failure: an item\n\t\t * carrying both configuration and records installs the configuration\n\t\t * and names the records it refused.\n\t\t *\n\t\t * @param {{slug: string, title: string}} card The card to install.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync install(card) {\n\t\t\tthis.installing = card.slug\n\t\t\tthis.report = null\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\tgenerateUrl(`/apps/${this.resolvedApp}/api/store/items/${encodeURIComponent(card.slug)}/install`),\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\trequesttoken: window.OC?.requestToken,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tconst body = await response.json()\n\n\t\t\t\tif (response.ok !== true) {\n\t\t\t\t\tshowError(body.message\n\t\t\t\t\t\t?? t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst refused = (body.components ?? []).filter((c) => c.status !== 'installed')\n\t\t\t\tif (refused.length === 0) {\n\t\t\t\t\tshowSuccess(t('nextcloud-vue', 'Installed.'))\n\t\t\t\t\tthis.report = null\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.report = {\n\t\t\t\t\ttype: body.success === true ? 'info' : 'warning',\n\t\t\t\t\tmessage: t(\n\t\t\t\t\t\t'nextcloud-vue',\n\t\t\t\t\t\t'Some parts were not installed: {schemas}',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschemas: refused.map((c) => c.schema).join(', '),\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tshowError(t('nextcloud-vue', 'The item could not be installed.'))\n\t\t\t} finally {\n\t\t\t\tthis.installing = null\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-store-page {\n\tpadding: 16px;\n\tmax-width: 1200px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-store-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-store-page__heading {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 0 0 4px;\n}\n\n.cn-store-page__title {\n\tmargin: 0;\n}\n\n.cn-store-page__intro {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0 0 16px;\n}\n\n.cn-store-page__controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 12px;\n\talign-items: flex-end;\n\tmargin-bottom: 16px;\n}\n\n.cn-store-page__kinds {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n.cn-store-page__loading {\n\tmargin: 32px auto;\n}\n\n.cn-store-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n\tgap: 16px;\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 16px 0 0;\n}\n\n.cn-store-page__card {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-main-background);\n\tpadding: 16px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-store-page__card-title {\n\tmargin: 0;\n\tfont-size: 1rem;\n}\n\n.cn-store-page__card-kind {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n.cn-store-page__card-description {\n\tmargin: 0;\n\tflex: 1;\n}\n\n.cn-store-page__card-footer {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-store-page__card-version {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n/* Who published a configuration set matters more than its version does, since\n installing one applies somebody else's schemas and flows to this instance. */\n.cn-store-page__card-publisher {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n\tmargin-inline-end: auto;\n}\n\n.cn-store-page__builtin-title {\n\tmargin: 24px 0 0;\n}\n</style>\n"],"names":["_createElementVNode","_toDisplayString","_createBlock","_createVNode","_createElementBlock","_Fragment","_renderList","_createTextVNode","_openBlock"],"mappings":";;;AAiGM,EAAA,KAAK,EAAC,eAAe;AAAC,EAAA,aAAA,EAAA,YAAA;;AACrB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAC,GAAA;;AAEN,MAAA,UAAA,GAAA,EAAA,KAAI,EAAA,sBAAA,GAAA;;;AAWqB,EAAA,KAAK,EAAA,sBAAuB;;AAKtD,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAgB,GAAA;;;;OAgDrB,EAAC,qBAAA;AACJ,EAAA,aAAS,EAAC,eAAe;;AAEtB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,2BAAA,GAAA;;;AAGD,EAAA,KAAA,EAAA,0BAAA;;AAGA,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,iCAAE,GAAA;AAGN,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAAO,GAAA;;;AACR,EAAA,KAAA,EAAA,+BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AA/EZ,IAAAA,kBAAA,CAgBE,OAhBF,UAgBE,EAAA;yBAJK,iBAAA,EAAA;QAVLA,kBAEK,CAAA,IAAA,EAFL,UAEK,EAAAC,eAAA,CADD,QAAa,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;SAGV,QAAW,CAAA,WAAA;0BADlBC,WAMW,CAAA,mBAAA,EAAA;;AAJV,cAAA,OAAO,EAAC,WAAC;;AAER,cAAA,EAAE,EAAE,QAAM,CAAA,aAAA;;+BACX,MAA2B;gDAAxB,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,SAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;OAGG,QAAmB,CAAA,mBAAA;4EACzB,QAAA,CAAA,mBAAA,CAAA,EAAA,CAAA,YAAA;;;uBAyBE,iBAAA,EAAA;MApBLC,WAKsC,CAAA,sBAAA,EAAA;oBAJ5B,KAAM,CAAA,KAAA;;gDAAN,KAAM,CAAA,KAAA,IAAA,MAAA,CAAA,CAAA;UAIK,QAAa,CAAA,aAAA;;AAHhC,QAAA,KAAK,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,kBAAA,CAAA;AACR,QAAA,QAAQ,EAAE,QAAO,CAAA,OAAA;AAClB,QAAA,aAAI,EAAA,cAAA;;MAGJH,kBAYK,CAAA,KAAA,EAAA;AAXL,QAAA,KAAK,EAAC,sBAAsB;AAC3B,QAAA,IAAG,EAAC,OAAO;AACX,QAAA,YAAE,EAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,gBAAA,CAAA;;0BACHI,kBAOS,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CANS,QAAW,CAAA,WAAA,EAAA,CAArB,MAAM,KAAA;+BADdJ,WAOS,CAAA,mBAAA,EAAA;YALP,GAAG,EAAE,MAAM,CAAC;AACZ,YAAA,OAAK,EAAA,MAAA,CAAA,KAAA,KAAA,KAAA,CAAA,IAAA,GAAA,SAAA,GAAA,WAAA;AACL,YAAA,QAAQ,EAAE,QAAO,CAAA,OAAA;AACjB,YAAA,OAAK,EAAE,MAAA,KAAA,QAAA,CAAA,UAAU,CAAA,MAAA,CAAA,KAAA,CAAA,CAAA;;6BAClB,MAAM;AAAH,cAAAK,eAAA,CAAAN,eAAA,CAAA,MAAG,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;KAKY,KAAO,CAAA,OAAA;sBAAxBC,WAAmC,CAAA,wBAAA,EAAA;;AAAR,UAAA,IAAI,EAAE,EAAE;AAAA,UAAA,KAAA,EAAA,wBAAA;;SAG3B,QAAM,CAAA,OAAA;mCAIL,CAAA,qBAAA,EAAA;;AAHP,YAAA,IAAA,EAAA,MAAM;yBACA,EAAC,sBAAsB;;mCAC9B;8CAAD,QAAC,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA;;;;WAID,QAAA,CAAA,WAAA;qCAIS,CAAA,qBAAA,EAAA;;;AAFZ,cAAA,aAAE,EAAA,mBAAA;;+BACF,MAAwB;gDAArB,QAAkB,CAAA,kBAAA,CAAA,EAAA,CAAA,YAAA;;;;AAIjB,YAAA,CAAA,KAAA,CAAA,KAAA,CAAA,MAAA,KAAA,CAAA;4BADHA,WAKG,CAAA,yBAAA,EAAA;;AAHH,gBAAA,IAAI,EAAE,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,6BAAA,CAAA;gBACP,WAAE;IAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,iDAAA,CAAA;;;;MAKI,KAAO,CAAA,OAAA,IAAI,KAAK,CAAA,KAAA,CAAC,MAAM,GAAA,CAAA;AAD5B,SAAAM,SAAA,EAAA,EAAAJ,kBAAA,OAAA;4BAICA,8CAAgB,KAAK,CAAA,KAAA,EAAA,CAAb,IAAI,KAAA;iCAAZA;cAAqB,GAAA,EAAA,IAAA,CAAA,IAAA;AAAA,cAAA,KAAA,EAAA,qBAAA;;cACtBJ,kBAEE,CAAA,IAAA,EAFF,UAEE,EADCC,eAAA,CAAA,IAAA,CAAA,KAAA,IAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;AAED,cAAA,CAAA,IAAA,CAAA,QAAA,IAAA,IAAA,CAAA,IAAA;gCAAHG,kBAEI,CAAA,GAAA,EAFJ,WAEI;;AACJ,cAAAJ,kBAAA,CAEI,GAFJ,EAAA,WAEI,EADAC,eAAA,CAAA,IAAI,CAAC,WAAQ,CAAA,EAAA,CAAA,YAAA;AAEjB,cAAAD,kBAAA,QAAA;AACU,gBAAA,CAAA,IAAA,CAAA,SAAA;AAAT,qBAAAQ,SAAA,EAAA,EAAAJ,kBAAA,OAAA,EAAA,aACCH,eAAA,CAAA,IAAA,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}