@conduction/nextcloud-vue 2.61.0 → 2.63.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 (186) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +394 -27
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -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":"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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,10 @@
1
+ import script from './CnTimelineWidget.vue2.js';
2
+ import { render } from './CnTimelineWidget.vue3.js';
3
+
4
+ script.render = render;
5
+ script.__scopeId = "data-v-621fcccf";
6
+ script.__file = "src/components/CnTimelineWidget/CnTimelineWidget.vue";
7
+ const __sfc_main = script;
8
+
9
+ export { __sfc_main as default };
10
+ //# sourceMappingURL=CnTimelineWidget.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnTimelineWidget.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
@@ -0,0 +1,337 @@
1
+ import { translate, getLanguage } from '@nextcloud/l10n';
2
+ import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue';
3
+ import { fieldEvents, relatedEvents, auditEvents, timelineEntryEvents, sortEvents } from '../../utils/timelineEvents.js';
4
+
5
+ /**
6
+ * CnTimelineWidget — an object's dated events in time order (`timeline`
7
+ * widget type for detail pages).
8
+ *
9
+ * Generalises dossiq's case timeline. It merges up to four sources, all
10
+ * optional, into one list:
11
+ *
12
+ * - `fields`: the object's own date properties, `[{ field, label }]`. A
13
+ * dotted path reaches metadata, such as `@self.created`.
14
+ * - `related`: related objects, `[{ register?, schema, field, dateField?,
15
+ * label, titleField? }]`. Rows of `schema` whose `field` holds this object's
16
+ * id become events at their `dateField` (default `@self.created`).
17
+ * - `auditTrail: true`: the object's audit trail, one event per change.
18
+ * - `timeline: true`: OpenRegister's timeline of notes, calls and messages.
19
+ *
20
+ * Oldest first by default (`order: 'desc'` flips it). A moment after now is
21
+ * marked upcoming, in words as well as style.
22
+ *
23
+ * ```json
24
+ * { "type": "timeline", "content": {
25
+ * "title": "Timeline",
26
+ * "fields": [
27
+ * { "field": "@self.created", "label": "Booking created" },
28
+ * { "field": "depositClearedAt", "label": "Deposit cleared" },
29
+ * { "field": "confirmationSentAt", "label": "Confirmation mail sent" },
30
+ * { "field": "startsAt", "label": "Starts" },
31
+ * { "field": "endsAt", "label": "Ends" }
32
+ * ] } }
33
+ * ```
34
+ *
35
+ * The object comes from the detail page's context, like `audit-trail`:
36
+ * explicit props win, then the injects, then `content`.
37
+ *
38
+ * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order
39
+ */
40
+ var script = {
41
+ name: 'CnTimelineWidget',
42
+
43
+ components: {
44
+ NcLoadingIcon,
45
+ NcNoteCard,
46
+ },
47
+
48
+ inject: {
49
+ /** Detail-page object context from CnDetailPage (a ref or a raw bag). */
50
+ cnObjectContext: { default: null },
51
+ /** Detail object context holder from CnPageRenderer (`{ value: { objectData, ... } }`). */
52
+ cnDetailObjectContext: { default: null },
53
+ /** Consumer translation function; labels are authored in English. */
54
+ cnTranslate: { default: () => (key) => key },
55
+ },
56
+
57
+ props: {
58
+ /** Register slug; object context spread as props by CnWidgetGrid. */
59
+ register: { type: String, default: '' },
60
+ /**
61
+ * Schema slug, or the schema object the detail context supplies.
62
+ *
63
+ * @type {string|object}
64
+ */
65
+ schema: { type: [String, Object], default: '' },
66
+ /** The object's id. */
67
+ objectId: { type: String, default: '' },
68
+ /** The object itself, when the host already has it. */
69
+ object: { type: Object, default: null },
70
+ /** Card title override. */
71
+ title: { type: String, default: '' },
72
+ /** Stored widget content: `{ title, fields, related, auditTrail, timeline, order }`. */
73
+ content: { type: Object, default: () => ({}) },
74
+ /** OpenRegister API base. */
75
+ apiBase: { type: String, default: '/apps/openregister/api' },
76
+ },
77
+
78
+ data() {
79
+ return {
80
+ loading: false,
81
+ relatedRows: {},
82
+ auditEntries: [],
83
+ timelineEntries: [],
84
+ failedSources: [],
85
+ fetchedObject: null,
86
+ loadSeq: 0,
87
+ }
88
+ },
89
+
90
+ computed: {
91
+ ctx() {
92
+ const raw = this.cnObjectContext;
93
+ const inj = raw && (raw.value !== undefined ? raw.value : raw);
94
+ const holder = this.cnDetailObjectContext && this.cnDetailObjectContext.value;
95
+ return inj || holder || {}
96
+ },
97
+
98
+ resolvedObjectId() {
99
+ return this.objectId || this.ctx.objectId || this.content.objectId || ''
100
+ },
101
+
102
+ resolvedRegister() {
103
+ return this.register || this.ctx.register || this.content.register || ''
104
+ },
105
+
106
+ resolvedSchema() {
107
+ const s = this.schema || this.ctx.schema || this.content.schema || '';
108
+ return typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''
109
+ },
110
+
111
+ /**
112
+ * The object: explicit prop, then the page context, then a fetch of
113
+ * our own when only the id is known.
114
+ *
115
+ * @return {object|null}
116
+ */
117
+ resolvedObject() {
118
+ return this.object || this.ctx.object || this.ctx.objectData || this.fetchedObject || null
119
+ },
120
+
121
+ resolvedTitle() {
122
+ const raw = this.title || this.content.title || '';
123
+ return raw ? this.translate(raw) : ''
124
+ },
125
+
126
+ fieldsConfig() {
127
+ return Array.isArray(this.content.fields) ? this.content.fields : []
128
+ },
129
+
130
+ relatedConfig() {
131
+ return Array.isArray(this.content.related) ? this.content.related.filter((r) => r && r.schema && r.field) : []
132
+ },
133
+
134
+ /**
135
+ * Every event from every source, in time order.
136
+ *
137
+ * @return {Array<object>}
138
+ */
139
+ events() {
140
+ const now = new Date();
141
+ const all = [
142
+ ...fieldEvents(this.resolvedObject || {}, this.fieldsConfig, now),
143
+ ...this.relatedConfig.flatMap((cfg, i) => relatedEvents(this.relatedRows[i] || [], cfg, now)),
144
+ ...auditEvents(this.auditEntries, this.describeAudit, now),
145
+ ...timelineEntryEvents(this.timelineEntries, translate('nextcloud-vue', 'Note'), now),
146
+ ];
147
+ return sortEvents(all, this.content.order === 'desc' ? 'desc' : 'asc')
148
+ },
149
+
150
+ /**
151
+ * A signature of what to fetch, so a change refetches once.
152
+ *
153
+ * @return {string}
154
+ */
155
+ loadKey() {
156
+ return JSON.stringify({
157
+ id: this.resolvedObjectId,
158
+ register: this.resolvedRegister,
159
+ schema: this.resolvedSchema,
160
+ related: this.relatedConfig,
161
+ audit: this.content.auditTrail === true,
162
+ timeline: this.content.timeline === true,
163
+ needObject: this.fieldsConfig.length > 0 && !(this.object || this.ctx.object || this.ctx.objectData),
164
+ })
165
+ },
166
+
167
+ failedText() {
168
+ return translate('nextcloud-vue', 'Part of the timeline could not load: {parts}.', {
169
+ parts: this.failedSources.map((s) => this.sourceName(s)).join(', '),
170
+ })
171
+ },
172
+
173
+ loadingLabel() {
174
+ return translate('nextcloud-vue', 'Loading…')
175
+ },
176
+
177
+ emptyLabel() {
178
+ return translate('nextcloud-vue', 'Nothing has happened yet.')
179
+ },
180
+
181
+ upcomingLabel() {
182
+ return translate('nextcloud-vue', 'Upcoming')
183
+ },
184
+ },
185
+
186
+ watch: {
187
+ loadKey: {
188
+ immediate: true,
189
+ handler() {
190
+ this.load();
191
+ },
192
+ },
193
+ },
194
+
195
+ methods: {
196
+ translate(text) {
197
+ return text ? this.cnTranslate(text) : ''
198
+ },
199
+
200
+ sourceName(source) {
201
+ if (source === 'audit') {
202
+ return translate('nextcloud-vue', 'changes')
203
+ }
204
+ if (source === 'timeline') {
205
+ return translate('nextcloud-vue', 'notes and messages')
206
+ }
207
+ if (source === 'object') {
208
+ return translate('nextcloud-vue', 'dates')
209
+ }
210
+ return translate('nextcloud-vue', 'related items')
211
+ },
212
+
213
+ /**
214
+ * The label of an audit event, from its action and who did it.
215
+ *
216
+ * @param {string} action The audit action (create, update, delete).
217
+ * @param {string} actor Who did it.
218
+ * @return {string}
219
+ */
220
+ describeAudit(action, actor) {
221
+ const known = {
222
+ create: translate('nextcloud-vue', 'Created'),
223
+ update: translate('nextcloud-vue', 'Updated'),
224
+ delete: translate('nextcloud-vue', 'Deleted'),
225
+ };
226
+ const what = known[action] || action || translate('nextcloud-vue', 'Changed');
227
+ return actor ? translate('nextcloud-vue', '{action} by {actor}', { action: what, actor }) : what
228
+ },
229
+
230
+ /**
231
+ * Show a moment in the reader's language: a date for a calendar day,
232
+ * date and time otherwise.
233
+ *
234
+ * @param {object} event The event.
235
+ * @return {string}
236
+ */
237
+ formatMoment(event) {
238
+ const locale = (typeof getLanguage === 'function' && getLanguage()) || undefined;
239
+ const options = event.dateOnly
240
+ ? { dateStyle: 'medium' }
241
+ : { dateStyle: 'medium', timeStyle: 'short' };
242
+ try {
243
+ return new Intl.DateTimeFormat(locale, options).format(event.at)
244
+ } catch {
245
+ return event.at.toLocaleString()
246
+ }
247
+ },
248
+
249
+ /**
250
+ * Fetch every configured source. Each one fails on its own, so one
251
+ * unreachable source never hides the others.
252
+ *
253
+ * @return {Promise<void>}
254
+ * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order
255
+ */
256
+ async load() {
257
+ const id = this.resolvedObjectId;
258
+ const register = this.resolvedRegister;
259
+ const schema = this.resolvedSchema;
260
+ const seq = ++this.loadSeq;
261
+ if (!id || !register || !schema) {
262
+ return
263
+ }
264
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
265
+ import('@nextcloud/axios'),
266
+ import('@nextcloud/router'),
267
+ ]);
268
+ const base = `${this.apiBase}/objects/${register}/${schema}/${id}`;
269
+ const failed = [];
270
+ const jobs = [];
271
+
272
+ if (this.fieldsConfig.length > 0 && !(this.object || this.ctx.object || this.ctx.objectData)) {
273
+ jobs.push(axios.get(generateUrl(base))
274
+ .then((r) => {
275
+ this.fetchedObject = (r && r.data) || null;
276
+ })
277
+ .catch(() => failed.push('object')));
278
+ }
279
+
280
+ const rows = {};
281
+ this.relatedConfig.forEach((cfg, i) => {
282
+ const url = generateUrl(`${this.apiBase}/objects/${cfg.register || register}/${cfg.schema}`);
283
+ jobs.push(axios.get(url, { params: { [cfg.field]: id, _limit: cfg.limit || 50 } })
284
+ .then((r) => {
285
+ const data = r && r.data;
286
+ rows[i] = (data && (data.results || data)) || [];
287
+ })
288
+ .catch(() => {
289
+ rows[i] = [];
290
+ if (!failed.includes('related')) {
291
+ failed.push('related');
292
+ }
293
+ }));
294
+ });
295
+
296
+ if (this.content.auditTrail === true) {
297
+ jobs.push(axios.get(generateUrl(`${base}/audit-trail`), { params: { limit: this.content.auditLimit || 50 } })
298
+ .then((r) => {
299
+ const data = r && r.data;
300
+ this.auditEntries = (data && (data.results || data)) || [];
301
+ })
302
+ .catch(() => {
303
+ this.auditEntries = [];
304
+ failed.push('audit');
305
+ }));
306
+ }
307
+
308
+ if (this.content.timeline === true) {
309
+ jobs.push(axios.get(generateUrl(`${base}/timeline`))
310
+ .then((r) => {
311
+ const data = r && r.data;
312
+ this.timelineEntries = (data && (data.results || data)) || [];
313
+ })
314
+ .catch(() => {
315
+ this.timelineEntries = [];
316
+ failed.push('timeline');
317
+ }));
318
+ }
319
+
320
+ if (jobs.length === 0) {
321
+ this.failedSources = [];
322
+ return
323
+ }
324
+ this.loading = true;
325
+ await Promise.all(jobs);
326
+ if (seq !== this.loadSeq) {
327
+ return
328
+ }
329
+ this.relatedRows = rows;
330
+ this.failedSources = failed;
331
+ this.loading = false;
332
+ },
333
+ },
334
+ };
335
+
336
+ export { script as default };
337
+ //# sourceMappingURL=CnTimelineWidget.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnTimelineWidget.vue2.js","sources":["../../../../src/components/CnTimelineWidget/CnTimelineWidget.vue"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<section class=\"cn-timeline-widget\" data-testid=\"cn-timeline-widget\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<h3 v-if=\"resolvedTitle\" class=\"cn-timeline-widget__title\">\n\t\t\t{{ resolvedTitle }}\n\t\t</h3>\n\n\t\t<div v-if=\"loading && events.length === 0\" class=\"cn-timeline-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"24\" aria-hidden=\"true\" />\n\t\t\t<span>{{ loadingLabel }}</span>\n\t\t</div>\n\n\t\t<template v-else>\n\t\t\t<!-- A failed read is not an empty history: say which part is missing. -->\n\t\t\t<NcNoteCard v-if=\"failedSources.length > 0\" type=\"warning\" data-testid=\"cn-timeline-widget-error\">\n\t\t\t\t{{ failedText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<p v-if=\"events.length === 0\" class=\"cn-timeline-widget__empty\" data-testid=\"cn-timeline-widget-empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</p>\n\n\t\t\t<ol v-else class=\"cn-timeline-widget__list\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"event in events\"\n\t\t\t\t\t:key=\"event.id\"\n\t\t\t\t\tclass=\"cn-timeline-widget__event\"\n\t\t\t\t\t:class=\"{ 'cn-timeline-widget__event--upcoming': event.upcoming }\"\n\t\t\t\t\t:data-source=\"event.source\"\n\t\t\t\t\tdata-testid=\"cn-timeline-widget-event\">\n\t\t\t\t\t<span class=\"cn-timeline-widget__dot\" aria-hidden=\"true\" />\n\t\t\t\t\t<div class=\"cn-timeline-widget__body\">\n\t\t\t\t\t\t<time class=\"cn-timeline-widget__when\" :datetime=\"event.at.toISOString()\">\n\t\t\t\t\t\t\t{{ formatMoment(event) }}\n\t\t\t\t\t\t</time>\n\t\t\t\t\t\t<span v-if=\"event.upcoming\" class=\"cn-timeline-widget__badge\">\n\t\t\t\t\t\t\t{{ upcomingLabel }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<p class=\"cn-timeline-widget__label\">\n\t\t\t\t\t\t\t{{ translate(event.label) }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p v-if=\"event.detail\" class=\"cn-timeline-widget__detail\">\n\t\t\t\t\t\t\t{{ event.detail }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</li>\n\t\t\t</ol>\n\t\t</template>\n\t</section>\n</template>\n\n<script>\nimport { getLanguage, translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport { auditEvents, fieldEvents, relatedEvents, sortEvents, timelineEntryEvents } from '../../utils/timelineEvents.js'\n\n/**\n * CnTimelineWidget — an object's dated events in time order (`timeline`\n * widget type for detail pages).\n *\n * Generalises dossiq's case timeline. It merges up to four sources, all\n * optional, into one list:\n *\n * - `fields`: the object's own date properties, `[{ field, label }]`. A\n * dotted path reaches metadata, such as `@self.created`.\n * - `related`: related objects, `[{ register?, schema, field, dateField?,\n * label, titleField? }]`. Rows of `schema` whose `field` holds this object's\n * id become events at their `dateField` (default `@self.created`).\n * - `auditTrail: true`: the object's audit trail, one event per change.\n * - `timeline: true`: OpenRegister's timeline of notes, calls and messages.\n *\n * Oldest first by default (`order: 'desc'` flips it). A moment after now is\n * marked upcoming, in words as well as style.\n *\n * ```json\n * { \"type\": \"timeline\", \"content\": {\n * \"title\": \"Timeline\",\n * \"fields\": [\n * { \"field\": \"@self.created\", \"label\": \"Booking created\" },\n * { \"field\": \"depositClearedAt\", \"label\": \"Deposit cleared\" },\n * { \"field\": \"confirmationSentAt\", \"label\": \"Confirmation mail sent\" },\n * { \"field\": \"startsAt\", \"label\": \"Starts\" },\n * { \"field\": \"endsAt\", \"label\": \"Ends\" }\n * ] } }\n * ```\n *\n * The object comes from the detail page's context, like `audit-trail`:\n * explicit props win, then the injects, then `content`.\n *\n * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order\n */\nexport default {\n\tname: 'CnTimelineWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tinject: {\n\t\t/** Detail-page object context from CnDetailPage (a ref or a raw bag). */\n\t\tcnObjectContext: { default: null },\n\t\t/** Detail object context holder from CnPageRenderer (`{ value: { objectData, ... } }`). */\n\t\tcnDetailObjectContext: { default: null },\n\t\t/** Consumer translation function; labels are authored in English. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Register slug; object context spread as props by CnWidgetGrid. */\n\t\tregister: { type: String, default: '' },\n\t\t/**\n\t\t * Schema slug, or the schema object the detail context supplies.\n\t\t *\n\t\t * @type {string|object}\n\t\t */\n\t\tschema: { type: [String, Object], default: '' },\n\t\t/** The object's id. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** The object itself, when the host already has it. */\n\t\tobject: { type: Object, default: null },\n\t\t/** Card title override. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Stored widget content: `{ title, fields, related, auditTrail, timeline, order }`. */\n\t\tcontent: { type: Object, default: () => ({}) },\n\t\t/** OpenRegister API base. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: false,\n\t\t\trelatedRows: {},\n\t\t\tauditEntries: [],\n\t\t\ttimelineEntries: [],\n\t\t\tfailedSources: [],\n\t\t\tfetchedObject: null,\n\t\t\tloadSeq: 0,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tctx() {\n\t\t\tconst raw = this.cnObjectContext\n\t\t\tconst inj = raw && (raw.value !== undefined ? raw.value : raw)\n\t\t\tconst holder = this.cnDetailObjectContext && this.cnDetailObjectContext.value\n\t\t\treturn inj || holder || {}\n\t\t},\n\n\t\tresolvedObjectId() {\n\t\t\treturn this.objectId || this.ctx.objectId || this.content.objectId || ''\n\t\t},\n\n\t\tresolvedRegister() {\n\t\t\treturn this.register || this.ctx.register || this.content.register || ''\n\t\t},\n\n\t\tresolvedSchema() {\n\t\t\tconst s = this.schema || this.ctx.schema || this.content.schema || ''\n\t\t\treturn typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * The object: explicit prop, then the page context, then a fetch of\n\t\t * our own when only the id is known.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedObject() {\n\t\t\treturn this.object || this.ctx.object || this.ctx.objectData || this.fetchedObject || null\n\t\t},\n\n\t\tresolvedTitle() {\n\t\t\tconst raw = this.title || this.content.title || ''\n\t\t\treturn raw ? this.translate(raw) : ''\n\t\t},\n\n\t\tfieldsConfig() {\n\t\t\treturn Array.isArray(this.content.fields) ? this.content.fields : []\n\t\t},\n\n\t\trelatedConfig() {\n\t\t\treturn Array.isArray(this.content.related) ? this.content.related.filter((r) => r && r.schema && r.field) : []\n\t\t},\n\n\t\t/**\n\t\t * Every event from every source, in time order.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tevents() {\n\t\t\tconst now = new Date()\n\t\t\tconst all = [\n\t\t\t\t...fieldEvents(this.resolvedObject || {}, this.fieldsConfig, now),\n\t\t\t\t...this.relatedConfig.flatMap((cfg, i) => relatedEvents(this.relatedRows[i] || [], cfg, now)),\n\t\t\t\t...auditEvents(this.auditEntries, this.describeAudit, now),\n\t\t\t\t...timelineEntryEvents(this.timelineEntries, t('nextcloud-vue', 'Note'), now),\n\t\t\t]\n\t\t\treturn sortEvents(all, this.content.order === 'desc' ? 'desc' : 'asc')\n\t\t},\n\n\t\t/**\n\t\t * A signature of what to fetch, so a change refetches once.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tloadKey() {\n\t\t\treturn JSON.stringify({\n\t\t\t\tid: this.resolvedObjectId,\n\t\t\t\tregister: this.resolvedRegister,\n\t\t\t\tschema: this.resolvedSchema,\n\t\t\t\trelated: this.relatedConfig,\n\t\t\t\taudit: this.content.auditTrail === true,\n\t\t\t\ttimeline: this.content.timeline === true,\n\t\t\t\tneedObject: this.fieldsConfig.length > 0 && !(this.object || this.ctx.object || this.ctx.objectData),\n\t\t\t})\n\t\t},\n\n\t\tfailedText() {\n\t\t\treturn t('nextcloud-vue', 'Part of the timeline could not load: {parts}.', {\n\t\t\t\tparts: this.failedSources.map((s) => this.sourceName(s)).join(', '),\n\t\t\t})\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\n\t\temptyLabel() {\n\t\t\treturn t('nextcloud-vue', 'Nothing has happened yet.')\n\t\t},\n\n\t\tupcomingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Upcoming')\n\t\t},\n\t},\n\n\twatch: {\n\t\tloadKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\ttranslate(text) {\n\t\t\treturn text ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\tsourceName(source) {\n\t\t\tif (source === 'audit') {\n\t\t\t\treturn t('nextcloud-vue', 'changes')\n\t\t\t}\n\t\t\tif (source === 'timeline') {\n\t\t\t\treturn t('nextcloud-vue', 'notes and messages')\n\t\t\t}\n\t\t\tif (source === 'object') {\n\t\t\t\treturn t('nextcloud-vue', 'dates')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'related items')\n\t\t},\n\n\t\t/**\n\t\t * The label of an audit event, from its action and who did it.\n\t\t *\n\t\t * @param {string} action The audit action (create, update, delete).\n\t\t * @param {string} actor Who did it.\n\t\t * @return {string}\n\t\t */\n\t\tdescribeAudit(action, actor) {\n\t\t\tconst known = {\n\t\t\t\tcreate: t('nextcloud-vue', 'Created'),\n\t\t\t\tupdate: t('nextcloud-vue', 'Updated'),\n\t\t\t\tdelete: t('nextcloud-vue', 'Deleted'),\n\t\t\t}\n\t\t\tconst what = known[action] || action || t('nextcloud-vue', 'Changed')\n\t\t\treturn actor ? t('nextcloud-vue', '{action} by {actor}', { action: what, actor }) : what\n\t\t},\n\n\t\t/**\n\t\t * Show a moment in the reader's language: a date for a calendar day,\n\t\t * date and time otherwise.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @return {string}\n\t\t */\n\t\tformatMoment(event) {\n\t\t\tconst locale = (typeof getLanguage === 'function' && getLanguage()) || undefined\n\t\t\tconst options = event.dateOnly\n\t\t\t\t? { dateStyle: 'medium' }\n\t\t\t\t: { dateStyle: 'medium', timeStyle: 'short' }\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale, options).format(event.at)\n\t\t\t} catch {\n\t\t\t\treturn event.at.toLocaleString()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch every configured source. Each one fails on its own, so one\n\t\t * unreachable source never hides the others.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order\n\t\t */\n\t\tasync load() {\n\t\t\tconst id = this.resolvedObjectId\n\t\t\tconst register = this.resolvedRegister\n\t\t\tconst schema = this.resolvedSchema\n\t\t\tconst seq = ++this.loadSeq\n\t\t\tif (!id || !register || !schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst base = `${this.apiBase}/objects/${register}/${schema}/${id}`\n\t\t\tconst failed = []\n\t\t\tconst jobs = []\n\n\t\t\tif (this.fieldsConfig.length > 0 && !(this.object || this.ctx.object || this.ctx.objectData)) {\n\t\t\t\tjobs.push(axios.get(generateUrl(base))\n\t\t\t\t\t.then((r) => {\n\t\t\t\t\t\tthis.fetchedObject = (r && r.data) || null\n\t\t\t\t\t})\n\t\t\t\t\t.catch(() => failed.push('object')))\n\t\t\t}\n\n\t\t\tconst rows = {}\n\t\t\tthis.relatedConfig.forEach((cfg, i) => {\n\t\t\t\tconst url = generateUrl(`${this.apiBase}/objects/${cfg.register || register}/${cfg.schema}`)\n\t\t\t\tjobs.push(axios.get(url, { params: { [cfg.field]: id, _limit: cfg.limit || 50 } })\n\t\t\t\t\t.then((r) => {\n\t\t\t\t\t\tconst data = r && r.data\n\t\t\t\t\t\trows[i] = (data && (data.results || data)) || []\n\t\t\t\t\t})\n\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\trows[i] = []\n\t\t\t\t\t\tif (!failed.includes('related')) {\n\t\t\t\t\t\t\tfailed.push('related')\n\t\t\t\t\t\t}\n\t\t\t\t\t}))\n\t\t\t})\n\n\t\t\tif (this.content.auditTrail === true) {\n\t\t\t\tjobs.push(axios.get(generateUrl(`${base}/audit-trail`), { params: { limit: this.content.auditLimit || 50 } })\n\t\t\t\t\t.then((r) => {\n\t\t\t\t\t\tconst data = r && r.data\n\t\t\t\t\t\tthis.auditEntries = (data && (data.results || data)) || []\n\t\t\t\t\t})\n\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\tthis.auditEntries = []\n\t\t\t\t\t\tfailed.push('audit')\n\t\t\t\t\t}))\n\t\t\t}\n\n\t\t\tif (this.content.timeline === true) {\n\t\t\t\tjobs.push(axios.get(generateUrl(`${base}/timeline`))\n\t\t\t\t\t.then((r) => {\n\t\t\t\t\t\tconst data = r && r.data\n\t\t\t\t\t\tthis.timelineEntries = (data && (data.results || data)) || []\n\t\t\t\t\t})\n\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\tthis.timelineEntries = []\n\t\t\t\t\t\tfailed.push('timeline')\n\t\t\t\t\t}))\n\t\t\t}\n\n\t\t\tif (jobs.length === 0) {\n\t\t\t\tthis.failedSources = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tawait Promise.all(jobs)\n\t\t\tif (seq !== this.loadSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.relatedRows = rows\n\t\t\tthis.failedSources = failed\n\t\t\tthis.loading = false\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-timeline-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-timeline-widget__title {\n\tmargin: 0;\n\tfont-size: var(--font-size-large, 1.1em);\n}\n\n.cn-timeline-widget__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-widget__empty {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-widget__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-timeline-widget__event {\n\tposition: relative;\n\tdisplay: flex;\n\tgap: 12px;\n\tpadding-block-end: 12px;\n}\n\n/* The line between dots: drawn from each dot down to the next event. */\n.cn-timeline-widget__event:not(:last-child)::before {\n\tcontent: '';\n\tposition: absolute;\n\tinset-block: 14px 0;\n\tinset-inline-start: 5px;\n\tborder-inline-start: 2px solid var(--color-border-dark);\n}\n\n.cn-timeline-widget__dot {\n\tflex: 0 0 auto;\n\twidth: 12px;\n\theight: 12px;\n\tmargin-block-start: 4px;\n\tborder-radius: 50%;\n\tbackground-color: var(--color-primary-element);\n}\n\n/* Upcoming: a hollow dot, plus the word \"Upcoming\" beside the date. */\n.cn-timeline-widget__event--upcoming .cn-timeline-widget__dot {\n\tbox-sizing: border-box;\n\tborder: 2px solid var(--color-primary-element);\n\tbackground-color: var(--color-main-background);\n}\n\n.cn-timeline-widget__body {\n\tmin-width: 0;\n}\n\n.cn-timeline-widget__when {\n\tfont-size: var(--font-size-small, 13px);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-widget__badge {\n\tmargin-inline-start: 8px;\n\tfont-size: var(--font-size-small, 13px);\n\tfont-weight: bold;\n\tcolor: var(--color-main-text);\n}\n\n.cn-timeline-widget__label {\n\tmargin: 0;\n\tfont-weight: bold;\n}\n\n.cn-timeline-widget__detail {\n\tmargin: 0;\n\toverflow-wrap: anywhere;\n}\n</style>\n"],"names":["t"],"mappings":";;;;AAyDA;;;AAGC;;AAEA;AACA;;;AAGA;AACA;;;AAGA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;AAIA;AACA;AACA;AACA;AACA;AACA;;;AAGA;AACA;;AAED,aAAe;CACd,IAAI,EAAE,kBAAkB;;CAExB,UAAU,EAAE;EACX,aAAa;EACb,UAAU;EACV;;AAED,CAAA,MAAM,EAAE;;AAEP,EAAA,eAAe,EAAE,EAAE,OAAO,EAAE,IAAG,EAAG;AAClC;AACA,EAAA,qBAAqB,EAAE,EAAE,OAAO,EAAE,MAAM;;AAExC,EAAA,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,KAAK,KAAK;EAC5C;;AAED,CAAA,KAAK,EAAE;;AAEN,EAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACvC;;AAEC;AACA;;AAED,EAAA,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI;AAC/C;AACA,EAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACvC;AACA,EAAA,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;AAEvC,EAAA,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAC,EAAG;AACpC;AACA,EAAA,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG;;EAE9C,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,wBAAuB,EAAG;EAC5D;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;GACN,OAAO,EAAE,KAAK;AACd,GAAA,WAAW,EAAE,EAAE;AACf,GAAA,YAAY,EAAE,EAAE;AAChB,GAAA,eAAe,EAAE,EAAE;AACnB,GAAA,aAAa,EAAE,EAAE;AACjB,GAAA,aAAa,EAAE,IAAI;AACnB,GAAA,OAAO,EAAE,CAAC;AACX,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;EACT,GAAG,GAAG;GACL,MAAM,GAAE,GAAI,IAAI,CAAC,gBAAc;GAC/B,MAAM,GAAE,GAAI,GAAE,KAAM,GAAG,CAAC,KAAM,KAAI,SAAQ,GAAI,GAAG,CAAC,KAAM,GAAE,GAAG,EAAA;AAC7D,GAAA,MAAM,MAAO,GAAE,IAAI,CAAC,qBAAsB,IAAG,IAAI,CAAC,qBAAqB,CAAC,MAAI;AAC5E,GAAA,OAAO,GAAI,IAAG,MAAO,IAAG,EAAC;GACzB;;AAED,EAAA,gBAAgB,GAAG;AAClB,GAAA,OAAO,IAAI,CAAC,QAAO,IAAK,IAAI,CAAC,GAAG,CAAC,QAAO,IAAK,IAAI,CAAC,OAAO,CAAC,QAAS,IAAG,EAAC;GACvE;;AAED,EAAA,gBAAgB,GAAG;AAClB,GAAA,OAAO,IAAI,CAAC,QAAO,IAAK,IAAI,CAAC,GAAG,CAAC,QAAO,IAAK,IAAI,CAAC,OAAO,CAAC,QAAS,IAAG,EAAC;GACvE;;AAED,EAAA,cAAc,GAAG;GAChB,MAAM,CAAA,GAAI,IAAI,CAAC,MAAO,IAAG,IAAI,CAAC,GAAG,CAAC,MAAO,IAAG,IAAI,CAAC,OAAO,CAAC,MAAK,IAAK,GAAC;GACpE,OAAO,OAAO,CAAA,KAAM,QAAO,GAAI,CAAA,GAAI,CAAC,MAAM,CAAC,CAAC,IAAK,IAAG,CAAC,CAAC,IAAG,IAAK,CAAC,CAAC,EAAE,CAAC,KAAK,EAAC;GACzE;;AAED;;;AAGC;AACA;;AAED,EAAA,cAAc,GAAG;AAChB,GAAA,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,MAAK,IAAK,IAAI,CAAC,GAAG,CAAC,UAAS,IAAK,IAAI,CAAC,aAAY,IAAK,IAAG;GACzF;;AAED,EAAA,aAAa,GAAG;AACf,GAAA,MAAM,MAAM,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,KAAM,IAAG,GAAC;AACjD,GAAA,OAAO,GAAE,GAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAA,GAAI,EAAC;GACpC;;AAED,EAAA,YAAY,GAAG;AACd,GAAA,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAE,GAAE,IAAI,CAAC,OAAO,CAAC,MAAO,GAAE,EAAC;GACnE;;AAED,EAAA,aAAa,GAAG;GACf,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAE,GAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAE,IAAG,CAAC,CAAC,MAAK,IAAK,CAAC,CAAC,KAAK,CAAE,GAAE,EAAC;GAC7G;;AAED;AACC;AACA;;;AAGD,EAAA,MAAM,GAAG;AACR,GAAA,MAAM,GAAE,GAAI,IAAI,IAAI,GAAC;AACrB,GAAA,MAAM,MAAM;IACX,GAAG,WAAW,CAAC,IAAI,CAAC,cAAa,IAAK,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC;AACjE,IAAA,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAE,IAAG,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAC7F,GAAG,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;AAC1D,IAAA,GAAG,mBAAmB,CAAC,IAAI,CAAC,eAAe,EAAEA,SAAC,CAAC,eAAe,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC;AAC9E,KAAA;GACA,OAAO,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,KAAM,KAAI,MAAK,GAAI,MAAO,GAAE,KAAK,CAAA;GACrE;;AAED;;AAEC;AACA;;AAED,EAAA,OAAO,GAAG;AACT,GAAA,OAAO,IAAI,CAAC,SAAS,CAAC;IACrB,EAAE,EAAE,IAAI,CAAC,gBAAgB;AACzB,IAAA,QAAQ,EAAE,IAAI,CAAC,gBAAgB;IAC/B,MAAM,EAAE,IAAI,CAAC,cAAc;IAC3B,OAAO,EAAE,IAAI,CAAC,aAAa;AAC3B,IAAA,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,UAAS,KAAM,IAAI;AACvC,IAAA,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAO,KAAM,IAAI;AACxC,IAAA,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,MAAO,GAAE,KAAK,EAAE,IAAI,CAAC,MAAO,IAAG,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;IACpG,CAAA;GACD;;EAED,UAAU,GAAG;AACZ,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,+CAA+C,EAAE;AAC1E,IAAA,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IACnE,CAAA;GACD;;AAED,EAAA,YAAY,GAAG;AACd,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,UAAU,CAAA;GACpC;;EAED,UAAU,GAAG;GACZ,OAAOA,SAAC,CAAC,eAAe,EAAE,2BAA2B,CAAA;GACrD;;AAED,EAAA,aAAa,GAAG;AACf,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,UAAU,CAAA;GACpC;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,OAAO,EAAE;AACR,GAAA,SAAS,EAAE,IAAI;AACf,GAAA,OAAO,GAAG;IACT,IAAI,CAAC,IAAI,GAAC;IACV;GACD;EACD;;AAED,CAAA,OAAO,EAAE;AACR,EAAA,SAAS,CAAC,IAAI,EAAE;AACf,GAAA,OAAO,IAAG,GAAI,IAAI,CAAC,WAAW,CAAC,IAAI,CAAA,GAAI,EAAC;GACxC;;AAED,EAAA,UAAU,CAAC,MAAM,EAAE;AAClB,GAAA,IAAI,MAAO,KAAI,OAAO,EAAE;AACvB,IAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,SAAS,CAAA;AACpC,IAAA;GACA,IAAI,WAAW,UAAU,EAAE;AAC1B,IAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,oBAAoB,CAAA;AAC/C,IAAA;AACA,GAAA,IAAI,MAAO,KAAI,QAAQ,EAAE;AACxB,IAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,OAAO,CAAA;AAClC,IAAA;AACA,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,eAAe,CAAA;GACzC;;AAED;;AAEC;;;AAGA;;EAED,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE;GAC5B,MAAM,KAAI,GAAI;AACb,IAAA,MAAM,EAAEA,SAAC,CAAC,eAAe,EAAE,SAAS,CAAC;AACrC,IAAA,MAAM,EAAEA,SAAC,CAAC,eAAe,EAAE,SAAS,CAAC;AACrC,IAAA,MAAM,EAAEA,SAAC,CAAC,eAAe,EAAE,SAAS,CAAC;AACtC,KAAA;GACA,MAAM,IAAK,GAAE,KAAK,CAAC,MAAM,CAAE,IAAG,UAAUA,SAAC,CAAC,eAAe,EAAE,SAAS,EAAA;AACpE,GAAA,OAAO,QAAQA,SAAC,CAAC,eAAe,EAAE,qBAAqB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAA,GAAI,IAAG;GACvF;;AAED;;AAEC;AACA;;AAEA;;AAED,EAAA,YAAY,CAAC,KAAK,EAAE;AACnB,GAAA,MAAM,MAAK,GAAI,CAAC,OAAO,WAAY,KAAI,UAAS,IAAK,WAAW,EAAE,KAAK,UAAQ;GAC/E,MAAM,OAAQ,GAAE,KAAK,CAAC,QAAO;AAC5B,MAAE,EAAE,SAAS,EAAE,QAAS,EAAA;AACxB,MAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAQ,GAAA;AAC7C,GAAA,IAAI;IACH,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;AAChE,IAAA,CAAE,MAAM;AACP,IAAA,OAAO,KAAK,CAAC,EAAE,CAAC,cAAc,EAAC;AAChC,IAAA;GACA;;AAED;;AAEC;AACA;;AAEA;;EAED,MAAM,IAAI,GAAG;GACZ,MAAM,EAAG,GAAE,IAAI,CAAC,iBAAe;AAC/B,GAAA,MAAM,QAAS,GAAE,IAAI,CAAC,iBAAe;AACrC,GAAA,MAAM,MAAK,GAAI,IAAI,CAAC,eAAa;GACjC,MAAM,GAAE,GAAI,EAAE,IAAI,CAAC,QAAM;GACzB,IAAI,CAAC,EAAC,IAAK,CAAC,QAAS,IAAG,CAAC,MAAM,EAAE;IAChC,MAAK;AACN,IAAA;GACA,MAAM,CAAC,EAAE,OAAO,EAAE,KAAI,EAAG,EAAE,EAAE,WAAU,EAAG,CAAE,GAAE,MAAM,OAAO,CAAC,GAAG,CAAC;IAC/D,OAAO,kBAAkB,CAAC;IAC1B,OAAO,mBAAmB,CAAC;IAC3B,EAAA;AACD,GAAA,MAAM,IAAG,GAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,EAAA;GACjE,MAAM,MAAO,GAAE,GAAC;GAChB,MAAM,IAAK,GAAE,GAAC;;AAEd,GAAA,IAAI,IAAI,CAAC,YAAY,CAAC,MAAO,GAAE,KAAK,EAAE,IAAI,CAAC,MAAK,IAAK,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE;AAC7F,IAAA,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;MACnC,IAAI,CAAC,CAAC,CAAC,KAAK;AACZ,MAAA,IAAI,CAAC,aAAc,GAAE,CAAC,CAAA,IAAK,CAAC,CAAC,IAAI,KAAK,KAAG;MACzC,CAAA;AACD,MAAC,KAAK,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAA;AACrC,IAAA;;GAEA,MAAM,IAAK,GAAE,GAAC;AACd,GAAA,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK;AACtC,IAAA,MAAM,MAAM,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,YAAY,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,EAAA;AAC3F,IAAA,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,KAAI,IAAK,EAAC,IAAK,CAAA;MAC/E,IAAI,CAAC,CAAC,CAAC,KAAK;AACZ,MAAA,MAAM,IAAG,GAAI,CAAA,IAAK,CAAC,CAAC,KAAG;AACvB,MAAA,IAAI,CAAC,CAAC,IAAI,CAAC,IAAK,KAAI,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,KAAK,GAAC;MAC/C,CAAA;MACA,KAAK,CAAC,MAAM;AACZ,MAAA,IAAI,CAAC,CAAC,CAAA,GAAI,GAAC;AACX,MAAA,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE;AAChC,OAAA,MAAM,CAAC,IAAI,CAAC,SAAS,EAAA;AACtB,OAAA;AACD,MAAC,CAAC,EAAA;IACH,EAAA;;AAED,GAAA,IAAI,IAAI,CAAC,OAAO,CAAC,UAAS,KAAM,IAAI,EAAE;IACrC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,UAAS,IAAK,EAAG,EAAA,EAAG,CAAA;MAC1G,IAAI,CAAC,CAAC,CAAC,KAAK;AACZ,MAAA,MAAM,IAAG,GAAI,CAAA,IAAK,CAAC,CAAC,KAAG;MACvB,IAAI,CAAC,YAAW,GAAI,CAAC,IAAK,KAAI,IAAI,CAAC,WAAW,IAAI,CAAC,KAAK,GAAC;MACzD,CAAA;MACA,KAAK,CAAC,MAAM;AACZ,MAAA,IAAI,CAAC,YAAW,GAAI,GAAC;AACrB,MAAA,MAAM,CAAC,IAAI,CAAC,OAAO,EAAA;AACpB,MAAC,CAAC,EAAA;AACJ,IAAA;;AAEA,GAAA,IAAI,IAAI,CAAC,OAAO,CAAC,QAAO,KAAM,IAAI,EAAE;IACnC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAA;MACjD,IAAI,CAAC,CAAC,CAAC,KAAK;AACZ,MAAA,MAAM,IAAG,GAAI,CAAA,IAAK,CAAC,CAAC,KAAG;MACvB,IAAI,CAAC,kBAAkB,CAAC,IAAG,KAAM,IAAI,CAAC,OAAQ,IAAG,IAAI,CAAC,KAAK,GAAC;MAC5D,CAAA;MACA,KAAK,CAAC,MAAM;MACZ,IAAI,CAAC,eAAc,GAAI,GAAC;MACxB,MAAM,CAAC,IAAI,CAAC,UAAU,EAAA;AACvB,MAAC,CAAC,EAAA;AACJ,IAAA;;AAEA,GAAA,IAAI,IAAI,CAAC,MAAK,KAAM,CAAC,EAAE;AACtB,IAAA,IAAI,CAAC,aAAY,GAAI,GAAC;IACtB,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;AAClB,GAAA,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAA;GACtB,IAAI,QAAQ,IAAI,CAAC,OAAO,EAAE;IACzB,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,cAAc,KAAG;GACtB,IAAI,CAAC,aAAY,GAAI,OAAK;AAC1B,GAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;GACnB;EACD;AACF;;;;"}