@conduction/nextcloud-vue 2.105.0 → 2.106.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 (153) hide show
  1. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  2. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +18 -0
  3. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -1
  5. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +18 -0
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +1 -1
  10. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +53 -4
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +9 -0
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js.map +1 -0
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +24 -0
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -0
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +49 -0
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -0
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +36 -18
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +17 -38
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +53 -2
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +119 -111
  32. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  34. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +62 -1
  35. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +159 -50
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +121 -98
  40. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  42. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +24 -0
  43. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +20 -4
  45. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -4
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +112 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +100 -44
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  53. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +67 -4
  54. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  56. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +295 -12
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +267 -144
  61. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +4 -0
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +3 -1
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +93 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +322 -246
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +122 -7
  74. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +189 -93
  76. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  78. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  81. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +42 -0
  82. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +38 -19
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  85. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js +9 -0
  86. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue.js.map +1 -0
  87. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js +258 -0
  88. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue2.js.map +1 -0
  89. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js +69 -0
  90. package/dist/esm/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue3.js.map +1 -0
  91. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +21 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +21 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +803 -453
  101. package/dist/esm/utils/dueText.js +18 -3
  102. package/dist/esm/utils/dueText.js.map +1 -1
  103. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  104. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  105. package/dist/esm/utils/sortOrder.js +11 -1
  106. package/dist/esm/utils/sortOrder.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +8281 -6350
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +803 -453
  112. package/l10n/en.json +21 -1
  113. package/l10n/nl.json +21 -1
  114. package/package.json +1 -1
  115. package/src/components/CnCardGrid/CnCardGrid.md +2 -0
  116. package/src/components/CnCardGrid/CnCardGrid.vue +20 -0
  117. package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -1
  118. package/src/components/CnDetailPage/CnDetailPage.md +2 -2
  119. package/src/components/CnDetailPage/CnDetailPage.vue +53 -4
  120. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +60 -0
  121. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +36 -29
  122. package/src/components/CnFactsWidget/CnFactsWidget.vue +4 -0
  123. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +70 -4
  124. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +62 -1
  125. package/src/components/CnIndexPage/CnIndexPage.md +7 -5
  126. package/src/components/CnIndexPage/CnIndexPage.vue +187 -58
  127. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +36 -1
  128. package/src/components/CnIndexPage/useSelfFetchList.js +22 -4
  129. package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
  130. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +148 -1
  131. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +68 -5
  132. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  133. package/src/components/CnObjectCard/CnObjectCard.vue +354 -13
  134. package/src/components/CnObjectSidebar/CnFilesTab.vue +6 -0
  135. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +190 -4
  136. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +209 -9
  137. package/src/components/CnTabs/CnTabs.vue +11 -0
  138. package/src/components/CnTabsWidget/CnTabsWidget.vue +91 -18
  139. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.md +11 -0
  140. package/src/components/CnTeamWorkloadWidget/CnTeamWorkloadWidget.vue +382 -0
  141. package/src/components/CnTeamWorkloadWidget/index.js +14 -0
  142. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
  143. package/src/components/index.js +1 -0
  144. package/src/css/index-page.css +14 -2
  145. package/src/css/look-board-detail.css +48 -4
  146. package/src/css/look-board-index.css +64 -2
  147. package/src/css/look-board.css +25 -2
  148. package/src/index.js +1 -1
  149. package/src/schemas/app-manifest-v2.schema.json +75 -6
  150. package/src/utils/dueText.js +18 -3
  151. package/src/utils/libraryWidgetKeys.js +1 -0
  152. package/src/utils/sortOrder.js +10 -0
  153. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"CnNcWidgetWidget.vue3.js","sources":["../../../../src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue?vue&type=template&id=f553a198&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-nc-widget-widget\" :class=\"{ 'cn-nc-widget-widget--horizontal': displayMode === 'horizontal' }\">\n\t\t<header v-if=\"showsHeader\" class=\"cn-nc-widget-widget__header\" data-testid=\"cn-nc-widget-header\">\n\t\t\t<img\n\t\t\t\tv-if=\"widgetIconUrl\"\n\t\t\t\tclass=\"cn-nc-widget-widget__header-icon\"\n\t\t\t\t:src=\"widgetIconUrl\"\n\t\t\t\talt=\"\">\n\t\t\t<span class=\"cn-nc-widget-widget__header-title\">{{ widgetTitle }}</span>\n\t\t</header>\n\n\t\t<!--\n\t\t\tNative callback container — always present but hidden until the\n\t\t\tOCA.Dashboard callback mounts into it. Keeping it mounted avoids a\n\t\t\tflicker when the native path wins after the API list has painted.\n\n\t\t\tThe `--horizontal` reskin class lives on the ROOT element, not here:\n\t\t\tnative widget callbacks mount via `new Vue(...).$mount(el)`, which\n\t\t\tREPLACES this element in the live DOM with the callback's own root\n\t\t\tnode — any class we put directly on it is discarded along with the\n\t\t\telement itself, so it can never appear in the rendered page.\n\t\t-->\n\t\t<div\n\t\t\tv-show=\"mode === 'native'\"\n\t\t\tref=\"nativeContainer\"\n\t\t\tclass=\"cn-nc-widget-widget__native\" />\n\n\t\t<div\n\t\t\tv-if=\"mode !== 'native'\"\n\t\t\tclass=\"cn-nc-widget-widget__body\"\n\t\t\t:class=\"bodyClass\">\n\t\t\t<div v-if=\"loading\" class=\"cn-nc-widget-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading…') }}\n\t\t\t</div>\n\n\t\t\t<div v-else-if=\"!widgetId\" class=\"cn-nc-widget-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No widget selected') }}\n\t\t\t</div>\n\n\t\t\t<div v-else-if=\"unsupported\" class=\"cn-nc-widget-widget__state\" data-testid=\"nc-widget-unsupported\">\n\t\t\t\t{{ t('nextcloud-vue', 'This widget can only be shown on the Nextcloud dashboard itself.') }}\n\t\t\t</div>\n\n\t\t\t<div v-else-if=\"items.length === 0\" class=\"cn-nc-widget-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No items available') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"(item, idx) in shownItems\"\n\t\t\t\t\t:key=\"item.sinceId || item.id || idx\"\n\t\t\t\t\tclass=\"cn-nc-widget-widget__item\"\n\t\t\t\t\t:class=\"itemClass\"\n\t\t\t\t\t:href=\"item.link || '#'\"\n\t\t\t\t\t:title=\"item.title\">\n\t\t\t\t\t<span class=\"cn-nc-widget-widget__item-icon-wrap\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"item.iconUrl\"\n\t\t\t\t\t\t\tclass=\"cn-nc-widget-widget__item-icon\"\n\t\t\t\t\t\t\t:src=\"item.iconUrl\"\n\t\t\t\t\t\t\talt=\"\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"item.overlayIconUrl\"\n\t\t\t\t\t\t\tclass=\"cn-nc-widget-widget__item-overlay\"\n\t\t\t\t\t\t\t:src=\"item.overlayIconUrl\"\n\t\t\t\t\t\t\talt=\"\">\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-nc-widget-widget__item-text\">\n\t\t\t\t\t\t<span class=\"cn-nc-widget-widget__item-title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.subtitle\" class=\"cn-nc-widget-widget__item-subtitle\">{{ item.subtitle }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t\t<!-- With a `limit`, the rest is one link away (`showAllUrl`, else the widget's own page). -->\n\t\t\t\t<a\n\t\t\t\t\tv-if=\"showAllHref\"\n\t\t\t\t\tclass=\"cn-nc-widget-widget__show-all\"\n\t\t\t\t\tdata-testid=\"nc-widget-show-all\"\n\t\t\t\t\t:href=\"showAllHref\">\n\t\t\t\t\t{{ showAllLabel }}\n\t\t\t\t</a>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateOcsUrl } from '@nextcloud/router'\n\nconst API_ITEM_LIMIT = 7\n\n/**\n * Read the global `OCA.Dashboard` registry without assuming it exists.\n *\n * @return {object|null} the `OCA.Dashboard` object, or `null` when absent.\n */\nfunction getDashboardGlobal() {\n\tif (typeof window === 'undefined') {\n\t\treturn null\n\t}\n\tconst oca = window.OCA\n\treturn oca && oca.Dashboard ? oca.Dashboard : null\n}\n\n/**\n * CnNcWidgetWidget — proxies any native Nextcloud Dashboard widget into a\n * dashboard grid cell.\n *\n * Two-mode rendering (decoupling seam — reads NC directly, no app bridge):\n * - **native** — when the widget bundle has registered a callback on the\n * global `OCA.Dashboard` (`getWidget(id)` / `widgets[id]`), the callback is\n * handed the render container and paints itself (native fast-path).\n * - **api** — otherwise the OCS Dashboard endpoint\n * `/apps/dashboard/api/v{1,2}/widget-items` is fetched directly via\n * `@nextcloud/axios` and a flat item list is rendered.\n *\n * The header title + icon come from the dashboard widget metadata\n * (`OCA.Dashboard` registry, falling back to the configured `widgetId`). When\n * no `widgetId` is configured the widget renders an empty state and never hits\n * the network.\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnNcWidgetWidget',\n\n\tinject: {\n\t\t/** Whether the hosting card draws a title row (CnWidgetWrapper). */\n\t\tcnWidgetTitled: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?}`.\n\t\t * `showHeader: false` drops the widget's own header (icon and title);\n\t\t * it is dropped too when the hosting card draws a title row.\n\t\t * `limit` is the most items shown (the list then comes from the items\n\t\t * API, so it fits its cell); `showAllUrl` / `showAllLabel` the link to\n\t\t * the rest (default the widget's own page and \"Show all\").\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** @type {'pending'|'native'|'api'} the resolved mounting mode. */\n\t\t\tmode: 'pending',\n\t\t\t/**\n\t\t\t * Whether the items API has no entry for this widget at all.\n\t\t\t *\n\t\t\t * Not the same as an empty list. A widget whose provider implements\n\t\t\t * only `IWidget` (no `IAPIWidget`/`IAPIWidgetV2`) declares\n\t\t\t * `itemApiVersions: []` and is simply ABSENT from the widget-items\n\t\t\t * response, while a widget with nothing to show right now comes\n\t\t\t * back as its own key holding an empty list. The Tasks app's widget\n\t\t\t * is the first kind: with no native callback registered on the page\n\t\t\t * it can never render a single item here, and saying \"No items\n\t\t\t * available\" under it told the reader their task list was empty.\n\t\t\t *\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tunsupported: false,\n\t\t\tloading: false,\n\t\t\titems: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The configured native widget id.\n\t\t *\n\t\t * @return {string} the widget id (empty when unset).\n\t\t */\n\t\twidgetId() {\n\t\t\treturn typeof (this.content && this.content.widgetId) === 'string' ? this.content.widgetId : ''\n\t\t},\n\n\t\t/**\n\t\t * The item list orientation.\n\t\t *\n\t\t * @return {string} `horizontal` or `vertical`.\n\t\t */\n\t\tdisplayMode() {\n\t\t\treturn (this.content && this.content.displayMode) === 'horizontal' ? 'horizontal' : 'vertical'\n\t\t},\n\n\t\t/**\n\t\t * The native widget metadata from the `OCA.Dashboard` global.\n\t\t *\n\t\t * @return {object|null} the widget metadata, or `null`.\n\t\t */\n\t\twidgetMeta() {\n\t\t\tconst dashboard = getDashboardGlobal()\n\t\t\tif (!dashboard || !this.widgetId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof dashboard.getWidget === 'function') {\n\t\t\t\treturn dashboard.getWidget(this.widgetId) || null\n\t\t\t}\n\t\t\tif (dashboard.widgets && typeof dashboard.widgets === 'object') {\n\t\t\t\treturn dashboard.widgets[this.widgetId] || null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Whether the widget draws its own header: not with\n\t\t * `content.showHeader: false`, nor inside a card that draws a title.\n\t\t *\n\t\t * @spec openspec/changes/nc-widget-header/specs/nc-widget-header/spec.md#requirement-a-nextcloud-widget-draws-no-second-heading\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsHeader() {\n\t\t\tif (this.content && this.content.showHeader === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !(typeof this.cnWidgetTitled === 'function' && this.cnWidgetTitled())\n\t\t},\n\n\t\t/**\n\t\t * The header title.\n\t\t *\n\t\t * @return {string} the title.\n\t\t */\n\t\twidgetTitle() {\n\t\t\treturn (this.widgetMeta && this.widgetMeta.title) || this.widgetId || t('nextcloud-vue', 'Widget')\n\t\t},\n\n\t\t/**\n\t\t * The header icon URL.\n\t\t *\n\t\t * @return {string} the icon URL (empty when none).\n\t\t */\n\t\twidgetIconUrl() {\n\t\t\treturn (this.widgetMeta && this.widgetMeta.iconUrl) || ''\n\t\t},\n\n\t\t/**\n\t\t * The most items the list shows (`content.limit`), or 0 for no limit.\n\t\t * A limit lets the widget fit a dashboard cell instead of scrolling\n\t\t * inside it.\n\t\t *\n\t\t * @spec openspec/changes/screens-nc-widget-limit/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-shows-at-most-its-limit\n\t\t * @return {number} the limit.\n\t\t */\n\t\titemLimit() {\n\t\t\tconst limit = Number(this.content && this.content.limit)\n\t\t\treturn Number.isInteger(limit) && limit > 0 ? limit : 0\n\t\t},\n\n\t\t/**\n\t\t * The items drawn: at most `itemLimit`.\n\t\t *\n\t\t * @return {object[]} the items.\n\t\t */\n\t\tshownItems() {\n\t\t\treturn this.itemLimit > 0 ? this.items.slice(0, this.itemLimit) : this.items\n\t\t},\n\n\t\t/**\n\t\t * Where \"Show all\" goes: `content.showAllUrl`, else the page the widget\n\t\t * names itself (its `url`). Only with a limit; '' for no link.\n\t\t *\n\t\t * @spec openspec/changes/screens-nc-widget-limit/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-shows-at-most-its-limit\n\t\t * @return {string} the URL.\n\t\t */\n\t\tshowAllHref() {\n\t\t\tif (this.itemLimit === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst own = this.content && typeof this.content.showAllUrl === 'string' ? this.content.showAllUrl : ''\n\t\t\treturn own || (this.widgetMeta && typeof this.widgetMeta.url === 'string' ? this.widgetMeta.url : '')\n\t\t},\n\n\t\t/**\n\t\t * The \"Show all\" text (`content.showAllLabel`, else the library's).\n\t\t *\n\t\t * @return {string} the label.\n\t\t */\n\t\tshowAllLabel() {\n\t\t\tconst label = this.content && this.content.showAllLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : t('nextcloud-vue', 'Show all')\n\t\t},\n\n\t\t/**\n\t\t * The body orientation CSS modifier class.\n\t\t *\n\t\t * @return {string} the body class.\n\t\t */\n\t\tbodyClass() {\n\t\t\treturn `cn-nc-widget-widget__body--${this.displayMode}`\n\t\t},\n\n\t\t/**\n\t\t * The item orientation CSS modifier class.\n\t\t *\n\t\t * @return {string} the item class.\n\t\t */\n\t\titemClass() {\n\t\t\treturn `cn-nc-widget-widget__item--${this.displayMode}`\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.tryMount()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Resolve the mounting mode. Prefers the native `OCA.Dashboard`\n\t\t * callback fast-path; otherwise falls back to the OCS item list.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttryMount() {\n\t\t\tif (!this.widgetId) {\n\t\t\t\tthis.mode = 'api'\n\t\t\t\tthis.items = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The native callback paints its own opaque markup, so `displayMode`\n\t\t\t// (bodyClass/itemClass below) has no effect on it — only the API\n\t\t\t// item list honours the vertical/horizontal setting. Horizontal mode\n\t\t\t// must therefore skip the native fast-path, or the \"Horizontal\n\t\t\t// (cards)\" option would silently do nothing for the (common) case\n\t\t\t// where the widget has a registered native callback.\n\t\t\t// A `limit` also takes the API list first: the native callback paints\n\t\t\t// as many items as it likes. Its fallback to native stays.\n\t\t\tif (this.displayMode !== 'horizontal' && this.itemLimit === 0) {\n\t\t\t\tconst callback = this.resolveNativeCallback()\n\t\t\t\tif (typeof callback === 'function') {\n\t\t\t\t\tthis.mountNative(callback)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.mode = 'api'\n\t\t\tthis.loadApiItems()\n\t\t},\n\n\t\t/**\n\t\t * Look up a native render callback on the `OCA.Dashboard` global.\n\t\t *\n\t\t * @return {((el: HTMLElement, ctx: {widget: object}) => unknown)|null} the callback, or `null` when none is registered.\n\t\t */\n\t\tresolveNativeCallback() {\n\t\t\tconst dashboard = getDashboardGlobal()\n\t\t\tif (!dashboard) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.widgetMeta && typeof this.widgetMeta.callback === 'function') {\n\t\t\t\treturn this.widgetMeta.callback\n\t\t\t}\n\t\t\tif (dashboard.callbacks && typeof dashboard.callbacks[this.widgetId] === 'function') {\n\t\t\t\treturn dashboard.callbacks[this.widgetId]\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Mount the widget natively by invoking its registered callback with\n\t\t * our render container.\n\t\t *\n\t\t * @param {(el: HTMLElement, ctx: {widget: object}) => unknown} callback the native render callback.\n\t\t * @return {void}\n\t\t */\n\t\tmountNative(callback) {\n\t\t\tthis.mode = 'native'\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst container = this.$refs.nativeContainer\n\t\t\t\tif (!container) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Native callback failed — fall back to the API list.\n\t\t\t\tconst fallback = () => {\n\t\t\t\t\tthis.mode = 'api'\n\t\t\t\t\tthis.loadApiItems()\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\t// NC dashboard widgets are registered as\n\t\t\t\t\t// `register(id, (el, { widget }) => …)` — the second argument\n\t\t\t\t\t// MUST be the `{ widget }` envelope, not the bare metadata, or\n\t\t\t\t\t// the widget destructures `widget` as undefined and throws on\n\t\t\t\t\t// first use (e.g. `widget.title`).\n\t\t\t\t\tconst result = callback(container, { widget: this.widgetMeta || {} })\n\t\t\t\t\t// Most widget callbacks are async; a synchronous try/catch\n\t\t\t\t\t// can't see a rejected promise, so wire the fallback to it too.\n\t\t\t\t\tif (result && typeof result.then === 'function') {\n\t\t\t\t\t\tresult.catch(fallback)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tfallback()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Fetch the API fallback list directly from the OCS Dashboard\n\t\t * endpoint. Malformed or failed responses collapse to an empty list\n\t\t * (the empty state) instead of throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadApiItems() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\t// The v2 widget-items API only returns an entry for widgets\n\t\t\t\t// whose server-side provider implements `IAPIWidgetV2` — plenty\n\t\t\t\t// of native dashboard widgets are JS-callback-only (`IWidget`)\n\t\t\t\t// and never appear in this response at all, which looks\n\t\t\t\t// identical to \"0 items\" from here. extractItems() tolerates\n\t\t\t\t// both the flat-array (older providers) and `{items}` (v2) shapes.\n\t\t\t\tconst url = generateOcsUrl('/apps/dashboard/api/v2/widget-items')\n\t\t\t\tconst response = await axios.get(url, {\n\t\t\t\t\tparams: { widgets: [this.widgetId], limit: this.itemLimit > 0 ? this.itemLimit : API_ITEM_LIMIT },\n\t\t\t\t})\n\t\t\t\tconst data = response && response.data && response.data.ocs ? response.data.ocs.data : null\n\t\t\t\tconst widgetData = data ? data[this.widgetId] : null\n\t\t\t\tthis.items = this.extractItems(widgetData)\n\t\t\t\t// Only a SUCCESSFUL response can say the widget is absent. A\n\t\t\t\t// failed request says nothing about the widget, so it keeps\n\t\t\t\t// the ordinary empty state rather than a claim about the app.\n\t\t\t\tthis.unsupported = data !== null && (widgetData === null || widgetData === undefined)\n\t\t\t} catch {\n\t\t\t\tthis.items = []\n\t\t\t\tthis.unsupported = false\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\n\t\t\t// Horizontal mode deliberately skips the native fast-path (see\n\t\t\t// tryMount()) to get the styled card layout. But when the API has\n\t\t\t// no entry for this widget at all — no `IAPIWidgetV2` support, not\n\t\t\t// \"no items right now\" — that would render a false empty state for\n\t\t\t// a widget that actually has content. Fall back to native\n\t\t\t// rendering rather than lying about there being nothing to show.\n\t\t\tif (this.items.length === 0) {\n\t\t\t\tconst callback = this.resolveNativeCallback()\n\t\t\t\tif (typeof callback === 'function') {\n\t\t\t\t\tthis.mountNative(callback)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Normalise a per-widget OCS payload to an items array, tolerating both\n\t\t * the flat array and the `{items}` envelope shapes.\n\t\t *\n\t\t * @param {unknown} widgetData the per-widget payload.\n\t\t * @return {object[]} the items array (possibly empty).\n\t\t */\n\t\textractItems(widgetData) {\n\t\t\tif (!widgetData) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (Array.isArray(widgetData)) {\n\t\t\t\treturn widgetData\n\t\t\t}\n\t\t\tif (Array.isArray(widgetData.items)) {\n\t\t\t\treturn widgetData.items\n\t\t\t}\n\t\t\tif (widgetData.items && typeof widgetData.items === 'object') {\n\t\t\t\treturn Object.values(widgetData.items)\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-nc-widget-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\twidth: 100%;\n\theight: 100%;\n\toverflow: hidden;\n}\n\n/*\n * Header + content match the native Nextcloud Dashboard panel (apps/dashboard)\n * exactly — this component's whole job is rendering NC dashboard widgets, so it\n * should look native by default: 16px header, 32px leading icon with a 16px\n * gap, a 20px/700 title, and the native panel content inset (16px sides +\n * bottom, flush to the header on top). Same design tokens as core; overridable\n * via the host's CnWidgetWrapper styleConfig / nldesign.\n */\n.cn-nc-widget-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 16px;\n\tpadding: 16px;\n\tfont-weight: 700;\n}\n\n.cn-nc-widget-widget__header-icon {\n\twidth: 32px;\n\theight: 32px;\n\tflex: 0 0 auto;\n}\n\n.cn-nc-widget-widget__header-title {\n\tflex: 1;\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 24px;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-nc-widget-widget__show-all {\n\talign-self: flex-start;\n\tpadding: 8px 0 0;\n\tcolor: var(--color-primary-element);\n\tfont-weight: 600;\n\ttext-decoration: none;\n}\n\n.cn-nc-widget-widget__show-all:hover,\n.cn-nc-widget-widget__show-all:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-nc-widget-widget__native {\n\tflex: 1;\n\toverflow: auto;\n\tpadding: 0 16px 16px;\n}\n\n/*\n * The native `OCA.Dashboard` callback paints its own markup we don't control\n * — for first-party widgets that's almost always `@nextcloud/vue`'s\n * `NcDashboardWidget` / `NcDashboardWidgetItem` (`.dashboard-widget` wrapping\n * a bare `<ul>`/`<li>` list, each item a `.item-list__entry`).\n *\n * Only the LIST CONTAINER is retiled into a grid here — `.item-list__entry`'s\n * own internal layout is left untouched. An earlier version of this override\n * also forced `.item-list__entry` from row to column, but its icon-overlay\n * badge (`.item-icon`, see @nextcloud/vue's NcDashboardWidgetItem CSS) is\n * positioned with negative margins calibrated for the row layout, so\n * flipping to column made items visually overlap. Tiling the `<li>`s into a\n * grid while each entry keeps its normal icon+text row reads as a grid of\n * card tiles without touching that internal math.\n *\n * Scoped from `.cn-nc-widget-widget--horizontal` on the ROOT element, not\n * `.cn-nc-widget-widget__native`: the callback mounts via `.$mount(el)`,\n * which replaces `nativeContainer` in the live DOM outright, so any class\n * placed directly on it never survives to be selectable.\n *\n * The 250px column floor matches `.item-list__entry`'s own natural minimum\n * width (avatar + padding + its un-truncatable-below-this content) — a grid\n * track narrower than that squeezes the entry below its real minimum and it\n * overflows into the neighbouring cell. `min-width: 0` on the `<li>` alone\n * isn't enough to shrink it further: the item's own internal flex layout\n * still claims that much room regardless of the grid track size, so the\n * track itself must not go below it.\n */\n.cn-nc-widget-widget--horizontal :deep(.dashboard-widget > ul) {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n\tgap: 8px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-nc-widget-widget--horizontal :deep(.dashboard-widget > ul > li) {\n\twidth: auto;\n\tmin-width: 0;\n}\n\n.cn-nc-widget-widget--horizontal :deep(.item-list__entry) {\n\theight: 100%;\n\tmargin: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n}\n\n.cn-nc-widget-widget__body {\n\tflex: 1;\n\toverflow: auto;\n\tpadding: 0 16px 16px;\n}\n\n.cn-nc-widget-widget__body--vertical {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-nc-widget-widget__body--horizontal {\n\tdisplay: flex;\n\tflex-direction: row;\n\tflex-wrap: wrap;\n\tgap: 12px;\n}\n\n.cn-nc-widget-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex: 1;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n\n.cn-nc-widget-widget__item {\n\tdisplay: flex;\n\ttext-decoration: none;\n\tcolor: var(--color-main-text);\n\tborder-radius: var(--border-radius);\n\tpadding: 4px;\n}\n\n.cn-nc-widget-widget__item:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-nc-widget-widget__item--vertical {\n\tflex-direction: row;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-nc-widget-widget__item--horizontal {\n\tflex-direction: column;\n\talign-items: center;\n\twidth: 120px;\n\ttext-align: center;\n}\n\n.cn-nc-widget-widget__item-icon-wrap {\n\twidth: 32px;\n\theight: 32px;\n\tflex: 0 0 32px;\n\tposition: relative;\n}\n\n.cn-nc-widget-widget__item-icon {\n\twidth: 32px;\n\theight: 32px;\n\tobject-fit: cover;\n\tborder-radius: 50%;\n}\n\n.cn-nc-widget-widget__item-overlay {\n\tposition: absolute;\n\tright: -2px;\n\tbottom: -2px;\n\twidth: 14px;\n\theight: 14px;\n}\n\n.cn-nc-widget-widget__item-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-width: 0;\n\tflex: 1;\n}\n\n.cn-nc-widget-widget__item-title {\n\tfont-weight: 600;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-nc-widget-widget__item-subtitle {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_openBlock","_createElementVNode","_toDisplayString","_withDirectives","_Fragment","_renderList"],"mappings":"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"}
