@conduction/nextcloud-vue 2.115.0 → 2.116.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 (117) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +92 -0
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +2 -2
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js +9 -0
  7. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js.map +1 -0
  8. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js +145 -0
  9. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js.map +1 -0
  10. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js +58 -0
  11. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js.map +1 -0
  12. package/dist/esm/components/CnChecklistWidget/index.js +28 -0
  13. package/dist/esm/components/CnChecklistWidget/index.js.map +1 -0
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -0
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +376 -51
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +364 -285
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -0
  25. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +6 -4
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  28. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +1 -1
  29. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +48 -1
  30. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +11 -2
  32. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +1 -1
  34. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +11 -3
  35. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +8 -3
  37. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +55 -2
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +94 -4
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +248 -239
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +9 -0
  48. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js.map +1 -0
  49. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +282 -0
  50. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -0
  51. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +202 -0
  52. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -0
  53. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  54. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
  55. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  58. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  59. package/dist/esm/composables/indexSources.js +64 -0
  60. package/dist/esm/composables/indexSources.js.map +1 -1
  61. package/dist/esm/composables/useTaskInboxStore.js +85 -0
  62. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  63. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +9 -0
  64. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js.map +1 -0
  65. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +107 -0
  66. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -0
  67. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +110 -0
  68. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -0
  69. package/dist/esm/index.js +1 -0
  70. package/dist/esm/index.js.map +1 -1
  71. package/dist/esm/l10n/en.json.js +19 -1
  72. package/dist/esm/l10n/en.json.js.map +1 -1
  73. package/dist/esm/l10n/nl.json.js +23 -1
  74. package/dist/esm/l10n/nl.json.js.map +1 -1
  75. package/dist/esm/library-style.js +1 -1
  76. package/dist/esm/nextcloud-vue.css +1054 -911
  77. package/dist/esm/utils/actionsDispatcher.js +10 -5
  78. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  79. package/dist/esm/utils/detailActionModel.js +6 -2
  80. package/dist/esm/utils/detailActionModel.js.map +1 -1
  81. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  82. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  83. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  84. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  85. package/dist/esm/utils/widgetDispatch.js +1 -1
  86. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  87. package/dist/nextcloud-vue.cjs.js +17078 -15224
  88. package/dist/nextcloud-vue.cjs.js.map +2 -2
  89. package/dist/nextcloud-vue.css +1054 -911
  90. package/l10n/en.json +19 -1
  91. package/l10n/nl.json +23 -1
  92. package/package.json +1 -1
  93. package/src/components/CnActionButtons/CnActionButtons.vue +93 -1
  94. package/src/components/CnChecklistWidget/CnChecklistWidget.vue +206 -0
  95. package/src/components/CnChecklistWidget/index.js +27 -0
  96. package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
  97. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  98. package/src/components/CnDetailPage/CnDetailPage.vue +377 -10
  99. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +15 -2
  100. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +59 -1
  101. package/src/components/CnMacrosWidget/CnMacrosWidget.vue +16 -6
  102. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +55 -2
  103. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +101 -3
  104. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +508 -0
  105. package/src/components/CnTabsWidget/CnTabsWidget.vue +10 -5
  106. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  107. package/src/components/index.js +1 -0
  108. package/src/composables/indexSources.js +64 -0
  109. package/src/composables/useTaskInboxStore.js +85 -0
  110. package/src/dialogs/CnAccessPurposeDialog.vue +183 -0
  111. package/src/index.js +1 -0
  112. package/src/schemas/app-manifest-v2.schema.json +49 -3
  113. package/src/utils/actionsDispatcher.js +10 -5
  114. package/src/utils/detailActionModel.js +6 -2
  115. package/src/utils/libraryWidgetKeys.js +1 -0
  116. package/src/utils/validateManifestV2.compiled.js +3 -3
  117. package/src/utils/widgetDispatch.js +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CnGroupedCardsWidget.vue3.js","sources":["../../../../src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue?vue&type=template&id=cf2cd50a&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-grouped-cards\" data-testid=\"cn-grouped-cards\">\n\t\t<p v-if=\"error\" class=\"cn-grouped-cards__state\" data-testid=\"cn-grouped-cards-error\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && groups.length === 0\" class=\"cn-grouped-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"groups.length === 0 && !extra\" class=\"cn-grouped-cards__state\" data-testid=\"cn-grouped-cards-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<div\n\t\t\tv-else\n\t\t\tclass=\"cn-grouped-cards__grid\"\n\t\t\t:style=\"{ '--cn-grouped-cards-columns': columns }\">\n\t\t\t<section\n\t\t\t\tv-for=\"group in groups\"\n\t\t\t\t:key=\"group.key\"\n\t\t\t\tclass=\"cn-grouped-cards__group\"\n\t\t\t\t:aria-labelledby=\"group.headingId\"\n\t\t\t\tdata-testid=\"cn-grouped-cards-group\">\n\t\t\t\t<header class=\"cn-grouped-cards__group-header\">\n\t\t\t\t\t<h4 :id=\"group.headingId\" class=\"cn-grouped-cards__group-title\">\n\t\t\t\t\t\t{{ group.title }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<span v-if=\"group.count\" class=\"cn-grouped-cards__count\" data-testid=\"cn-grouped-cards-count\">{{ group.count }}</span>\n\t\t\t\t</header>\n\t\t\t\t<ul v-if=\"group.items.length > 0\" class=\"cn-grouped-cards__items\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"item in group.items\"\n\t\t\t\t\t\t:key=\"item.key\"\n\t\t\t\t\t\tclass=\"cn-grouped-cards__item\"\n\t\t\t\t\t\tdata-testid=\"cn-grouped-cards-item\">\n\t\t\t\t\t\t<span class=\"cn-grouped-cards__item-title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-grouped-cards__item-meta\">{{ item.meta }}</span>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<p v-else class=\"cn-grouped-cards__group-empty\">\n\t\t\t\t\t{{ groupEmptyLabel }}\n\t\t\t\t</p>\n\t\t\t</section>\n\t\t\t<section\n\t\t\t\tv-if=\"extra\"\n\t\t\t\tclass=\"cn-grouped-cards__group cn-grouped-cards__group--extra\"\n\t\t\t\t:aria-labelledby=\"extra.headingId\"\n\t\t\t\tdata-testid=\"cn-grouped-cards-extra\">\n\t\t\t\t<header class=\"cn-grouped-cards__group-header\">\n\t\t\t\t\t<h4 :id=\"extra.headingId\" class=\"cn-grouped-cards__group-title\">\n\t\t\t\t\t\t{{ extra.title }}\n\t\t\t\t\t</h4>\n\t\t\t\t</header>\n\t\t\t\t<p class=\"cn-grouped-cards__extra-text\">\n\t\t\t\t\t{{ extra.text }}\n\t\t\t\t</p>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport { useEndpointSource } from '../../composables/useEndpointSource.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { languageMapText } from '../../utils/languageMapText.js'\nimport { readPath } from '../../utils/readPath.js'\n\nlet uid = 0\n\n/**\n * Unwrap a value that may be a ref.\n *\n * @param {unknown} value The value.\n * @return {unknown} The plain value.\n */\nfunction unwrap(value) {\n\treturn value && typeof value === 'object' && 'value' in value ? value.value : value\n}\n\n/**\n * A row as the templates read it: a language map (`{ nl, en }`) becomes its\n * text in the user's language, and a list of plain values is joined with \", \".\n * Nested objects stay as they are, for a template that reads a deeper path.\n *\n * @param {object} row The row.\n * @return {object} A shallow copy with readable values.\n */\nfunction readable(row) {\n\tif (!row || typeof row !== 'object') {\n\t\treturn {}\n\t}\n\tconst out = {}\n\tfor (const [key, value] of Object.entries(row)) {\n\t\tif (Array.isArray(value)) {\n\t\t\tconst plain = value.filter((entry) => typeof entry === 'string' || typeof entry === 'number')\n\t\t\tout[key] = plain.length === value.length ? plain.join(', ') : value\n\t\t} else if (value && typeof value === 'object' && Object.values(value).every((entry) => typeof entry === 'string')) {\n\t\t\tout[key] = languageMapText(value)\n\t\t} else {\n\t\t\tout[key] = value\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * CnGroupedCardsWidget — groups of cards side by side, read from an app\n * endpoint: a case type's phases, each with the tasks it runs, and a dashed\n * box of what is available in every phase (board DqZaaktype, Fasen en taken).\n *\n * Content-only on a card surface: the detail host or the dashboard draws the\n * titled card around it.\n */\nexport default {\n\tname: 'CnGroupedCardsWidget',\n\n\tcomponents: { NcLoadingIcon },\n\n\tinject: {\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's app config map, for money tokens in a template. */\n\t\tcnAppConfig: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget config: `{ endpointSource, groupsPath?, itemsKey?,\n\t\t * groupTitle?, groupCount?, itemTitle?, itemMeta?, columns?,\n\t\t * extraGroup?, emptyText?, groupEmptyText?, errorText? }`.\n\t\t *\n\t\t * - `endpointSource` (`{ url, params?, responsePath? }`) reads the payload; tokens\n\t\t * such as `@objectId` resolve as on every widget.\n\t\t * - `groupsPath` (default `groups`) is the list of groups in the payload, and\n\t\t * `itemsKey` (default `items`) the list of cards in each group.\n\t\t * - `groupTitle` and `itemTitle` are templates (`\"Phase {order}: {name}\"`,\n\t\t * `\"{title}\"`); `itemMeta` is a template or a list of templates joined\n\t\t * with \" · \", a part whose token is empty being left out.\n\t\t * - `groupCount` (`{ one, other }`, `{count}` filled in) draws a count pill.\n\t\t * - `extraGroup` (`{ title, path, itemText, join? }`) closes the grid with a\n\t\t * dashed box whose entries, read at `path` of the payload, are one line.\n\t\t * - `columns` (default 2) is the number of groups side by side.\n\t\t *\n\t\t * A language map (`{ nl, en }`) reads in the user's language, and a list\n\t\t * of plain values reads joined with \", \". Texts go through the app's\n\t\t * translate.\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\n\t\t/** The current record's id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current record, for `@object.<field>` tokens. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', null)\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst { data, loading, error } = useEndpointSource(\n\t\t\t() => props.content?.endpointSource || null,\n\t\t\t{\n\t\t\t\tctx: () => {\n\t\t\t\t\tconst injected = unwrap(objectCtxRaw) || {}\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...injected,\n\t\t\t\t\t\tobjectId: props.objectId ?? injected.objectId ?? props.objectData?.id ?? null,\n\t\t\t\t\t\tobject: props.objectData || injected.object || null,\n\t\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\t\tconfig: unwrapAppConfig(appConfigRaw) || {},\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t\tuid += 1\n\t\treturn { payload: data, loading, error, idPrefix: `cn-grouped-cards-${uid}` }\n\t},\n\n\tcomputed: {\n\t\t/** @return {number} The groups side by side. */\n\t\tcolumns() {\n\t\t\tconst value = Number(this.content?.columns)\n\t\t\treturn Number.isFinite(value) && value >= 1 ? Math.min(4, Math.floor(value)) : 2\n\t\t},\n\n\t\t/**\n\t\t * The groups ready to draw.\n\t\t *\n\t\t * @return {Array<{key: string, headingId: string, title: string, count: string, items: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint\n\t\t */\n\t\tgroups() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst list = readPath(this.payload, c.groupsPath || 'groups')\n\t\t\tif (!Array.isArray(list)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn list.filter((group) => group && typeof group === 'object').map((raw, index) => {\n\t\t\t\tconst group = readable(raw)\n\t\t\t\tconst itemList = Array.isArray(raw[c.itemsKey || 'items']) ? raw[c.itemsKey || 'items'] : []\n\t\t\t\tconst items = itemList.filter((item) => item && typeof item === 'object').map((rawItem, itemIndex) => {\n\t\t\t\t\tconst item = readable(rawItem)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(rawItem.id ?? `item-${itemIndex}`),\n\t\t\t\t\t\ttitle: this.fill(c.itemTitle || '{title}', item),\n\t\t\t\t\t\tmeta: this.metaOf(item),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\treturn {\n\t\t\t\t\tkey: String(raw.id ?? `group-${index}`),\n\t\t\t\t\theadingId: `${this.idPrefix}-group-${index}`,\n\t\t\t\t\ttitle: this.fill(c.groupTitle || '{name}', group),\n\t\t\t\t\tcount: this.countOf(items.length),\n\t\t\t\t\titems,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The closing box of entries available everywhere, or null.\n\t\t *\n\t\t * @return {{headingId: string, title: string, text: string}|null} The box.\n\t\t * @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint\n\t\t */\n\t\textra() {\n\t\t\tconst spec = this.content?.extraGroup\n\t\t\tif (!spec || typeof spec !== 'object' || !spec.path) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst list = readPath(this.payload, spec.path)\n\t\t\tif (!Array.isArray(list) || list.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parts = list\n\t\t\t\t.map((entry) => (entry && typeof entry === 'object' ? this.fill(spec.itemText || '{label}', readable(entry)) : languageMapText(entry)))\n\t\t\t\t.filter((text) => text !== '')\n\t\t\tif (parts.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn {\n\t\t\t\theadingId: `${this.idPrefix}-extra`,\n\t\t\t\ttitle: this.translate(spec.title || 'Always available'),\n\t\t\t\ttext: parts.join(typeof spec.join === 'string' ? spec.join : ' · '),\n\t\t\t}\n\t\t},\n\n\t\t/** @return {string} The empty text. */\n\t\temptyLabel() {\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing to show yet')\n\t\t},\n\n\t\t/** @return {string} The text of a group without cards. */\n\t\tgroupEmptyLabel() {\n\t\t\treturn this.content?.groupEmptyText ? this.translate(this.content.groupEmptyText) : t('nextcloud-vue', 'Nothing here yet')\n\t\t},\n\n\t\t/** @return {string} The error text. */\n\t\terrorLabel() {\n\t\t\treturn this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')\n\t\t},\n\n\t\t/** @return {string} The loading label. */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a text through the host app's catalogue.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttranslate(text) {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\treturn fn(text)\n\t\t},\n\n\t\t/**\n\t\t * Fill a template from a row, after translating it.\n\t\t *\n\t\t * @param {string} template The template.\n\t\t * @param {object} row The row.\n\t\t * @return {string} The text, '' when a token is empty.\n\t\t */\n\t\tfill(template, row) {\n\t\t\tif (typeof template !== 'string' || template === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn fillTemplate(this.translate(template), row, new Date(), { config: unwrapAppConfig(this.cnAppConfig) })\n\t\t},\n\n\t\t/**\n\t\t * A card's meta line: one template, or several joined with \" · \", a\n\t\t * part whose token is empty being left out with its separator.\n\t\t *\n\t\t * @param {object} item The card's row.\n\t\t * @return {string} The line.\n\t\t */\n\t\tmetaOf(item) {\n\t\t\tconst spec = this.content?.itemMeta\n\t\t\tconst parts = Array.isArray(spec) ? spec : (typeof spec === 'string' ? [spec] : [])\n\t\t\treturn parts.map((part) => this.fill(part, item)).filter((text) => text !== '').join(' · ')\n\t\t},\n\n\t\t/**\n\t\t * The count pill of a group (`groupCount: { one, other }`).\n\t\t *\n\t\t * @param {number} count The number of cards.\n\t\t * @return {string} The text, '' without `groupCount`.\n\t\t */\n\t\tcountOf(count) {\n\t\t\tconst spec = this.content?.groupCount\n\t\t\tif (!spec || typeof spec !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst template = count === 1 ? spec.one : spec.other\n\t\t\treturn typeof template === 'string' && template !== '' ? this.translate(template).replace(/\\{count\\}/g, String(count)) : ''\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-grouped-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(var(--cn-grouped-cards-columns, 2), minmax(0, 1fr));\n\tgap: 16px;\n}\n\n@media (max-width: 700px) {\n\t.cn-grouped-cards__grid {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n\n.cn-grouped-cards__group {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tpadding: 12px 14px 14px;\n\tborder-radius: var(--border-radius-large, 10px);\n\tbackground: var(--color-background-hover);\n}\n\n.cn-grouped-cards__group--extra {\n\tbackground: transparent;\n\tborder: 1px dashed var(--color-border-dark);\n}\n\n.cn-grouped-cards__group-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-grouped-cards__group-title {\n\tmargin: 0;\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-grouped-cards__count {\n\tpadding: 2px 8px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 12px;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n}\n\n.cn-grouped-cards__items {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-grouped-cards__item {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-grouped-cards__item-title {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-grouped-cards__item-meta,\n.cn-grouped-cards__extra-text,\n.cn-grouped-cards__group-empty,\n.cn-grouped-cards__state {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-grouped-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createVNode","_Fragment","_renderList","_createElementVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnGroupedCardsWidget.vue3.js","sources":["../../../../src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue?vue&type=template&id=23662746&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-grouped-cards\" data-testid=\"cn-grouped-cards\">\n\t\t<p v-if=\"error\" class=\"cn-grouped-cards__state\" data-testid=\"cn-grouped-cards-error\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && groups.length === 0\" class=\"cn-grouped-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"groups.length === 0 && !extra\" class=\"cn-grouped-cards__state\" data-testid=\"cn-grouped-cards-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<div\n\t\t\tv-else\n\t\t\tclass=\"cn-grouped-cards__grid\"\n\t\t\t:style=\"{ '--cn-grouped-cards-columns': columns }\">\n\t\t\t<section\n\t\t\t\tv-for=\"group in groups\"\n\t\t\t\t:key=\"group.key\"\n\t\t\t\tclass=\"cn-grouped-cards__group\"\n\t\t\t\t:aria-labelledby=\"group.headingId\"\n\t\t\t\tdata-testid=\"cn-grouped-cards-group\">\n\t\t\t\t<header class=\"cn-grouped-cards__group-header\">\n\t\t\t\t\t<h4 :id=\"group.headingId\" class=\"cn-grouped-cards__group-title\">\n\t\t\t\t\t\t{{ group.title }}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<span v-if=\"group.count\" class=\"cn-grouped-cards__count\" data-testid=\"cn-grouped-cards-count\">{{ group.count }}</span>\n\t\t\t\t</header>\n\t\t\t\t<ul v-if=\"group.items.length > 0\" class=\"cn-grouped-cards__items\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"item in group.items\"\n\t\t\t\t\t\t:key=\"item.key\"\n\t\t\t\t\t\tclass=\"cn-grouped-cards__item\"\n\t\t\t\t\t\tdata-testid=\"cn-grouped-cards-item\">\n\t\t\t\t\t\t<span class=\"cn-grouped-cards__item-title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-grouped-cards__item-meta\">{{ item.meta }}</span>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<p v-else class=\"cn-grouped-cards__group-empty\">\n\t\t\t\t\t{{ groupEmptyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<!-- `groupAction`: one action per group (\"Add a task\" on a phase),\n\t\t\t\t its `@group.<path>` tokens filled from the group. -->\n\t\t\t\t<CnActionButtons\n\t\t\t\t\tv-if=\"group.action\"\n\t\t\t\t\tclass=\"cn-grouped-cards__group-action\"\n\t\t\t\t\t:actions=\"[group.action]\"\n\t\t\t\t\tdata-testid=\"cn-grouped-cards-group-action\" />\n\t\t\t</section>\n\t\t\t<section\n\t\t\t\tv-if=\"extra\"\n\t\t\t\tclass=\"cn-grouped-cards__group cn-grouped-cards__group--extra\"\n\t\t\t\t:aria-labelledby=\"extra.headingId\"\n\t\t\t\tdata-testid=\"cn-grouped-cards-extra\">\n\t\t\t\t<header class=\"cn-grouped-cards__group-header\">\n\t\t\t\t\t<h4 :id=\"extra.headingId\" class=\"cn-grouped-cards__group-title\">\n\t\t\t\t\t\t{{ extra.title }}\n\t\t\t\t\t</h4>\n\t\t\t\t</header>\n\t\t\t\t<p class=\"cn-grouped-cards__extra-text\">\n\t\t\t\t\t{{ extra.text }}\n\t\t\t\t</p>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { inject, ref } from 'vue'\nimport { useEndpointSource } from '../../composables/useEndpointSource.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { languageMapText } from '../../utils/languageMapText.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { CnActionButtons } from '../CnActionButtons/index.js'\n\nlet uid = 0\n\n/**\n * Unwrap a value that may be a ref.\n *\n * @param {unknown} value The value.\n * @return {unknown} The plain value.\n */\nfunction unwrap(value) {\n\treturn value && typeof value === 'object' && 'value' in value ? value.value : value\n}\n\n/**\n * A row as the templates read it: a language map (`{ nl, en }`) becomes its\n * text in the user's language, and a list of plain values is joined with \", \".\n * Nested objects stay as they are, for a template that reads a deeper path.\n *\n * @param {object} row The row.\n * @return {object} A shallow copy with readable values.\n */\nfunction readable(row) {\n\tif (!row || typeof row !== 'object') {\n\t\treturn {}\n\t}\n\tconst out = {}\n\tfor (const [key, value] of Object.entries(row)) {\n\t\tif (Array.isArray(value)) {\n\t\t\tconst plain = value.filter((entry) => typeof entry === 'string' || typeof entry === 'number')\n\t\t\tout[key] = plain.length === value.length ? plain.join(', ') : value\n\t\t} else if (value && typeof value === 'object' && Object.values(value).every((entry) => typeof entry === 'string')) {\n\t\t\tout[key] = languageMapText(value)\n\t\t} else {\n\t\t\tout[key] = value\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * CnGroupedCardsWidget — groups of cards side by side, read from an app\n * endpoint: a case type's phases, each with the tasks it runs, and a dashed\n * box of what is available in every phase (board DqZaaktype, Fasen en taken).\n *\n * Content-only on a card surface: the detail host or the dashboard draws the\n * titled card around it.\n */\nexport default {\n\tname: 'CnGroupedCardsWidget',\n\n\tcomponents: { CnActionButtons, NcLoadingIcon },\n\n\tinject: {\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's app config map, for money tokens in a template. */\n\t\tcnAppConfig: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget config: `{ endpointSource, groupsPath?, itemsKey?,\n\t\t * groupTitle?, groupCount?, itemTitle?, itemMeta?, columns?,\n\t\t * extraGroup?, emptyText?, groupEmptyText?, errorText? }`.\n\t\t *\n\t\t * - `endpointSource` (`{ url, params?, responsePath? }`) reads the payload; tokens\n\t\t * such as `@objectId` resolve as on every widget.\n\t\t * - `groupsPath` (default `groups`) is the list of groups in the payload, and\n\t\t * `itemsKey` (default `items`) the list of cards in each group.\n\t\t * - `groupTitle` and `itemTitle` are templates (`\"Phase {order}: {name}\"`,\n\t\t * `\"{title}\"`); `itemMeta` is a template or a list of templates joined\n\t\t * with \" · \", a part whose token is empty being left out.\n\t\t * - `groupCount` (`{ one, other }`, `{count}` filled in) draws a count pill.\n\t\t * - `extraGroup` (`{ title, path, itemText, join? }`) closes the grid with a\n\t\t * dashed box whose entries, read at `path` of the payload, are one line.\n\t\t * - `columns` (default 2) is the number of groups side by side.\n\t\t * - `groupAction` is an action drawn at the foot of every group (as\n\t\t * CnActionButtons draws it), with `@group.<path>` in its strings read\n\t\t * from that group: `{ type: \"open-form\", endpoint: { url:\n\t\t * \"/apps/x/api/phases/@group.id/steps\", method: \"POST\" }, ... }`\n\t\t * adds a card to the group.\n\t\t *\n\t\t * A language map (`{ nl, en }`) reads in the user's language, and a list\n\t\t * of plain values reads joined with \", \". Texts go through the app's\n\t\t * translate.\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\n\t\t/** The current record's id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current record, for `@object.<field>` tokens. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\tconst objectCtxRaw = inject('cnObjectContext', null)\n\t\tconst workspaceRaw = inject('cnWorkspaceContext', null)\n\t\tconst appConfigRaw = inject('cnAppConfig', ref({}))\n\t\tconst { data, loading, error } = useEndpointSource(\n\t\t\t() => props.content?.endpointSource || null,\n\t\t\t{\n\t\t\t\tctx: () => {\n\t\t\t\t\tconst injected = unwrap(objectCtxRaw) || {}\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...injected,\n\t\t\t\t\t\tobjectId: props.objectId ?? injected.objectId ?? props.objectData?.id ?? null,\n\t\t\t\t\t\tobject: props.objectData || injected.object || null,\n\t\t\t\t\t\tworkspace: unwrap(workspaceRaw) || {},\n\t\t\t\t\t\tconfig: unwrapAppConfig(appConfigRaw) || {},\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t\tuid += 1\n\t\treturn { payload: data, loading, error, idPrefix: `cn-grouped-cards-${uid}` }\n\t},\n\n\tcomputed: {\n\t\t/** @return {number} The groups side by side. */\n\t\tcolumns() {\n\t\t\tconst value = Number(this.content?.columns)\n\t\t\treturn Number.isFinite(value) && value >= 1 ? Math.min(4, Math.floor(value)) : 2\n\t\t},\n\n\t\t/**\n\t\t * The groups ready to draw.\n\t\t *\n\t\t * @return {Array<{key: string, headingId: string, title: string, count: string, items: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint\n\t\t */\n\t\tgroups() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst list = readPath(this.payload, c.groupsPath || 'groups')\n\t\t\tif (!Array.isArray(list)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn list.filter((group) => group && typeof group === 'object').map((raw, index) => {\n\t\t\t\tconst group = readable(raw)\n\t\t\t\tconst itemList = Array.isArray(raw[c.itemsKey || 'items']) ? raw[c.itemsKey || 'items'] : []\n\t\t\t\tconst items = itemList.filter((item) => item && typeof item === 'object').map((rawItem, itemIndex) => {\n\t\t\t\t\tconst item = readable(rawItem)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(rawItem.id ?? `item-${itemIndex}`),\n\t\t\t\t\t\ttitle: this.fill(c.itemTitle || '{title}', item),\n\t\t\t\t\t\tmeta: this.metaOf(item),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\treturn {\n\t\t\t\t\tkey: String(raw.id ?? `group-${index}`),\n\t\t\t\t\theadingId: `${this.idPrefix}-group-${index}`,\n\t\t\t\t\ttitle: this.fill(c.groupTitle || '{name}', group),\n\t\t\t\t\tcount: this.countOf(items.length),\n\t\t\t\t\titems,\n\t\t\t\t\taction: this.groupActionFor(raw, index),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The closing box of entries available everywhere, or null.\n\t\t *\n\t\t * @return {{headingId: string, title: string, text: string}|null} The box.\n\t\t * @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint\n\t\t */\n\t\textra() {\n\t\t\tconst spec = this.content?.extraGroup\n\t\t\tif (!spec || typeof spec !== 'object' || !spec.path) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst list = readPath(this.payload, spec.path)\n\t\t\tif (!Array.isArray(list) || list.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst parts = list\n\t\t\t\t.map((entry) => (entry && typeof entry === 'object' ? this.fill(spec.itemText || '{label}', readable(entry)) : languageMapText(entry)))\n\t\t\t\t.filter((text) => text !== '')\n\t\t\tif (parts.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn {\n\t\t\t\theadingId: `${this.idPrefix}-extra`,\n\t\t\t\ttitle: this.translate(spec.title || 'Always available'),\n\t\t\t\ttext: parts.join(typeof spec.join === 'string' ? spec.join : ' · '),\n\t\t\t}\n\t\t},\n\n\t\t/** @return {string} The empty text. */\n\t\temptyLabel() {\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing to show yet')\n\t\t},\n\n\t\t/** @return {string} The text of a group without cards. */\n\t\tgroupEmptyLabel() {\n\t\t\treturn this.content?.groupEmptyText ? this.translate(this.content.groupEmptyText) : t('nextcloud-vue', 'Nothing here yet')\n\t\t},\n\n\t\t/** @return {string} The error text. */\n\t\terrorLabel() {\n\t\t\treturn this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')\n\t\t},\n\n\t\t/** @return {string} The loading label. */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The `groupAction` for one group, with every `@group.<path>` token in\n\t\t * its strings read from that group, and its id made unique per group.\n\t\t * Null without a `groupAction`, or for a group missing a value a token\n\t\t * names (the action would address no group).\n\t\t *\n\t\t * @param {object} raw The group as read.\n\t\t * @param {number} index The group's position.\n\t\t * @return {object|null} The action.\n\t\t * @spec openspec/changes/grouped-cards-group-action/specs/grouped-cards-group-action/spec.md#requirement-every-group-can-carry-an-action\n\t\t */\n\t\tgroupActionFor(raw, index) {\n\t\t\tconst action = this.content?.groupAction\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet missing = false\n\t\t\tconst fill = (value) => {\n\t\t\t\tif (typeof value === 'string') {\n\t\t\t\t\treturn value.replace(/@group\\.([\\w.]+)/g, (token, path) => {\n\t\t\t\t\t\tconst found = readPath(raw, path)\n\t\t\t\t\t\tif (found === undefined || found === null || found === '') {\n\t\t\t\t\t\t\tmissing = true\n\t\t\t\t\t\t\treturn ''\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn encodeURIComponent(String(found))\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\treturn value.map(fill)\n\t\t\t\t}\n\t\t\t\tif (value && typeof value === 'object') {\n\t\t\t\t\treturn Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, fill(inner)]))\n\t\t\t\t}\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst filled = fill(action)\n\t\t\treturn missing ? null : { ...filled, id: `${action.id || 'group-action'}-${index}` }\n\t\t},\n\n\t\t/**\n\t\t * Translate a text through the host app's catalogue.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttranslate(text) {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t\treturn fn(text)\n\t\t},\n\n\t\t/**\n\t\t * Fill a template from a row, after translating it.\n\t\t *\n\t\t * @param {string} template The template.\n\t\t * @param {object} row The row.\n\t\t * @return {string} The text, '' when a token is empty.\n\t\t */\n\t\tfill(template, row) {\n\t\t\tif (typeof template !== 'string' || template === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn fillTemplate(this.translate(template), row, new Date(), { config: unwrapAppConfig(this.cnAppConfig) })\n\t\t},\n\n\t\t/**\n\t\t * A card's meta line: one template, or several joined with \" · \", a\n\t\t * part whose token is empty being left out with its separator.\n\t\t *\n\t\t * @param {object} item The card's row.\n\t\t * @return {string} The line.\n\t\t */\n\t\tmetaOf(item) {\n\t\t\tconst spec = this.content?.itemMeta\n\t\t\tconst parts = Array.isArray(spec) ? spec : (typeof spec === 'string' ? [spec] : [])\n\t\t\treturn parts.map((part) => this.fill(part, item)).filter((text) => text !== '').join(' · ')\n\t\t},\n\n\t\t/**\n\t\t * The count pill of a group (`groupCount: { one, other }`).\n\t\t *\n\t\t * @param {number} count The number of cards.\n\t\t * @return {string} The text, '' without `groupCount`.\n\t\t */\n\t\tcountOf(count) {\n\t\t\tconst spec = this.content?.groupCount\n\t\t\tif (!spec || typeof spec !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst template = count === 1 ? spec.one : spec.other\n\t\t\treturn typeof template === 'string' && template !== '' ? this.translate(template).replace(/\\{count\\}/g, String(count)) : ''\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-grouped-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(var(--cn-grouped-cards-columns, 2), minmax(0, 1fr));\n\tgap: 16px;\n}\n\n@media (max-width: 700px) {\n\t.cn-grouped-cards__grid {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n\n.cn-grouped-cards__group {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tpadding: 12px 14px 14px;\n\tborder-radius: var(--border-radius-large, 10px);\n\tbackground: var(--color-background-hover);\n}\n\n.cn-grouped-cards__group--extra {\n\tbackground: transparent;\n\tborder: 1px dashed var(--color-border-dark);\n}\n\n.cn-grouped-cards__group-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-grouped-cards__group-title {\n\tmargin: 0;\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-grouped-cards__group-action {\n\tmargin-top: 8px;\n}\n\n.cn-grouped-cards__count {\n\tpadding: 2px 8px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 12px;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n}\n\n.cn-grouped-cards__items {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-grouped-cards__item {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-grouped-cards__item-title {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-grouped-cards__item-meta,\n.cn-grouped-cards__extra-text,\n.cn-grouped-cards__group-empty,\n.cn-grouped-cards__state {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-grouped-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createVNode","_Fragment","_renderList","_createElementVNode","_createBlock"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnMacrosWidget.vue2.js';
2
2
  import { render } from './CnMacrosWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-982c6094";
5
+ script.__scopeId = "data-v-0d95ed8d";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,6 +1,6 @@
1
1
  import axios__default from '@nextcloud/axios';
2
2
  import { emit } from '@nextcloud/event-bus';
3
- import { translate } from '@nextcloud/l10n';
3
+ import { translate, translatePlural } from '@nextcloud/l10n';
4
4
  import { generateUrl } from '@nextcloud/router';
5
5
  import { NcButton, NcNoteCard } from '@nextcloud/vue';
6
6
  import { declaredMacros } from '../../utils/declaredMacros.js';
@@ -73,6 +73,7 @@ var script = {
73
73
 
74
74
  methods: {
75
75
  t: translate,
76
+ n: translatePlural,
76
77
 
77
78
  /**
78
79
  * Translate an app-supplied text through the host app when it offers it.
@@ -126,8 +127,11 @@ var script = {
126
127
  const entry = (res?.data?.results || [])[0];
127
128
  const key = entry?.changed?.declaredAction;
128
129
  const macro = this.macros.find((candidate) => candidate.key === key);
130
+ const changes = Array.isArray(entry?.changed?.changes)
131
+ ? entry.changed.changes.map((change) => (change && typeof change.step === 'string' ? change.step : '')).filter(Boolean)
132
+ : [];
129
133
  this.lastRun = macro && entry.created
130
- ? { name: macro.name, date: new Date(entry.created).toLocaleString() }
134
+ ? { name: this.tr(macro.name), date: new Date(entry.created).toLocaleString(), changes }
131
135
  : null;
132
136
  } catch {
133
137
  this.lastRun = null;
@@ -146,8 +150,12 @@ var script = {
146
150
  this.error = '';
147
151
  try {
148
152
  await axios__default.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }));
149
- this.lastRun = { name: macro.name, date: new Date().toLocaleString() };
153
+ this.lastRun = { name: this.tr(macro.name), date: new Date().toLocaleString(), changes: [] };
150
154
  emit('cn:page:refresh', { source: 'cn-macros-widget' });
155
+ // The audit entry of this run says what it changed.
156
+ const local = this.lastRun;
157
+ await this.loadLastRun();
158
+ this.lastRun = this.lastRun || local;
151
159
  } catch (e) {
152
160
  this.error = translate('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' });
153
161
  } finally {
@@ -1 +1 @@
1
- {"version":3,"file":"CnMacrosWidget.vue2.js","sources":["../../../../src/components/CnMacrosWidget/CnMacrosWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<!--\n CnMacrosWidget — the macros a record's schema declares, each with a button\n that runs it (`type: \"macros\"`, DqZaakAssistent \"Macro's\").\n\n A macro is a declared action (`configuration['x-openregister-action']`) with\n `macro: true` and a bound manual flow; OpenRegister runs it as the person,\n under the action's own right, through\n `POST /api/objects/{register}/{schema}/{id}/actions/{action}`. The widget\n lists what the schema declares and runs what is pressed: it adds no rights and\n no list of its own. The last run comes from the record's audit trail\n (`action.macro`), and after a run the page is asked to refresh.\n-->\n<template>\n\t<div class=\"cn-macros-widget\" data-testid=\"cn-macros-widget\">\n\t\t<p v-if=\"content.description\" class=\"cn-macros-widget__description\">\n\t\t\t{{ tr(content.description) }}\n\t\t</p>\n\t\t<p v-if=\"loading\" class=\"cn-macros-widget__muted\">\n\t\t\t{{ t('nextcloud-vue', 'Loading …') }}\n\t\t</p>\n\t\t<p v-else-if=\"macros.length === 0\" class=\"cn-macros-widget__muted\" data-testid=\"cn-macros-widget-empty\">\n\t\t\t{{ content.emptyText ? tr(content.emptyText) : t('nextcloud-vue', 'This record has no macros') }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-macros-widget__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"macro in macros\"\n\t\t\t\t:key=\"macro.key\"\n\t\t\t\tclass=\"cn-macros-widget__row\"\n\t\t\t\t:data-testid=\"`cn-macros-widget-row-${macro.key}`\">\n\t\t\t\t<div class=\"cn-macros-widget__text\">\n\t\t\t\t\t<span class=\"cn-macros-widget__name\">{{ macro.name }}</span>\n\t\t\t\t\t<span v-if=\"macro.description\" class=\"cn-macros-widget__hint\">{{ macro.description }}</span>\n\t\t\t\t</div>\n\t\t\t\t<NcButton\n\t\t\t\t\t:disabled=\"running !== ''\"\n\t\t\t\t\t:data-testid=\"`cn-macros-widget-run-${macro.key}`\"\n\t\t\t\t\t@click=\"run(macro)\">\n\t\t\t\t\t{{ running === macro.key ? t('nextcloud-vue', 'Running …') : t('nextcloud-vue', 'Execute') }}\n\t\t\t\t</NcButton>\n\t\t\t</li>\n\t\t</ul>\n\t\t<NcNoteCard\n\t\t\tv-if=\"error\"\n\t\t\ttype=\"error\"\n\t\t\tdata-testid=\"cn-macros-widget-error\">\n\t\t\t{{ error }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"lastRun\"\n\t\t\ttype=\"success\"\n\t\t\tdata-testid=\"cn-macros-widget-last-run\">\n\t\t\t{{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { emit } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard } from '@nextcloud/vue'\nimport { declaredMacros } from '../../utils/declaredMacros.js'\n\nexport default {\n\tname: 'CnMacrosWidget',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcNoteCard,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `description` (the line under the title),\n\t\t * `emptyText`, both translated.\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tmacros: [],\n\t\t\tloading: false,\n\t\t\trunning: '',\n\t\t\terror: '',\n\t\t\tlastRun: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {{register: string, schema: string, id: string}} Where the record lives. */\n\t\ttarget() {\n\t\t\tconst self = this.objectData?.['@self'] || {}\n\t\t\treturn {\n\t\t\t\tregister: String(self.register ?? ''),\n\t\t\t\tschema: String(self.schema ?? ''),\n\t\t\t\tid: String(this.objectId || self.id || this.objectData?.id || ''),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\t'target.schema': {\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\tt,\n\n\t\t/**\n\t\t * Translate an app-supplied text through the host app when it offers it.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Read the schema's macros and the record's last macro run.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tif (schema === '' || id === '') {\n\t\t\t\tthis.macros = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(generateUrl('/apps/openregister/api/schemas/{schema}', { schema }), register ? { params: { register } } : undefined)\n\t\t\t\tthis.macros = declaredMacros(res?.data || null)\n\t\t\t} catch {\n\t\t\t\tthis.macros = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t\tif (this.macros.length > 0) {\n\t\t\t\tawait this.loadLastRun()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The record's most recent macro run, from its audit trail. Left empty\n\t\t * when the trail cannot be read: it is a convenience, not a fact the\n\t\t * widget may invent.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLastRun() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(\n\t\t\t\t\tgenerateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails', { register, schema, id }),\n\t\t\t\t\t{ params: { action: 'action.macro', _limit: 1, '_sort[created]': 'DESC' } },\n\t\t\t\t)\n\t\t\t\tconst entry = (res?.data?.results || [])[0]\n\t\t\t\tconst key = entry?.changed?.declaredAction\n\t\t\t\tconst macro = this.macros.find((candidate) => candidate.key === key)\n\t\t\t\tthis.lastRun = macro && entry.created\n\t\t\t\t\t? { name: macro.name, date: new Date(entry.created).toLocaleString() }\n\t\t\t\t\t: null\n\t\t\t} catch {\n\t\t\t\tthis.lastRun = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run one macro as the person, then ask the page to refresh.\n\t\t *\n\t\t * @param {{key: string, name: string}} macro The macro.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync run(macro) {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tthis.running = macro.key\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tawait axios.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }))\n\t\t\t\tthis.lastRun = { name: macro.name, date: new Date().toLocaleString() }\n\t\t\t\temit('cn:page:refresh', { source: 'cn-macros-widget' })\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = t('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' })\n\t\t\t} finally {\n\t\t\t\tthis.running = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-macros-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-macros-widget__description,\n.cn-macros-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-macros-widget__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-macros-widget__row {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tpadding-block: 10px;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-macros-widget__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-macros-widget__name {\n\tfont-weight: 600;\n}\n\n.cn-macros-widget__hint {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n</style>\n"],"names":["t","axios"],"mappings":"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"}
1
+ {"version":3,"file":"CnMacrosWidget.vue2.js","sources":["../../../../src/components/CnMacrosWidget/CnMacrosWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<!--\n CnMacrosWidget — the macros a record's schema declares, each with a button\n that runs it (`type: \"macros\"`, DqZaakAssistent \"Macro's\").\n\n A macro is a declared action (`configuration['x-openregister-action']`) with\n `macro: true` and a bound manual flow; OpenRegister runs it as the person,\n under the action's own right, through\n `POST /api/objects/{register}/{schema}/{id}/actions/{action}`. The widget\n lists what the schema declares and runs what is pressed: it adds no rights and\n no list of its own. The last run comes from the record's audit trail\n (`action.macro`), and after a run the page is asked to refresh.\n-->\n<template>\n\t<div class=\"cn-macros-widget\" data-testid=\"cn-macros-widget\">\n\t\t<p v-if=\"content.description\" class=\"cn-macros-widget__description\">\n\t\t\t{{ tr(content.description) }}\n\t\t</p>\n\t\t<p v-if=\"loading\" class=\"cn-macros-widget__muted\">\n\t\t\t{{ t('nextcloud-vue', 'Loading …') }}\n\t\t</p>\n\t\t<p v-else-if=\"macros.length === 0\" class=\"cn-macros-widget__muted\" data-testid=\"cn-macros-widget-empty\">\n\t\t\t{{ content.emptyText ? tr(content.emptyText) : t('nextcloud-vue', 'This record has no macros') }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-macros-widget__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"macro in macros\"\n\t\t\t\t:key=\"macro.key\"\n\t\t\t\tclass=\"cn-macros-widget__row\"\n\t\t\t\t:data-testid=\"`cn-macros-widget-row-${macro.key}`\">\n\t\t\t\t<div class=\"cn-macros-widget__text\">\n\t\t\t\t\t<span class=\"cn-macros-widget__name\">{{ tr(macro.name) }}</span>\n\t\t\t\t\t<span v-if=\"macro.description\" class=\"cn-macros-widget__hint\">{{ tr(macro.description) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<NcButton\n\t\t\t\t\t:disabled=\"running !== ''\"\n\t\t\t\t\t:data-testid=\"`cn-macros-widget-run-${macro.key}`\"\n\t\t\t\t\t@click=\"run(macro)\">\n\t\t\t\t\t{{ running === macro.key ? t('nextcloud-vue', 'Running …') : t('nextcloud-vue', 'Execute') }}\n\t\t\t\t</NcButton>\n\t\t\t</li>\n\t\t</ul>\n\t\t<NcNoteCard\n\t\t\tv-if=\"error\"\n\t\t\ttype=\"error\"\n\t\t\tdata-testid=\"cn-macros-widget-error\">\n\t\t\t{{ error }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"lastRun\"\n\t\t\ttype=\"success\"\n\t\t\tdata-testid=\"cn-macros-widget-last-run\">\n\t\t\t{{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}<template v-if=\"lastRun.changes.length\">\n\t\t\t\t{{ n('nextcloud-vue', '{count} change: {list}', '{count} changes: {list}', lastRun.changes.length, { count: lastRun.changes.length, list: lastRun.changes.join(', ') }) }}\n\t\t\t</template>\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { emit } from '@nextcloud/event-bus'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard } from '@nextcloud/vue'\nimport { declaredMacros } from '../../utils/declaredMacros.js'\n\nexport default {\n\tname: 'CnMacrosWidget',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcNoteCard,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `description` (the line under the title),\n\t\t * `emptyText`, both translated.\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tmacros: [],\n\t\t\tloading: false,\n\t\t\trunning: '',\n\t\t\terror: '',\n\t\t\tlastRun: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {{register: string, schema: string, id: string}} Where the record lives. */\n\t\ttarget() {\n\t\t\tconst self = this.objectData?.['@self'] || {}\n\t\t\treturn {\n\t\t\t\tregister: String(self.register ?? ''),\n\t\t\t\tschema: String(self.schema ?? ''),\n\t\t\t\tid: String(this.objectId || self.id || this.objectData?.id || ''),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\t'target.schema': {\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\tt,\n\t\tn,\n\n\t\t/**\n\t\t * Translate an app-supplied text through the host app when it offers it.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Read the schema's macros and the record's last macro run.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tif (schema === '' || id === '') {\n\t\t\t\tthis.macros = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(generateUrl('/apps/openregister/api/schemas/{schema}', { schema }), register ? { params: { register } } : undefined)\n\t\t\t\tthis.macros = declaredMacros(res?.data || null)\n\t\t\t} catch {\n\t\t\t\tthis.macros = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t\tif (this.macros.length > 0) {\n\t\t\t\tawait this.loadLastRun()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The record's most recent macro run, from its audit trail. Left empty\n\t\t * when the trail cannot be read: it is a convenience, not a fact the\n\t\t * widget may invent.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLastRun() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(\n\t\t\t\t\tgenerateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails', { register, schema, id }),\n\t\t\t\t\t{ params: { action: 'action.macro', _limit: 1, '_sort[created]': 'DESC' } },\n\t\t\t\t)\n\t\t\t\tconst entry = (res?.data?.results || [])[0]\n\t\t\t\tconst key = entry?.changed?.declaredAction\n\t\t\t\tconst macro = this.macros.find((candidate) => candidate.key === key)\n\t\t\t\tconst changes = Array.isArray(entry?.changed?.changes)\n\t\t\t\t\t? entry.changed.changes.map((change) => (change && typeof change.step === 'string' ? change.step : '')).filter(Boolean)\n\t\t\t\t\t: []\n\t\t\t\tthis.lastRun = macro && entry.created\n\t\t\t\t\t? { name: this.tr(macro.name), date: new Date(entry.created).toLocaleString(), changes }\n\t\t\t\t\t: null\n\t\t\t} catch {\n\t\t\t\tthis.lastRun = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run one macro as the person, then ask the page to refresh.\n\t\t *\n\t\t * @param {{key: string, name: string}} macro The macro.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync run(macro) {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tthis.running = macro.key\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tawait axios.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }))\n\t\t\t\tthis.lastRun = { name: this.tr(macro.name), date: new Date().toLocaleString(), changes: [] }\n\t\t\t\temit('cn:page:refresh', { source: 'cn-macros-widget' })\n\t\t\t\t// The audit entry of this run says what it changed.\n\t\t\t\tconst local = this.lastRun\n\t\t\t\tawait this.loadLastRun()\n\t\t\t\tthis.lastRun = this.lastRun || local\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = t('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' })\n\t\t\t} finally {\n\t\t\t\tthis.running = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-macros-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-macros-widget__description,\n.cn-macros-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-macros-widget__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-macros-widget__row {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tpadding-block: 10px;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-macros-widget__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-macros-widget__name {\n\tfont-weight: 600;\n}\n\n.cn-macros-widget__hint {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n</style>\n"],"names":["t","n","axios"],"mappings":"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"}
@@ -49,9 +49,9 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
49
49
  "data-testid": `cn-macros-widget-row-${macro.key}`
50
50
  }, [
51
51
  createElementVNode("div", _hoisted_7, [
52
- createElementVNode("span", _hoisted_8, toDisplayString(macro.name), 1 /* TEXT */),
52
+ createElementVNode("span", _hoisted_8, toDisplayString($options.tr(macro.name)), 1 /* TEXT */),
53
53
  (macro.description)
54
- ? (openBlock(), createElementBlock("span", _hoisted_9, toDisplayString(macro.description), 1 /* TEXT */))
54
+ ? (openBlock(), createElementBlock("span", _hoisted_9, toDisplayString($options.tr(macro.description)), 1 /* TEXT */))
55
55
  : createCommentVNode("v-if", true)
56
56
  ]),
57
57
  createVNode(_component_NcButton, {
@@ -85,7 +85,12 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
85
85
  "data-testid": "cn-macros-widget-last-run"
86
86
  }, {
87
87
  default: withCtx(() => [
88
- createTextVNode(toDisplayString($options.t('nextcloud-vue', 'Last run: {name}, {date}', { name: $data.lastRun.name, date: $data.lastRun.date })), 1 /* TEXT */)
88
+ createTextVNode(toDisplayString($options.t('nextcloud-vue', 'Last run: {name}, {date}', { name: $data.lastRun.name, date: $data.lastRun.date })), 1 /* TEXT */),
89
+ ($data.lastRun.changes.length)
90
+ ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
91
+ createTextVNode(toDisplayString($options.n('nextcloud-vue', '{count} change: {list}', '{count} changes: {list}', $data.lastRun.changes.length, { count: $data.lastRun.changes.length, list: $data.lastRun.changes.join(', ') })), 1 /* TEXT */)
92
+ ], 64 /* STABLE_FRAGMENT */))
93
+ : createCommentVNode("v-if", true)
89
94
  ]),
90
95
  _: 1 /* STABLE */
91
96
  }))
@@ -1 +1 @@
1
- {"version":3,"file":"CnMacrosWidget.vue3.js","sources":["../../../../src/components/CnMacrosWidget/CnMacrosWidget.vue?vue&type=template&id=982c6094&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<!--\n CnMacrosWidget — the macros a record's schema declares, each with a button\n that runs it (`type: \"macros\"`, DqZaakAssistent \"Macro's\").\n\n A macro is a declared action (`configuration['x-openregister-action']`) with\n `macro: true` and a bound manual flow; OpenRegister runs it as the person,\n under the action's own right, through\n `POST /api/objects/{register}/{schema}/{id}/actions/{action}`. The widget\n lists what the schema declares and runs what is pressed: it adds no rights and\n no list of its own. The last run comes from the record's audit trail\n (`action.macro`), and after a run the page is asked to refresh.\n-->\n<template>\n\t<div class=\"cn-macros-widget\" data-testid=\"cn-macros-widget\">\n\t\t<p v-if=\"content.description\" class=\"cn-macros-widget__description\">\n\t\t\t{{ tr(content.description) }}\n\t\t</p>\n\t\t<p v-if=\"loading\" class=\"cn-macros-widget__muted\">\n\t\t\t{{ t('nextcloud-vue', 'Loading …') }}\n\t\t</p>\n\t\t<p v-else-if=\"macros.length === 0\" class=\"cn-macros-widget__muted\" data-testid=\"cn-macros-widget-empty\">\n\t\t\t{{ content.emptyText ? tr(content.emptyText) : t('nextcloud-vue', 'This record has no macros') }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-macros-widget__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"macro in macros\"\n\t\t\t\t:key=\"macro.key\"\n\t\t\t\tclass=\"cn-macros-widget__row\"\n\t\t\t\t:data-testid=\"`cn-macros-widget-row-${macro.key}`\">\n\t\t\t\t<div class=\"cn-macros-widget__text\">\n\t\t\t\t\t<span class=\"cn-macros-widget__name\">{{ macro.name }}</span>\n\t\t\t\t\t<span v-if=\"macro.description\" class=\"cn-macros-widget__hint\">{{ macro.description }}</span>\n\t\t\t\t</div>\n\t\t\t\t<NcButton\n\t\t\t\t\t:disabled=\"running !== ''\"\n\t\t\t\t\t:data-testid=\"`cn-macros-widget-run-${macro.key}`\"\n\t\t\t\t\t@click=\"run(macro)\">\n\t\t\t\t\t{{ running === macro.key ? t('nextcloud-vue', 'Running …') : t('nextcloud-vue', 'Execute') }}\n\t\t\t\t</NcButton>\n\t\t\t</li>\n\t\t</ul>\n\t\t<NcNoteCard\n\t\t\tv-if=\"error\"\n\t\t\ttype=\"error\"\n\t\t\tdata-testid=\"cn-macros-widget-error\">\n\t\t\t{{ error }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"lastRun\"\n\t\t\ttype=\"success\"\n\t\t\tdata-testid=\"cn-macros-widget-last-run\">\n\t\t\t{{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { emit } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard } from '@nextcloud/vue'\nimport { declaredMacros } from '../../utils/declaredMacros.js'\n\nexport default {\n\tname: 'CnMacrosWidget',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcNoteCard,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `description` (the line under the title),\n\t\t * `emptyText`, both translated.\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tmacros: [],\n\t\t\tloading: false,\n\t\t\trunning: '',\n\t\t\terror: '',\n\t\t\tlastRun: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {{register: string, schema: string, id: string}} Where the record lives. */\n\t\ttarget() {\n\t\t\tconst self = this.objectData?.['@self'] || {}\n\t\t\treturn {\n\t\t\t\tregister: String(self.register ?? ''),\n\t\t\t\tschema: String(self.schema ?? ''),\n\t\t\t\tid: String(this.objectId || self.id || this.objectData?.id || ''),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\t'target.schema': {\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\tt,\n\n\t\t/**\n\t\t * Translate an app-supplied text through the host app when it offers it.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Read the schema's macros and the record's last macro run.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tif (schema === '' || id === '') {\n\t\t\t\tthis.macros = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(generateUrl('/apps/openregister/api/schemas/{schema}', { schema }), register ? { params: { register } } : undefined)\n\t\t\t\tthis.macros = declaredMacros(res?.data || null)\n\t\t\t} catch {\n\t\t\t\tthis.macros = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t\tif (this.macros.length > 0) {\n\t\t\t\tawait this.loadLastRun()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The record's most recent macro run, from its audit trail. Left empty\n\t\t * when the trail cannot be read: it is a convenience, not a fact the\n\t\t * widget may invent.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLastRun() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(\n\t\t\t\t\tgenerateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails', { register, schema, id }),\n\t\t\t\t\t{ params: { action: 'action.macro', _limit: 1, '_sort[created]': 'DESC' } },\n\t\t\t\t)\n\t\t\t\tconst entry = (res?.data?.results || [])[0]\n\t\t\t\tconst key = entry?.changed?.declaredAction\n\t\t\t\tconst macro = this.macros.find((candidate) => candidate.key === key)\n\t\t\t\tthis.lastRun = macro && entry.created\n\t\t\t\t\t? { name: macro.name, date: new Date(entry.created).toLocaleString() }\n\t\t\t\t\t: null\n\t\t\t} catch {\n\t\t\t\tthis.lastRun = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run one macro as the person, then ask the page to refresh.\n\t\t *\n\t\t * @param {{key: string, name: string}} macro The macro.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync run(macro) {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tthis.running = macro.key\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tawait axios.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }))\n\t\t\t\tthis.lastRun = { name: macro.name, date: new Date().toLocaleString() }\n\t\t\t\temit('cn:page:refresh', { source: 'cn-macros-widget' })\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = t('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' })\n\t\t\t} finally {\n\t\t\t\tthis.running = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-macros-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-macros-widget__description,\n.cn-macros-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-macros-widget__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-macros-widget__row {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tpadding-block: 10px;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-macros-widget__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-macros-widget__name {\n\tfont-weight: 600;\n}\n\n.cn-macros-widget__hint {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createElementVNode","_createVNode"],"mappings":";;;AAmCM,EAAA,KAAK,EAAC,kBAAkB;AAAC,EAAA,aAAW,EAAC,kBAAkB;;;;AAC7B,EAAA,KAAK,EAAC,+BAA8B;;;;AAGhD,EAAA,KAAK,EAAC,yBAAI;;;;AAGO,EAAA,KAAK,EAAC,yBAAyB;AAAC,EAAA,aAAW,EAAC,wBAAgB;;;;AAG3F,EAAA,KAAA,EAAA,wBAAA;;;AAMG,MAAA,UAAA,GAAA,EAAA,KAAK,EAAA,wBAAA,GAAA;AACH,MAAA,UAAA,GAAA,EAAA,KAAC,EAAA,wBAAA,GAAA;;;AACS,EAAA,KAAA,EAAA,wBAAA;;;;;;;;AAjBV,IAAA,CAAA,MAAA,CAAA,OAAO,CAAC,WAAW;AAAzB,SAAAA,SAAA,EAAA,EAAAC,kBAAA,CAEC,KAFD,UAEC,EAAAC,eAAA,CADA,WAAE,CAAC,MAAA,CAAA,OAAA,CAAA,WAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;KAEE,KAAO,CAAA,OAAA;wCAEZ,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,WAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AAES,QAAA,CAAA,KAAA,CAAA,MAAM,CAAC,MAAK,KAAA,CAAA;AAAvB,WAAAF,SAAA,EAAA,EAAAC,kBAAA,CAEC,GAFD,EAAA,UAEC,EADAC,eAAA,CAAA,MAAA,CAAA,OAAO,CAAC,SAAA,GAAA,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,SAAA,IAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,2BAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AAEZ,WAAAF,SAAA,EAAA,EAAAC,kBAAA,OAAA;8BACCA,8CACW,KAAA,CAAA,MAAA,EAAA,CAAH,KAAG,KAAA;mCADXA;gBAEE,GAAG,EAAE,KAAI,CAAA,GAAA;AACV,gBAAA,KAAK,EAAC,uBAAuB;6BACpB,EAAA,CAAA,qBAAA,EAAA,KAAA,CAAA,GAAA,CAAA,CAAA;;AACT,gBAAAE,kBAAA,CAGM,OAHN,UAGM,EAAA;AAFL,kBAAAA,kBAAA,CAAO,MAAP,EAAA,UAAO,EAAAD,eAAA,CAAA,KAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;AACK,kBAAA,CAAA,KAAI,CAAA,WAAA;AAAhB,uBAAAF,SAAA,EAAA,EAAAC,kBAAA,CAAgB,MAAhB,EAAA,UAAgB,EAAAC,eAAA,CAAA,KAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;;;gBAEjBE;AACE,kBAAA,QAAQ,EAAE,KAAO,CAAA,OAAA,KAAA,EAAA;kBACjB,aAAQ,EAAA,CAAA,qBAAA,EAAA,KAAA,CAAA,GAAA,CAAA,CAAA;kBACR,OAAA,EAAA,MAAA,KAAA,QAAA,CAAA,GAAA,CAAA,KAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnMacrosWidget.vue3.js","sources":["../../../../src/components/CnMacrosWidget/CnMacrosWidget.vue?vue&type=template&id=0d95ed8d&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<!--\n CnMacrosWidget — the macros a record's schema declares, each with a button\n that runs it (`type: \"macros\"`, DqZaakAssistent \"Macro's\").\n\n A macro is a declared action (`configuration['x-openregister-action']`) with\n `macro: true` and a bound manual flow; OpenRegister runs it as the person,\n under the action's own right, through\n `POST /api/objects/{register}/{schema}/{id}/actions/{action}`. The widget\n lists what the schema declares and runs what is pressed: it adds no rights and\n no list of its own. The last run comes from the record's audit trail\n (`action.macro`), and after a run the page is asked to refresh.\n-->\n<template>\n\t<div class=\"cn-macros-widget\" data-testid=\"cn-macros-widget\">\n\t\t<p v-if=\"content.description\" class=\"cn-macros-widget__description\">\n\t\t\t{{ tr(content.description) }}\n\t\t</p>\n\t\t<p v-if=\"loading\" class=\"cn-macros-widget__muted\">\n\t\t\t{{ t('nextcloud-vue', 'Loading …') }}\n\t\t</p>\n\t\t<p v-else-if=\"macros.length === 0\" class=\"cn-macros-widget__muted\" data-testid=\"cn-macros-widget-empty\">\n\t\t\t{{ content.emptyText ? tr(content.emptyText) : t('nextcloud-vue', 'This record has no macros') }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-macros-widget__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"macro in macros\"\n\t\t\t\t:key=\"macro.key\"\n\t\t\t\tclass=\"cn-macros-widget__row\"\n\t\t\t\t:data-testid=\"`cn-macros-widget-row-${macro.key}`\">\n\t\t\t\t<div class=\"cn-macros-widget__text\">\n\t\t\t\t\t<span class=\"cn-macros-widget__name\">{{ tr(macro.name) }}</span>\n\t\t\t\t\t<span v-if=\"macro.description\" class=\"cn-macros-widget__hint\">{{ tr(macro.description) }}</span>\n\t\t\t\t</div>\n\t\t\t\t<NcButton\n\t\t\t\t\t:disabled=\"running !== ''\"\n\t\t\t\t\t:data-testid=\"`cn-macros-widget-run-${macro.key}`\"\n\t\t\t\t\t@click=\"run(macro)\">\n\t\t\t\t\t{{ running === macro.key ? t('nextcloud-vue', 'Running …') : t('nextcloud-vue', 'Execute') }}\n\t\t\t\t</NcButton>\n\t\t\t</li>\n\t\t</ul>\n\t\t<NcNoteCard\n\t\t\tv-if=\"error\"\n\t\t\ttype=\"error\"\n\t\t\tdata-testid=\"cn-macros-widget-error\">\n\t\t\t{{ error }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard\n\t\t\tv-else-if=\"lastRun\"\n\t\t\ttype=\"success\"\n\t\t\tdata-testid=\"cn-macros-widget-last-run\">\n\t\t\t{{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}<template v-if=\"lastRun.changes.length\">\n\t\t\t\t{{ n('nextcloud-vue', '{count} change: {list}', '{count} changes: {list}', lastRun.changes.length, { count: lastRun.changes.length, list: lastRun.changes.join(', ') }) }}\n\t\t\t</template>\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { emit } from '@nextcloud/event-bus'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard } from '@nextcloud/vue'\nimport { declaredMacros } from '../../utils/declaredMacros.js'\n\nexport default {\n\tname: 'CnMacrosWidget',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcNoteCard,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `description` (the line under the title),\n\t\t * `emptyText`, both translated.\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tmacros: [],\n\t\t\tloading: false,\n\t\t\trunning: '',\n\t\t\terror: '',\n\t\t\tlastRun: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {{register: string, schema: string, id: string}} Where the record lives. */\n\t\ttarget() {\n\t\t\tconst self = this.objectData?.['@self'] || {}\n\t\t\treturn {\n\t\t\t\tregister: String(self.register ?? ''),\n\t\t\t\tschema: String(self.schema ?? ''),\n\t\t\t\tid: String(this.objectId || self.id || this.objectData?.id || ''),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\t'target.schema': {\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\tt,\n\t\tn,\n\n\t\t/**\n\t\t * Translate an app-supplied text through the host app when it offers it.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Read the schema's macros and the record's last macro run.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tif (schema === '' || id === '') {\n\t\t\t\tthis.macros = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(generateUrl('/apps/openregister/api/schemas/{schema}', { schema }), register ? { params: { register } } : undefined)\n\t\t\t\tthis.macros = declaredMacros(res?.data || null)\n\t\t\t} catch {\n\t\t\t\tthis.macros = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t\tif (this.macros.length > 0) {\n\t\t\t\tawait this.loadLastRun()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The record's most recent macro run, from its audit trail. Left empty\n\t\t * when the trail cannot be read: it is a convenience, not a fact the\n\t\t * widget may invent.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLastRun() {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get(\n\t\t\t\t\tgenerateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails', { register, schema, id }),\n\t\t\t\t\t{ params: { action: 'action.macro', _limit: 1, '_sort[created]': 'DESC' } },\n\t\t\t\t)\n\t\t\t\tconst entry = (res?.data?.results || [])[0]\n\t\t\t\tconst key = entry?.changed?.declaredAction\n\t\t\t\tconst macro = this.macros.find((candidate) => candidate.key === key)\n\t\t\t\tconst changes = Array.isArray(entry?.changed?.changes)\n\t\t\t\t\t? entry.changed.changes.map((change) => (change && typeof change.step === 'string' ? change.step : '')).filter(Boolean)\n\t\t\t\t\t: []\n\t\t\t\tthis.lastRun = macro && entry.created\n\t\t\t\t\t? { name: this.tr(macro.name), date: new Date(entry.created).toLocaleString(), changes }\n\t\t\t\t\t: null\n\t\t\t} catch {\n\t\t\t\tthis.lastRun = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run one macro as the person, then ask the page to refresh.\n\t\t *\n\t\t * @param {{key: string, name: string}} macro The macro.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync run(macro) {\n\t\t\tconst { register, schema, id } = this.target\n\t\t\tthis.running = macro.key\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tawait axios.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }))\n\t\t\t\tthis.lastRun = { name: this.tr(macro.name), date: new Date().toLocaleString(), changes: [] }\n\t\t\t\temit('cn:page:refresh', { source: 'cn-macros-widget' })\n\t\t\t\t// The audit entry of this run says what it changed.\n\t\t\t\tconst local = this.lastRun\n\t\t\t\tawait this.loadLastRun()\n\t\t\t\tthis.lastRun = this.lastRun || local\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = t('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' })\n\t\t\t} finally {\n\t\t\t\tthis.running = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-macros-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-macros-widget__description,\n.cn-macros-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-macros-widget__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-macros-widget__row {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tpadding-block: 10px;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-macros-widget__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-macros-widget__name {\n\tfont-weight: 600;\n}\n\n.cn-macros-widget__hint {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createElementVNode","_createVNode","_createTextVNode"],"mappings":";;;AAmCM,EAAA,KAAK,EAAC,kBAAkB;AAAC,EAAA,aAAW,EAAC,kBAAkB;;;;AAC7B,EAAA,KAAK,EAAC,+BAA8B;;;;AAGhD,EAAA,KAAK,EAAC,yBAAI;;;;AAGO,EAAA,KAAK,EAAC,yBAAyB;AAAC,EAAA,aAAW,EAAC,wBAAgB;;;;AAG3F,EAAA,KAAA,EAAA,wBAAA;;;AAMG,MAAA,UAAA,GAAA,EAAA,KAAK,EAAA,wBAAA,GAAA;AACH,MAAA,UAAA,GAAA,EAAA,KAAC,EAAA,wBAAA,GAAA;;;AACS,EAAA,KAAA,EAAA,wBAAA;;;;;;;;AAjBV,IAAA,CAAA,MAAA,CAAA,OAAO,CAAC,WAAW;AAAzB,SAAAA,SAAA,EAAA,EAAAC,kBAAA,CAEC,KAFD,UAEC,EAAAC,eAAA,CADA,WAAE,CAAC,MAAA,CAAA,OAAA,CAAA,WAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;KAEE,KAAO,CAAA,OAAA;wCAEZ,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAC,CAAA,CAAA,CAAA,eAAA,EAAA,WAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AAES,QAAA,CAAA,KAAA,CAAA,MAAM,CAAC,MAAK,KAAA,CAAA;AAAvB,WAAAF,SAAA,EAAA,EAAAC,kBAAA,CAEC,GAFD,EAAA,UAEC,EADAC,eAAA,CAAA,MAAA,CAAA,OAAO,CAAC,SAAA,GAAA,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,SAAA,IAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,2BAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AAEZ,WAAAF,SAAA,EAAA,EAAAC,kBAAA,OAAA;8BACCA,8CACW,KAAA,CAAA,MAAA,EAAA,CAAH,KAAG,KAAA;mCADXA;gBAEE,GAAG,EAAE,KAAI,CAAA,GAAA;AACV,gBAAA,KAAK,EAAC,uBAAuB;6BACpB,EAAA,CAAA,qBAAA,EAAA,KAAA,CAAA,GAAA,CAAA,CAAA;;AACT,gBAAAE,kBAAA,CAGM,OAHN,UAGM,EAAA;AAFL,kBAAAA,kBAAA,CAAO,QAAP,UAAO,EAAAD,eAAA,CAAA,WAAA,CAAA,KAAA,CAAA,IAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACK,kBAAA,CAAA,KAAI,CAAA,WAAA;AAAhB,uBAAAF,SAAA,EAAA,EAAAC,kBAAA,CAAgB,QAAhB,UAAgB,EAAAC,eAAA,CAAA,WAAA,CAAA,KAAA,CAAA,WAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;gBAEjBE,WAKE,CAAA,mBAAA,EAAA;AAJA,kBAAA,QAAQ,EAAE,KAAO,CAAA,OAAA,KAAA,EAAA;kBAChB,aAAU,EAAA,CAAA,qBAAA,EAA0B,KAAK,CAAC,GAAG,CAAA,CAAA;kBAC9C,OAAK,EAAA,MAAA,KAAE,QAAA,CAAA,GAAA,CAAA,KAAA,CAAA,CAAA;;mCACR,MAAS;AAAN,oBAAAC,eAAA,CAAAH,eAAA,CAAA,KAAA,CAAA,OAAM,KAAA,KAAA,CAAA,GAAA,GAAA,QAAA,CAAA,CAAA,iCAAA,QAAA,CAAA,CAAA,CAAA,eAAA,EAAA,SAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnObjectListWidget.vue2.js';
2
2
  import { render } from './CnObjectListWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-78ec6d64";
5
+ script.__scopeId = "data-v-e11b98ec";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -9,6 +9,7 @@ import __sfc_main$2 from '../CnPagination/CnPagination.vue.js';
9
9
  import __sfc_main$3 from '../CnWidgetEmptyState/CnWidgetEmptyState.vue.js';
10
10
  import { useWriteFeedback } from '../../composables/useWriteFeedback.js';
11
11
  import { hasActionTargetTokens, actionLink, dispatchAction } from '../../utils/actionsDispatcher.js';
12
+ import { buildExportUrl } from '../../utils/indexExportHelpers.js';
12
13
  import { resolveHref, followLinkClick, openRowTarget } from '../../utils/linkNavigation.js';
13
14
  import { objectFieldValue, objectDisplayName } from '../../utils/objectName.js';
14
15
  import { rowIdOf, renumberRows, moveIndex } from '../../utils/reorderRows.js';
@@ -16,6 +17,7 @@ import { dropOptionalUnresolved, resolveFilterTokens, hasUnresolvedTokens } from
16
17
  import { markNewTabHandled } from '../../utils/rowAuxClick.js';
17
18
  import { changedKeys } from '../../utils/saveEdited.js';
18
19
  import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js';
20
+ import { getCurrentUserGroups } from '../../utils/widgetVisibility.js';
19
21
  import __sfc_main$4 from '../CnRowActions/CnRowActions.vue.js';
20
22
 
21
23
  /**
@@ -144,8 +146,11 @@ var script = {
144
146
  *
145
147
  * `footnote` is a line under the table, through the host translate.
146
148
  *
149
+ * `export` (`{ label?, format?, group? }`) offers the filtered list as a
150
+ * download in the card's header; `group` shows it to one group only.
151
+ *
147
152
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
148
- * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
153
+ * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}}}
149
154
  */
150
155
  content: {
151
156
  type: Object,
@@ -153,7 +158,7 @@ var script = {
153
158
  },
154
159
  },
155
160
 
156
- emits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped'],
161
+ emits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped', 'header'],
157
162
 
158
163
  data() {
159
164
  return {
@@ -962,6 +967,7 @@ var script = {
962
967
  mounted() {
963
968
  this.fetchRows();
964
969
  this.loadHeadingsIfNeeded();
970
+ this.offerExport();
965
971
  // A table wider than the widget fits its columns to the width instead of
966
972
  // scrolling sideways (no inner scrollbars).
967
973
  if (typeof ResizeObserver !== 'undefined') {
@@ -1012,6 +1018,53 @@ var script = {
1012
1018
  },
1013
1019
 
1014
1020
  methods: {
1021
+ /**
1022
+ * Offer `content.export` as a button in the card's header: the list as
1023
+ * it is filtered here, through OpenRegister's export (which applies its
1024
+ * own read rights to every row). `group` keeps the button to the members
1025
+ * of one Nextcloud group, a role such as AI oversight; it hides the
1026
+ * button, and the export endpoint still decides what anyone may read.
1027
+ *
1028
+ * @return {Promise<void>}
1029
+ * @spec openspec/changes/widget-subtitle-and-list-export/specs/object-list-widget/spec.md#requirement-a-list-can-offer-its-rows-as-an-export
1030
+ */
1031
+ /**
1032
+ * The export URL for this list as it is filtered here.
1033
+ *
1034
+ * @param {'csv'|'excel'} format The export format.
1035
+ * @return {string} The URL.
1036
+ */
1037
+ exportUrl(format) {
1038
+ return buildExportUrl(this.content.register, this.content.schema, this.resolvedFilter, format)
1039
+ },
1040
+
1041
+ async offerExport() {
1042
+ const config = this.content && this.content.export;
1043
+ if (!config || typeof config !== 'object' || !this.content.register || !this.content.schema) {
1044
+ return
1045
+ }
1046
+ if (typeof config.group === 'string' && config.group !== '') {
1047
+ const groups = await getCurrentUserGroups().catch(() => []);
1048
+ if (!groups.includes(config.group)) {
1049
+ return
1050
+ }
1051
+ }
1052
+ const format = config.format === 'excel' ? 'excel' : 'csv';
1053
+ const label = typeof config.label === 'string' && config.label !== '' ? (typeof this.cnTranslate === 'function' ? this.cnTranslate(config.label) : config.label) : translate('nextcloud-vue', 'Download');
1054
+ /**
1055
+ * @event header The card header's Download action for the list as
1056
+ * filtered here (`content.export`), handed to the host card.
1057
+ * @type {{action: {label: string, icon: string, run: Function}}}
1058
+ */
1059
+ this.$emit('header', {
1060
+ action: {
1061
+ label,
1062
+ icon: 'Download',
1063
+ run: () => window.location.assign(this.exportUrl(format)),
1064
+ },
1065
+ });
1066
+ },
1067
+
1015
1068
  /**
1016
1069
  * Move a row one place up or down (the row actions).
1017
1070
  *