1
+ {"version":3,"file":"CnNcWidgetWidget.vue3.js","sources":["../../../../src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue?vue&type=template&id=6d5d59a8&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-nc-widget-widget\" :class=\"{ 'cn-nc-widget-widget--horizontal': displayMode === 'horizontal' }\">\n\t\t<header v-if=\"showsHeader\" class=\"cn-nc-widget-widget__header\" data-testid=\"cn-nc-widget-header\">\n\t\t\t<img\n\t\t\t\tv-if=\"widgetIconUrl\"\n\t\t\t\tclass=\"cn-nc-widget-widget__header-icon\"\n\t\t\t\t:src=\"widgetIconUrl\"\n\t\t\t\talt=\"\">\n\t\t\t<span class=\"cn-nc-widget-widget__header-title\">{{ widgetTitle }}</span>\n\t\t</header>\n\n\t\t<!--\n\t\t\tNative callback container — always present but hidden until the\n\t\t\tOCA.Dashboard callback mounts into it. Keeping it mounted avoids a\n\t\t\tflicker when the native path wins after the API list has painted.\n\n\t\t\tThe `--horizontal` reskin class lives on the ROOT element, not here:\n\t\t\tnative widget callbacks mount via `new Vue(...).$mount(el)`, which\n\t\t\tREPLACES this element in the live DOM with the callback's own root\n\t\t\tnode — any class we put directly on it is discarded along with the\n\t\t\telement itself, so it can never appear in the rendered page.\n\t\t-->\n\t\t<div\n\t\t\tv-show=\"mode === 'native'\"\n\t\t\tref=\"nativeContainer\"\n\t\t\tclass=\"cn-nc-widget-widget__native\" />\n\n\t\t<div\n\t\t\tv-if=\"mode !== 'native'\"\n\t\t\tclass=\"cn-nc-widget-widget__body\"\n\t\t\t:class=\"bodyClass\">\n\t\t\t<div v-if=\"loading\" class=\"cn-nc-widget-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading…') }}\n\t\t\t</div>\n\n\t\t\t<div v-else-if=\"!widgetId\" class=\"cn-nc-widget-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No widget selected') }}\n\t\t\t</div>\n\n\t\t\t<div v-else-if=\"unsupported\" class=\"cn-nc-widget-widget__state\" data-testid=\"nc-widget-unsupported\">\n\t\t\t\t{{ t('nextcloud-vue', 'This widget can only be shown on the Nextcloud dashboard itself.') }}\n\t\t\t</div>\n\n\t\t\t<div v-else-if=\"shownItems.length === 0\" class=\"cn-nc-widget-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No items available') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"(item, idx) in shownItems\"\n\t\t\t\t\t:key=\"item.sinceId || item.id || idx\"\n\t\t\t\t\tclass=\"cn-nc-widget-widget__item\"\n\t\t\t\t\t:class=\"itemClass\"\n\t\t\t\t\t:href=\"item.link || '#'\"\n\t\t\t\t\t:title=\"item.title\">\n\t\t\t\t\t<span class=\"cn-nc-widget-widget__item-icon-wrap\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"item.iconUrl\"\n\t\t\t\t\t\t\tclass=\"cn-nc-widget-widget__item-icon\"\n\t\t\t\t\t\t\t:src=\"item.iconUrl\"\n\t\t\t\t\t\t\talt=\"\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"item.overlayIconUrl\"\n\t\t\t\t\t\t\tclass=\"cn-nc-widget-widget__item-overlay\"\n\t\t\t\t\t\t\t:src=\"item.overlayIconUrl\"\n\t\t\t\t\t\t\talt=\"\">\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-nc-widget-widget__item-text\">\n\t\t\t\t\t\t<span class=\"cn-nc-widget-widget__item-title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.subtitle\" class=\"cn-nc-widget-widget__item-subtitle\">{{ item.subtitle }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t\t<!-- With a `limit`, the rest is one link away (`showAllUrl`, else the widget's own page). -->\n\t\t\t\t<a\n\t\t\t\t\tv-if=\"showAllHref\"\n\t\t\t\t\tclass=\"cn-nc-widget-widget__show-all\"\n\t\t\t\t\tdata-testid=\"nc-widget-show-all\"\n\t\t\t\t\t:href=\"showAllHref\">\n\t\t\t\t\t{{ showAllLabel }}\n\t\t\t\t</a>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateOcsUrl } from '@nextcloud/router'\n\nconst API_ITEM_LIMIT = 7\n\n/**\n * Read the global `OCA.Dashboard` registry without assuming it exists.\n *\n * @return {object|null} the `OCA.Dashboard` object, or `null` when absent.\n */\nfunction getDashboardGlobal() {\n\tif (typeof window === 'undefined') {\n\t\treturn null\n\t}\n\tconst oca = window.OCA\n\treturn oca && oca.Dashboard ? oca.Dashboard : null\n}\n\n/**\n * CnNcWidgetWidget — proxies any native Nextcloud Dashboard widget into a\n * dashboard grid cell.\n *\n * Two-mode rendering (decoupling seam — reads NC directly, no app bridge):\n * - **native** — when the widget bundle has registered a callback on the\n * global `OCA.Dashboard` (`getWidget(id)` / `widgets[id]`), the callback is\n * handed the render container and paints itself (native fast-path).\n * - **api** — otherwise the OCS Dashboard endpoint\n * `/apps/dashboard/api/v{1,2}/widget-items` is fetched directly via\n * `@nextcloud/axios` and a flat item list is rendered.\n *\n * The header title + icon come from the dashboard widget metadata\n * (`OCA.Dashboard` registry, falling back to the configured `widgetId`). When\n * no `widgetId` is configured the widget renders an empty state and never hits\n * the network.\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnNcWidgetWidget',\n\n\tinject: {\n\t\t/** Whether the hosting card draws a title row (CnWidgetWrapper). */\n\t\tcnWidgetTitled: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?, excludeApps?, excludeTypes?}`.\n\t\t * `excludeApps` (`[\"systemtags\"]`) and `excludeTypes` leave out the\n\t\t * items of those apps or types (an item's own `app` / `type`, else the\n\t\t * app in its link or icon URL); the list then comes from the items API.\n\t\t * `showHeader: false` drops the widget's own header (icon and title);\n\t\t * it is dropped too when the hosting card draws a title row.\n\t\t * `limit` is the most items shown (the list then comes from the items\n\t\t * API, so it fits its cell); `showAllUrl` / `showAllLabel` the link to\n\t\t * the rest (default the widget's own page and \"Show all\").\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** @type {'pending'|'native'|'api'} the resolved mounting mode. */\n\t\t\tmode: 'pending',\n\t\t\t/**\n\t\t\t * Whether the items API has no entry for this widget at all.\n\t\t\t *\n\t\t\t * Not the same as an empty list. A widget whose provider implements\n\t\t\t * only `IWidget` (no `IAPIWidget`/`IAPIWidgetV2`) declares\n\t\t\t * `itemApiVersions: []` and is simply ABSENT from the widget-items\n\t\t\t * response, while a widget with nothing to show right now comes\n\t\t\t * back as its own key holding an empty list. The Tasks app's widget\n\t\t\t * is the first kind: with no native callback registered on the page\n\t\t\t * it can never render a single item here, and saying \"No items\n\t\t\t * available\" under it told the reader their task list was empty.\n\t\t\t *\n\t\t\t * @type {boolean}\n\t\t\t */\n\t\t\tunsupported: false,\n\t\t\tloading: false,\n\t\t\titems: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The configured native widget id.\n\t\t *\n\t\t * @return {string} the widget id (empty when unset).\n\t\t */\n\t\twidgetId() {\n\t\t\treturn typeof (this.content && this.content.widgetId) === 'string' ? this.content.widgetId : ''\n\t\t},\n\n\t\t/**\n\t\t * The item list orientation.\n\t\t *\n\t\t * @return {string} `horizontal` or `vertical`.\n\t\t */\n\t\tdisplayMode() {\n\t\t\treturn (this.content && this.content.displayMode) === 'horizontal' ? 'horizontal' : 'vertical'\n\t\t},\n\n\t\t/**\n\t\t * The native widget metadata from the `OCA.Dashboard` global.\n\t\t *\n\t\t * @return {object|null} the widget metadata, or `null`.\n\t\t */\n\t\twidgetMeta() {\n\t\t\tconst dashboard = getDashboardGlobal()\n\t\t\tif (!dashboard || !this.widgetId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof dashboard.getWidget === 'function') {\n\t\t\t\treturn dashboard.getWidget(this.widgetId) || null\n\t\t\t}\n\t\t\tif (dashboard.widgets && typeof dashboard.widgets === 'object') {\n\t\t\t\treturn dashboard.widgets[this.widgetId] || null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Whether the widget draws its own header: not with\n\t\t * `content.showHeader: false`, nor inside a card that draws a title.\n\t\t *\n\t\t * @spec openspec/changes/nc-widget-header/specs/nc-widget-header/spec.md#requirement-a-nextcloud-widget-draws-no-second-heading\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsHeader() {\n\t\t\tif (this.content && this.content.showHeader === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !(typeof this.cnWidgetTitled === 'function' && this.cnWidgetTitled())\n\t\t},\n\n\t\t/**\n\t\t * The header title.\n\t\t *\n\t\t * @return {string} the title.\n\t\t */\n\t\twidgetTitle() {\n\t\t\treturn (this.widgetMeta && this.widgetMeta.title) || this.widgetId || t('nextcloud-vue', 'Widget')\n\t\t},\n\n\t\t/**\n\t\t * The header icon URL.\n\t\t *\n\t\t * @return {string} the icon URL (empty when none).\n\t\t */\n\t\twidgetIconUrl() {\n\t\t\treturn (this.widgetMeta && this.widgetMeta.iconUrl) || ''\n\t\t},\n\n\t\t/**\n\t\t * The most items the list shows (`content.limit`), or 0 for no limit.\n\t\t * A limit lets the widget fit a dashboard cell instead of scrolling\n\t\t * inside it.\n\t\t *\n\t\t * @spec openspec/changes/screens-nc-widget-limit/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-shows-at-most-its-limit\n\t\t * @return {number} the limit.\n\t\t */\n\t\titemLimit() {\n\t\t\tconst limit = Number(this.content && this.content.limit)\n\t\t\treturn Number.isInteger(limit) && limit > 0 ? limit : 0\n\t\t},\n\n\t\t/**\n\t\t * The items drawn: at most `itemLimit`.\n\t\t *\n\t\t * @return {object[]} the items.\n\t\t */\n\t\tshownItems() {\n\t\t\tconst kept = this.hasExclusions ? this.items.filter((item) => !this.isExcluded(item)) : this.items\n\t\t\treturn this.itemLimit > 0 ? kept.slice(0, this.itemLimit) : kept\n\t\t},\n\n\t\t/**\n\t\t * The apps and item types `content.excludeApps` / `content.excludeTypes`\n\t\t * leave out, lower-cased.\n\t\t *\n\t\t * @spec openspec/changes/nc-widget-exclude-apps/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-can-leave-out-items-of-an-app-or-type\n\t\t * @return {{apps: string[], types: string[]}}\n\t\t */\n\t\texclusions() {\n\t\t\tconst list = (value) => (Array.isArray(value) ? value : (typeof value === 'string' && value !== '' ? [value] : []))\n\t\t\t\t.filter((entry) => typeof entry === 'string' && entry !== '')\n\t\t\t\t.map((entry) => entry.toLowerCase())\n\t\t\treturn { apps: list(this.content && this.content.excludeApps), types: list(this.content && this.content.excludeTypes) }\n\t\t},\n\n\t\t/**\n\t\t * How many items to ask the API for: the limit (or the default), and\n\t\t * more while items are left out, so a filtered list still fills it.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\trequestLimit() {\n\t\t\tconst base = this.itemLimit > 0 ? this.itemLimit : API_ITEM_LIMIT\n\t\t\treturn this.hasExclusions ? Math.max(base * 3, 20) : base\n\t\t},\n\n\t\t/** @return {boolean} Whether any app or type is left out. */\n\t\thasExclusions() {\n\t\t\treturn this.exclusions.apps.length > 0 || this.exclusions.types.length > 0\n\t\t},\n\n\t\t/**\n\t\t * Where \"Show all\" goes: `content.showAllUrl`, else the page the widget\n\t\t * names itself (its `url`). Only with a limit; '' for no link.\n\t\t *\n\t\t * @spec openspec/changes/screens-nc-widget-limit/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-shows-at-most-its-limit\n\t\t * @return {string} the URL.\n\t\t */\n\t\tshowAllHref() {\n\t\t\tif (this.itemLimit === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst own = this.content && typeof this.content.showAllUrl === 'string' ? this.content.showAllUrl : ''\n\t\t\treturn own || (this.widgetMeta && typeof this.widgetMeta.url === 'string' ? this.widgetMeta.url : '')\n\t\t},\n\n\t\t/**\n\t\t * The \"Show all\" text (`content.showAllLabel`, else the library's).\n\t\t *\n\t\t * @return {string} the label.\n\t\t */\n\t\tshowAllLabel() {\n\t\t\tconst label = this.content && this.content.showAllLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : t('nextcloud-vue', 'Show all')\n\t\t},\n\n\t\t/**\n\t\t * The body orientation CSS modifier class.\n\t\t *\n\t\t * @return {string} the body class.\n\t\t */\n\t\tbodyClass() {\n\t\t\treturn `cn-nc-widget-widget__body--${this.displayMode}`\n\t\t},\n\n\t\t/**\n\t\t * The item orientation CSS modifier class.\n\t\t *\n\t\t * @return {string} the item class.\n\t\t */\n\t\titemClass() {\n\t\t\treturn `cn-nc-widget-widget__item--${this.displayMode}`\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.tryMount()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an item belongs to an app or type the content leaves out: its\n\t\t * own `app` / `type` / `objectType`, else the app in an `/apps/<app>/`\n\t\t * part of its link or icons.\n\t\t *\n\t\t * @spec openspec/changes/nc-widget-exclude-apps/specs/nc-widget/spec.md#requirement-a-nextcloud-widget-can-leave-out-items-of-an-app-or-type\n\t\t * @param {object} item A widget item.\n\t\t * @return {boolean}\n\t\t */\n\t\tisExcluded(item) {\n\t\t\tif (!item || typeof item !== 'object') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst { apps, types } = this.exclusions\n\t\t\tconst own = (value) => (typeof value === 'string' ? value.toLowerCase() : '')\n\t\t\tconst appsOf = [own(item.app)]\n\t\t\tfor (const url of [item.link, item.iconUrl, item.overlayIconUrl]) {\n\t\t\t\tconst match = typeof url === 'string' ? url.match(/\\/apps\\/([a-z0-9_-]+)\\//i) : null\n\t\t\t\tif (match) {\n\t\t\t\t\tappsOf.push(match[1].toLowerCase())\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (apps.length > 0 && appsOf.some((app) => app !== '' && apps.includes(app))) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn types.length > 0 && [own(item.type), own(item.objectType)].some((type) => type !== '' && types.includes(type))\n\t\t},\n\n\t\tt,\n\n\t\t/**\n\t\t * Resolve the mounting mode. Prefers the native `OCA.Dashboard`\n\t\t * callback fast-path; otherwise falls back to the OCS item list.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttryMount() {\n\t\t\tif (!this.widgetId) {\n\t\t\t\tthis.mode = 'api'\n\t\t\t\tthis.items = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The native callback paints its own opaque markup, so `displayMode`\n\t\t\t// (bodyClass/itemClass below) has no effect on it — only the API\n\t\t\t// item list honours the vertical/horizontal setting. Horizontal mode\n\t\t\t// must therefore skip the native fast-path, or the \"Horizontal\n\t\t\t// (cards)\" option would silently do nothing for the (common) case\n\t\t\t// where the widget has a registered native callback.\n\t\t\t// A `limit` also takes the API list first: the native callback paints\n\t\t\t// as many items as it likes. Its fallback to native stays.\n\t\t\t// Leaving items out works on the API list only, so it skips native too.\n\t\t\tif (this.displayMode !== 'horizontal' && this.itemLimit === 0 && !this.hasExclusions) {\n\t\t\t\tconst callback = this.resolveNativeCallback()\n\t\t\t\tif (typeof callback === 'function') {\n\t\t\t\t\tthis.mountNative(callback)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.mode = 'api'\n\t\t\tthis.loadApiItems()\n\t\t},\n\n\t\t/**\n\t\t * Look up a native render callback on the `OCA.Dashboard` global.\n\t\t *\n\t\t * @return {((el: HTMLElement, ctx: {widget: object}) => unknown)|null} the callback, or `null` when none is registered.\n\t\t */\n\t\tresolveNativeCallback() {\n\t\t\tconst dashboard = getDashboardGlobal()\n\t\t\tif (!dashboard) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (this.widgetMeta && typeof this.widgetMeta.callback === 'function') {\n\t\t\t\treturn this.widgetMeta.callback\n\t\t\t}\n\t\t\tif (dashboard.callbacks && typeof dashboard.callbacks[this.widgetId] === 'function') {\n\t\t\t\treturn dashboard.callbacks[this.widgetId]\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Mount the widget natively by invoking its registered callback with\n\t\t * our render container.\n\t\t *\n\t\t * @param {(el: HTMLElement, ctx: {widget: object}) => unknown} callback the native render callback.\n\t\t * @return {void}\n\t\t */\n\t\tmountNative(callback) {\n\t\t\tthis.mode = 'native'\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst container = this.$refs.nativeContainer\n\t\t\t\tif (!container) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Native callback failed — fall back to the API list.\n\t\t\t\tconst fallback = () => {\n\t\t\t\t\tthis.mode = 'api'\n\t\t\t\t\tthis.loadApiItems()\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\t// NC dashboard widgets are registered as\n\t\t\t\t\t// `register(id, (el, { widget }) => …)` — the second argument\n\t\t\t\t\t// MUST be the `{ widget }` envelope, not the bare metadata, or\n\t\t\t\t\t// the widget destructures `widget` as undefined and throws on\n\t\t\t\t\t// first use (e.g. `widget.title`).\n\t\t\t\t\tconst result = callback(container, { widget: this.widgetMeta || {} })\n\t\t\t\t\t// Most widget callbacks are async; a synchronous try/catch\n\t\t\t\t\t// can't see a rejected promise, so wire the fallback to it too.\n\t\t\t\t\tif (result && typeof result.then === 'function') {\n\t\t\t\t\t\tresult.catch(fallback)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tfallback()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Fetch the API fallback list directly from the OCS Dashboard\n\t\t * endpoint. Malformed or failed responses collapse to an empty list\n\t\t * (the empty state) instead of throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadApiItems() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\t// The v2 widget-items API only returns an entry for widgets\n\t\t\t\t// whose server-side provider implements `IAPIWidgetV2` — plenty\n\t\t\t\t// of native dashboard widgets are JS-callback-only (`IWidget`)\n\t\t\t\t// and never appear in this response at all, which looks\n\t\t\t\t// identical to \"0 items\" from here. extractItems() tolerates\n\t\t\t\t// both the flat-array (older providers) and `{items}` (v2) shapes.\n\t\t\t\tconst url = generateOcsUrl('/apps/dashboard/api/v2/widget-items')\n\t\t\t\tconst response = await axios.get(url, {\n\t\t\t\t\tparams: { widgets: [this.widgetId], limit: this.requestLimit },\n\t\t\t\t})\n\t\t\t\tconst data = response && response.data && response.data.ocs ? response.data.ocs.data : null\n\t\t\t\tconst widgetData = data ? data[this.widgetId] : null\n\t\t\t\tthis.items = this.extractItems(widgetData)\n\t\t\t\t// Only a SUCCESSFUL response can say the widget is absent. A\n\t\t\t\t// failed request says nothing about the widget, so it keeps\n\t\t\t\t// the ordinary empty state rather than a claim about the app.\n\t\t\t\tthis.unsupported = data !== null && (widgetData === null || widgetData === undefined)\n\t\t\t} catch {\n\t\t\t\tthis.items = []\n\t\t\t\tthis.unsupported = false\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\n\t\t\t// Horizontal mode deliberately skips the native fast-path (see\n\t\t\t// tryMount()) to get the styled card layout. But when the API has\n\t\t\t// no entry for this widget at all — no `IAPIWidgetV2` support, not\n\t\t\t// \"no items right now\" — that would render a false empty state for\n\t\t\t// a widget that actually has content. Fall back to native\n\t\t\t// rendering rather than lying about there being nothing to show.\n\t\t\tif (this.items.length === 0) {\n\t\t\t\tconst callback = this.resolveNativeCallback()\n\t\t\t\tif (typeof callback === 'function') {\n\t\t\t\t\tthis.mountNative(callback)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Normalise a per-widget OCS payload to an items array, tolerating both\n\t\t * the flat array and the `{items}` envelope shapes.\n\t\t *\n\t\t * @param {unknown} widgetData the per-widget payload.\n\t\t * @return {object[]} the items array (possibly empty).\n\t\t */\n\t\textractItems(widgetData) {\n\t\t\tif (!widgetData) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (Array.isArray(widgetData)) {\n\t\t\t\treturn widgetData\n\t\t\t}\n\t\t\tif (Array.isArray(widgetData.items)) {\n\t\t\t\treturn widgetData.items\n\t\t\t}\n\t\t\tif (widgetData.items && typeof widgetData.items === 'object') {\n\t\t\t\treturn Object.values(widgetData.items)\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-nc-widget-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\twidth: 100%;\n\theight: 100%;\n\toverflow: hidden;\n}\n\n/*\n * Header + content match the native Nextcloud Dashboard panel (apps/dashboard)\n * exactly — this component's whole job is rendering NC dashboard widgets, so it\n * should look native by default: 16px header, 32px leading icon with a 16px\n * gap, a 20px/700 title, and the native panel content inset (16px sides +\n * bottom, flush to the header on top). Same design tokens as core; overridable\n * via the host's CnWidgetWrapper styleConfig / nldesign.\n */\n.cn-nc-widget-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 16px;\n\tpadding: 16px;\n\tfont-weight: 700;\n}\n\n.cn-nc-widget-widget__header-icon {\n\twidth: 32px;\n\theight: 32px;\n\tflex: 0 0 auto;\n}\n\n.cn-nc-widget-widget__header-title {\n\tflex: 1;\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 24px;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-nc-widget-widget__show-all {\n\talign-self: flex-start;\n\tpadding: 8px 0 0;\n\tcolor: var(--color-primary-element);\n\tfont-weight: 600;\n\ttext-decoration: none;\n}\n\n.cn-nc-widget-widget__show-all:hover,\n.cn-nc-widget-widget__show-all:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-nc-widget-widget__native {\n\tflex: 1;\n\toverflow: auto;\n\tpadding: 0 16px 16px;\n}\n\n/*\n * The native `OCA.Dashboard` callback paints its own markup we don't control\n * — for first-party widgets that's almost always `@nextcloud/vue`'s\n * `NcDashboardWidget` / `NcDashboardWidgetItem` (`.dashboard-widget` wrapping\n * a bare `<ul>`/`<li>` list, each item a `.item-list__entry`).\n *\n * Only the LIST CONTAINER is retiled into a grid here — `.item-list__entry`'s\n * own internal layout is left untouched. An earlier version of this override\n * also forced `.item-list__entry` from row to column, but its icon-overlay\n * badge (`.item-icon`, see @nextcloud/vue's NcDashboardWidgetItem CSS) is\n * positioned with negative margins calibrated for the row layout, so\n * flipping to column made items visually overlap. Tiling the `<li>`s into a\n * grid while each entry keeps its normal icon+text row reads as a grid of\n * card tiles without touching that internal math.\n *\n * Scoped from `.cn-nc-widget-widget--horizontal` on the ROOT element, not\n * `.cn-nc-widget-widget__native`: the callback mounts via `.$mount(el)`,\n * which replaces `nativeContainer` in the live DOM outright, so any class\n * placed directly on it never survives to be selectable.\n *\n * The 250px column floor matches `.item-list__entry`'s own natural minimum\n * width (avatar + padding + its un-truncatable-below-this content) — a grid\n * track narrower than that squeezes the entry below its real minimum and it\n * overflows into the neighbouring cell. `min-width: 0` on the `<li>` alone\n * isn't enough to shrink it further: the item's own internal flex layout\n * still claims that much room regardless of the grid track size, so the\n * track itself must not go below it.\n */\n.cn-nc-widget-widget--horizontal :deep(.dashboard-widget > ul) {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n\tgap: 8px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-nc-widget-widget--horizontal :deep(.dashboard-widget > ul > li) {\n\twidth: auto;\n\tmin-width: 0;\n}\n\n.cn-nc-widget-widget--horizontal :deep(.item-list__entry) {\n\theight: 100%;\n\tmargin: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n}\n\n.cn-nc-widget-widget__body {\n\tflex: 1;\n\toverflow: auto;\n\tpadding: 0 16px 16px;\n}\n\n.cn-nc-widget-widget__body--vertical {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-nc-widget-widget__body--horizontal {\n\tdisplay: flex;\n\tflex-direction: row;\n\tflex-wrap: wrap;\n\tgap: 12px;\n}\n\n.cn-nc-widget-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex: 1;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n\n.cn-nc-widget-widget__item {\n\tdisplay: flex;\n\ttext-decoration: none;\n\tcolor: var(--color-main-text);\n\tborder-radius: var(--border-radius);\n\tpadding: 4px;\n}\n\n.cn-nc-widget-widget__item:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-nc-widget-widget__item--vertical {\n\tflex-direction: row;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-nc-widget-widget__item--horizontal {\n\tflex-direction: column;\n\talign-items: center;\n\twidth: 120px;\n\ttext-align: center;\n}\n\n.cn-nc-widget-widget__item-icon-wrap {\n\twidth: 32px;\n\theight: 32px;\n\tflex: 0 0 32px;\n\tposition: relative;\n}\n\n.cn-nc-widget-widget__item-icon {\n\twidth: 32px;\n\theight: 32px;\n\tobject-fit: cover;\n\tborder-radius: 50%;\n}\n\n.cn-nc-widget-widget__item-overlay {\n\tposition: absolute;\n\tright: -2px;\n\tbottom: -2px;\n\twidth: 14px;\n\theight: 14px;\n}\n\n.cn-nc-widget-widget__item-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-width: 0;\n\tflex: 1;\n}\n\n.cn-nc-widget-widget__item-title {\n\tfont-weight: 600;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-nc-widget-widget__item-subtitle {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_openBlock","_createElementVNode","_toDisplayString","_withDirectives","_Fragment","_renderList"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnObjectCard.vue2.js';
2
2
  import { render } from './CnObjectCard.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-59552a5b";
5
+ script.__scopeId = "data-v-78efa77e";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,6 +1,12 @@
1
+ import { translate } from '@nextcloud/l10n';
1
2
  import { NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue';
2
3
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js';
3
4
  import { normalizeLook } from '../../composables/useLook.js';
5
+ import { isBadgeVariant } from '../../utils/badgeVariants.js';
6
+ import { formatBoardDate } from '../../utils/boardDate.js';
7
+ import { resolveDateVariant } from '../../utils/dateVariant.js';
8
+ import { resolveDue } from '../../utils/dueText.js';
9
+ import { readPath } from '../../utils/readPath.js';
4
10
  import { resolveImageUrl } from '../../utils/resolveImageUrl.js';
5
11
  import { preventMiddleClickAutoscroll, isRowMiddleClick } from '../../utils/rowAuxClick.js';
6
12
  import { formatValue } from '../../utils/schema.js';
@@ -49,6 +55,8 @@ var script = {
49
55
  cnTranslate: { default: () => (key) => key },
50
56
  /** The look CnAppRoot provides; `board` draws the record card of the screens. */
51
57
  cnLook: { default: 'nextcloud' },
58
+ /** The cell formatters CnAppRoot provides, for a layout part whose page column names one. */
59
+ cnFormatters: { default: () => ({}) },
52
60
  },
53
61
 
54
62
  props: {
@@ -141,6 +149,25 @@ var script = {
141
149
  default: null,
142
150
  },
143
151
 
152
+ /**
153
+ * Board look: the record layout (manifest key `config.cardLayout`).
154
+ * Every part names a field of the object (a dot path) and is left out
155
+ * while that field is empty:
156
+ * `{ headPill, number, title, meta, footerPill, footerDate }`.
157
+ * A pill is `{ field, colorMap?, labels?, variant? }`: `colorMap` maps a
158
+ * value to a CnStatusBadge variant (ignoring case), `labels` a value to
159
+ * the text shown. `meta` is a list of fields, joined with " · ".
160
+ * `footerDate` is `{ field, label? }`, drawn as "Termijn 5 okt".
161
+ * Without it the card keeps its head, facts and action.
162
+ *
163
+ * @type {{headPill?: object, number?: string, title?: string, meta?: Array<string>, footerPill?: object, footerDate?: {field: string, label?: string}}}
164
+ * @spec openspec/changes/board-record-card-layout/specs/board-record-card-layout/spec.md#requirement-a-board-card-can-draw-the-record-layout-from-config
165
+ */
166
+ layout: {
167
+ type: Object,
168
+ default: null,
169
+ },
170
+
144
171
  /**
145
172
  * The property keys the facts list shows, in order (manifest key
146
173
  * `config.cardFields`). Without it the card shows the first
@@ -153,6 +180,17 @@ var script = {
153
180
  default: null,
154
181
  },
155
182
 
183
+ /**
184
+ * The page's columns by key: a fact whose key names a column renders
185
+ * through that column's label, formatter and widget, as the table does.
186
+ *
187
+ * @type {object}
188
+ */
189
+ cardFieldColumns: {
190
+ type: Object,
191
+ default: null,
192
+ },
193
+
156
194
  /** Maximum number of metadata fields to show */
157
195
  maxMetadata: {
158
196
  type: Number,
@@ -204,6 +242,92 @@ var script = {
204
242
  return null
205
243
  },
206
244
 
245
+ /**
246
+ * The record layout when the board look draws it, else null.
247
+ *
248
+ * @return {?object}
249
+ * @spec openspec/changes/board-record-card-layout/specs/board-record-card-layout/spec.md#requirement-a-board-card-can-draw-the-record-layout-from-config
250
+ */
251
+ recordLayout() {
252
+ return this.isBoardLook && this.layout && typeof this.layout === 'object' ? this.layout : null
253
+ },
254
+
255
+ /**
256
+ * The record layout's head pill, or null.
257
+ *
258
+ * @return {?{label: string, variant: string}}
259
+ */
260
+ recordHeadPill() {
261
+ return this.recordLayout ? this.layoutPill(this.recordLayout.headPill) : null
262
+ },
263
+
264
+ /**
265
+ * The record layout's footer pill, or null.
266
+ *
267
+ * @return {?{label: string, variant: string}}
268
+ */
269
+ recordFootPill() {
270
+ return this.recordLayout ? this.layoutPill(this.recordLayout.footerPill) : null
271
+ },
272
+
273
+ /**
274
+ * The record number at the end of the head, or ''.
275
+ *
276
+ * @return {string}
277
+ */
278
+ recordNumber() {
279
+ return this.recordLayout ? this.layoutText(this.recordLayout.number) : ''
280
+ },
281
+
282
+ /**
283
+ * The title: the layout's `title` field, else the card's own title.
284
+ *
285
+ * @return {string}
286
+ */
287
+ recordTitle() {
288
+ const text = this.recordLayout ? this.layoutText(this.recordLayout.title) : '';
289
+ return text !== '' ? text : this.title
290
+ },
291
+
292
+ /**
293
+ * The meta line: the layout's `meta` fields that hold a value, joined.
294
+ *
295
+ * @return {string}
296
+ */
297
+ recordMeta() {
298
+ return this.recordMetaParts.map((part) => part.text).join(' · ')
299
+ },
300
+
301
+ /**
302
+ * The meta line's parts that hold a value: each with its text and, when
303
+ * its field names a page column, that column (whose widget draws it).
304
+ *
305
+ * @return {Array<{field: string, value: unknown, text: string, column: ?object}>}
306
+ */
307
+ recordMetaParts() {
308
+ const fields = this.recordLayout && Array.isArray(this.recordLayout.meta) ? this.recordLayout.meta : [];
309
+ return fields
310
+ .map((part) => this.metaPart(part))
311
+ .filter((part) => part !== null && part.text !== '')
312
+ },
313
+
314
+ /**
315
+ * The footer date with its label ("Termijn 5 okt"), or ''.
316
+ *
317
+ * @return {string}
318
+ */
319
+ recordDate() {
320
+ const def = this.recordLayout ? this.recordLayout.footerDate : null;
321
+ if (!def || typeof def.field !== 'string') {
322
+ return ''
323
+ }
324
+ const date = formatBoardDate(readPath(this.object, def.field));
325
+ if (date === '') {
326
+ return ''
327
+ }
328
+ return typeof def.label === 'string' && def.label !== '' ? `${this.cnTranslate(def.label)} ${date}` : date
329
+ },
330
+
207
331
  /**
208
332
  * The accent variant when it is a known one, else ''.
209
333
  *
@@ -272,12 +396,7 @@ var script = {
272
396
  if (Array.isArray(this.cardFields)) {
273
397
  return this.cardFields
274
398
  .filter((key) => typeof key === 'string' && this.schema.properties[key] && !this.configFields.includes(key))
275
- .map((key) => ({
276
- key,
277
- label: this.cnTranslate(this.schema.properties[key].title || key),
278
- value: this.object[key],
279
- property: this.schema.properties[key],
280
- }))
399
+ .map((key) => this.metadataField(key, this.schema.properties[key]))
281
400
  }
282
401
 
283
402
  return Object.entries(this.schema.properties)
@@ -302,16 +421,180 @@ var script = {
302
421
  return orderA - orderB
303
422
  })
304
423
  .slice(0, this.maxMetadata)
305
- .map(([key, prop]) => ({
306
- key,
307
- label: this.cnTranslate(prop.title || key),
308
- value: this.object[key],
309
- property: prop,
310
- }))
424
+ .map(([key, prop]) => this.metadataField(key, prop))
311
425
  },
312
426
  },
313
427
 
314
428
  methods: {
429
+ /**
430
+ * One meta part: a field name reads through the page column it names;
431
+ * `{ field, formatter?, formatterOptions? }` reads as plain text through
432
+ * its own formatter, so a column's widget (a badge) stays in the table.
433
+ *
434
+ * @param {string|{field: string, formatter?: string, formatterOptions?: object}} part The part.
435
+ * @return {?{field: string, value: unknown, text: string, column: ?object}} The part, or null when it names no field.
436
+ * @spec openspec/changes/card-layout-part-formatter/specs/card-layout-part-formatter/spec.md#requirement-a-card-meta-part-can-name-its-own-formatter
437
+ */
438
+ metaPart(part) {
439
+ if (typeof part === 'string' && part !== '') {
440
+ return { field: part, value: readPath(this.object, part), text: this.layoutText(part), column: this.layoutColumn(part) }
441
+ }
442
+ if (!part || typeof part !== 'object' || typeof part.field !== 'string' || part.field === '') {
443
+ return null
444
+ }
445
+ const column = typeof part.formatter === 'string' ? { formatter: part.formatter, formatterOptions: part.formatterOptions } : null;
446
+ return { field: part.field, value: readPath(this.object, part.field), text: this.layoutText(part.field, column), column }
447
+ },
448
+
449
+ /**
450
+ * A field's value as text: a person or a record by its display name,
451
+ * title or name, anything else as a string. '' for an empty value or
452
+ * a field that is not named.
453
+ *
454
+ * @param {unknown} field A dot path into the object.
455
+ * @param {?object} [own] The column to read through instead of the one the field names.
456
+ * @return {string}
457
+ */
458
+ layoutText(field, own) {
459
+ if (typeof field !== 'string' || field === '') {
460
+ return ''
461
+ }
462
+ const value = readPath(this.object, field);
463
+ if (value === null || value === undefined || value === '') {
464
+ return ''
465
+ }
466
+ // A part whose field names a page column with a formatter reads as
467
+ // its table cell does (a case type uuid as its name).
468
+ const column = own === undefined ? this.layoutColumn(field) : own;
469
+ const format = column && column.formatter && this.cnFormatters ? this.cnFormatters[column.formatter] : null;
470
+ if (typeof format === 'function') {
471
+ const formatted = format(value, this.object, this.schema?.properties?.[field], column.formatterOptions || undefined);
472
+ return formatted === null || formatted === undefined ? '' : String(formatted)
473
+ }
474
+ if (typeof value === 'object') {
475
+ const name = value.displayName || value.title || value.name;
476
+ return typeof name === 'string' ? name : ''
477
+ }
478
+ return String(value)
479
+ },
480
+
481
+ /**
482
+ * The page column a layout part's field names, or null.
483
+ *
484
+ * @param {string} field The part's field.
485
+ * @return {?object} The column.
486
+ */
487
+ layoutColumn(field) {
488
+ return (this.cardFieldColumns && typeof field === 'string' && this.cardFieldColumns[field]) || null
489
+ },
490
+
491
+ /**
492
+ * A pill that reads its date field through the due rules: `variantWhen`
493
+ * (the first rule the days left match gives the tone), else `due`
494
+ * (overdue reads as error, within `due.soonWithin` days as warning).
495
+ * The label is `labels[variant]`, else `labels.overdue` / `labels.soon`,
496
+ * else Overdue / Soon, through the host's translate. Any other date
497
+ * draws no pill.
498
+ *
499
+ * @param {{field: string, due?: object, variantWhen?: Array, labels?: object}} def The pill.
500
+ * @return {?{label: string, variant: string}}
501
+ */
502
+ datePill(def) {
503
+ const value = readPath(this.object, def.field);
504
+ const variant = Array.isArray(def.variantWhen)
505
+ ? this.dateRuleVariant(value, def.variantWhen)
506
+ : this.dueVariant(value, def.due);
507
+ if (variant === '') {
508
+ return null
509
+ }
510
+ const labels = def.labels && typeof def.labels === 'object' ? def.labels : {};
511
+ const state = variant === 'error' ? 'overdue' : (variant === 'warning' ? 'soon' : '');
512
+ const authored = labels[variant] || (state && labels[state]);
513
+ if (typeof authored === 'string' && authored !== '') {
514
+ return { label: this.cnTranslate(authored), variant }
515
+ }
516
+ if (state === 'overdue') {
517
+ return { label: translate('nextcloud-vue', 'Overdue'), variant }
518
+ }
519
+ if (state === 'soon') {
520
+ return { label: translate('nextcloud-vue', 'Soon'), variant }
521
+ }
522
+ return null
523
+ },
524
+
525
+ /**
526
+ * The variant of the first `variantWhen` rule a date matches, or ''.
527
+ *
528
+ * @param {unknown} value The date.
529
+ * @param {Array} rules The rules.
530
+ * @return {string} The variant, or ''.
531
+ */
532
+ dateRuleVariant(value, rules) {
533
+ const matched = resolveDateVariant(value, rules);
534
+ return matched === 'default' ? '' : matched
535
+ },
536
+
537
+ /**
538
+ * The variant the due rules give a date: error when overdue, warning
539
+ * within `soonWithin` days, else ''.
540
+ *
541
+ * @param {unknown} value The date.
542
+ * @param {object} config The `due` config.
543
+ * @return {string} The variant, or ''.
544
+ */
545
+ dueVariant(value, config) {
546
+ const due = resolveDue(value, config);
547
+ if (due && due.tone === 'danger') {
548
+ return 'error'
549
+ }
550
+ return due && due.soon ? 'warning' : ''
551
+ },
552
+
553
+ /**
554
+ * A layout pill as `{ label, variant }`, or null while its field is
555
+ * empty.
556
+ *
557
+ * @param {?{field: string, colorMap?: object, labels?: object, variant?: string}} def The pill.
558
+ * @return {?{label: string, variant: string}}
559
+ */
560
+ layoutPill(def) {
561
+ if (!def || typeof def !== 'object') {
562
+ return null
563
+ }
564
+ if (def.due || Array.isArray(def.variantWhen)) {
565
+ return this.datePill(def)
566
+ }
567
+ const value = this.layoutText(def.field);
568
+ if (value === '') {
569
+ return null
570
+ }
571
+ const labels = def.labels && typeof def.labels === 'object' ? def.labels : {};
572
+ const label = typeof labels[value] === 'string' ? this.cnTranslate(labels[value]) : value;
573
+ const colorMap = def.colorMap && typeof def.colorMap === 'object' ? def.colorMap : {};
574
+ const key = Object.keys(colorMap).find((k) => k.toLowerCase() === value.toLowerCase());
575
+ const variant = key !== undefined ? colorMap[key] : def.variant;
576
+ return { label, variant: isBadgeVariant(variant) ? variant : 'default' }
577
+ },
578
+
579
+ /**
580
+ * One fact of the card: the property's value with the matching page
581
+ * column (label, formatter, widget) when there is one.
582
+ *
583
+ * @param {string} key The property key.
584
+ * @param {object} prop The schema property.
585
+ * @return {object} { key, label, value, property, column }
586
+ */
587
+ metadataField(key, prop) {
588
+ const column = (this.cardFieldColumns && this.cardFieldColumns[key]) || null;
589
+ return {
590
+ key,
591
+ label: this.cnTranslate((column && column.label) || prop.title || key),
592
+ value: this.object[key],
593
+ property: prop,
594
+ column,
595
+ }
596
+ },
597
+
315
598
  formatValue,
316
599
 
317
600
  /**