@conduction/nextcloud-vue 2.143.0 → 2.144.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 (101) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  6. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  7. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
  8. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  9. package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
  10. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
  11. package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
  12. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
  13. package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
  14. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  15. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +2 -1
  16. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  17. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +3 -1
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  22. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
  23. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  24. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
  25. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  26. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  27. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
  28. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
  30. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  32. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
  34. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
  36. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
  37. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
  39. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  40. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  41. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
  42. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  43. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
  44. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  45. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  46. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
  47. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  48. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  49. package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
  50. package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
  51. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  52. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
  53. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  54. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
  55. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  56. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  57. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
  58. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  59. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  60. package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
  61. package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
  62. package/dist/esm/l10n/en.json.js +6 -0
  63. package/dist/esm/l10n/en.json.js.map +1 -1
  64. package/dist/esm/l10n/nl.json.js +6 -0
  65. package/dist/esm/l10n/nl.json.js.map +1 -1
  66. package/dist/esm/library-style.js +1 -1
  67. package/dist/esm/nextcloud-vue.css +815 -363
  68. package/dist/esm/utils/fetchFilterCounts.js +5 -2
  69. package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
  70. package/dist/esm/utils/nextcloudDirectory.js +19 -6
  71. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  72. package/dist/nextcloud-vue.cjs.js +1347 -758
  73. package/dist/nextcloud-vue.cjs.js.map +2 -2
  74. package/dist/nextcloud-vue.css +815 -363
  75. package/l10n/en.json +6 -0
  76. package/l10n/nl.json +6 -0
  77. package/package.json +1 -1
  78. package/src/components/CnActionDialog/CnActionDialog.vue +28 -3
  79. package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
  80. package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
  81. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
  82. package/src/components/CnIndexPage/CnIndexPage.vue +3 -1
  83. package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
  84. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
  85. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
  86. package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
  87. package/src/css/dialog.css +7 -1
  88. package/src/css/kpi-card.css +3 -0
  89. package/src/css/look-board-detail.css +317 -0
  90. package/src/css/look-board-index.css +14 -0
  91. package/src/css/look-board.css +25 -0
  92. package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
  93. package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
  94. package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
  95. package/src/integrations/builtin/calendar/eventStatus.js +63 -0
  96. package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
  97. package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
  98. package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
  99. package/src/integrations/builtin/deck/deckCardParts.js +56 -0
  100. package/src/utils/fetchFilterCounts.js +5 -2
  101. package/src/utils/nextcloudDirectory.js +19 -6
@@ -1 +1 @@
1
- {"version":3,"file":"CnEventsCard.vue3.js","sources":["../../../../src/components/CnEventsCard/CnEventsCard.vue?vue&type=template&id=079bed63&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<!--\n CnEventsCard — the appointments booked on a record, as a list\n (`type: \"events\"`). Each row is a calendar event as OpenRegister answers\n it: `summary`, `dtstart` / `dtend` and `location`. It reads a `source`\n (`{ kind: \"flow-task\" }` reads `/api/flow-tasks/{id}/events`) or a plain\n `endpoint`. With `content.headerActions` (`link`, `create`) it reports\n \"Bestaande koppelen\" and \"Afspraak maken\" for its card's title row and runs\n the calendar leaf's own flows against that same address.\n-->\n<template>\n\t<div class=\"cn-events-card\" data-testid=\"cn-events-card\">\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"20\" />\n\t\t<p\n\t\t\tv-else-if=\"failed\"\n\t\t\tclass=\"cn-events-card__muted\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-events-card-failed\">\n\t\t\t{{ failedText }}\n\t\t</p>\n\t\t<ul v-else-if=\"events.length > 0\" class=\"cn-events-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"(event, index) in events\"\n\t\t\t\t:key=\"keyOf(event, index)\"\n\t\t\t\tclass=\"cn-events-card__event\"\n\t\t\t\tdata-testid=\"cn-events-card-event\">\n\t\t\t\t<span class=\"cn-events-card__summary\">{{ summaryOf(event) }}</span>\n\t\t\t\t<span class=\"cn-events-card__when\">{{ whenOf(event) }}</span>\n\t\t\t\t<span v-if=\"event.location\" class=\"cn-events-card__where\">{{ event.location }}</span>\n\t\t\t</li>\n\t\t</ul>\n\t\t<p v-else class=\"cn-events-card__muted\" data-testid=\"cn-events-card-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\t\t<p\n\t\t\tv-if=\"linkError\"\n\t\t\tclass=\"cn-events-card__muted\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-events-card-link-error\">\n\t\t\t{{ linkError }}\n\t\t</p>\n\t\t<CnCalendarEventPicker\n\t\t\tv-if=\"showPicker\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@link=\"onPickerLink\"\n\t\t\t@close=\"showPicker = false\" />\n\t\t<CnCalendarEventCreate\n\t\t\tv-if=\"showCreate\"\n\t\t\t:objectId=\"String(objectId)\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t:eventsUrl=\"url\"\n\t\t\t@created=\"onEventCreated\"\n\t\t\t@close=\"showCreate = false\" />\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport CnCalendarEventCreate from '../CnCalendarEventCreate/CnCalendarEventCreate.vue'\nimport CnCalendarEventPicker from '../CnCalendarEventPicker/CnCalendarEventPicker.vue'\nimport { linkCalendarEvent } from '../../integrations/builtin/calendar/calendarEventLink.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { leafSourceUrl } from '../../utils/leafSourceUrl.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\n/**\n * CnEventsCard — the appointments on a record, read from a `source` or an\n * `endpoint`, as a list of summary, time and place.\n *\n * ```vue\n * <CnEventsCard :source=\"{ kind: 'flow-task' }\" :objectId=\"taskUuid\" />\n * ```\n *\n * As a manifest widget:\n *\n * ```json\n * { \"id\": \"task-appointments\", \"type\": \"events\", \"title\": \"Appointments\", \"content\": { \"source\": { \"kind\": \"flow-task\" } } }\n * ```\n *\n * @spec openspec/changes/leaf-cards-read-a-source/specs/leaf-cards-read-a-source/spec.md#requirement-a-leaf-card-reads-a-source-or-an-endpoint\n */\nexport default {\n\tname: 'CnEventsCard',\n\n\tcomponents: { CnCalendarEventCreate, CnCalendarEventPicker, NcLoadingIcon },\n\n\tinject: {\n\t\t/** The host app's translate function (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Where the events come from: `{ kind: 'flow-task', id? }`. `id` may be\n\t\t * left out or written `@objectId` for `objectId`. Also read from\n\t\t * `content.source`.\n\t\t *\n\t\t * @type {{kind: string, id?: (string|number)}|null}\n\t\t */\n\t\tsource: { type: Object, default: null },\n\n\t\t/** An events URL of its own; `{id}` is filled with the record id. Also read from `content.endpoint`. */\n\t\tendpoint: { type: String, default: '' },\n\n\t\t/** The page's record id. */\n\t\tobjectId: { type: [String, Number], default: '' },\n\n\t\t/** The OpenRegister API base. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\n\t\t/**\n\t\t * The widget's manifest content: `source`, `endpoint`, `emptyText`\n\t\t * (translated through the host) and `headerActions` (`link`, `create`):\n\t\t * the buttons it reports for its card's title row.\n\t\t *\n\t\t * `createVariant: \"secondary\"` and `createIcon` draw the create action\n\t\t * as the quiet header action with its own icon. `menuActions` (`link`,\n\t\t * `create`) puts those flows in the card's Actions menu instead.\n\t\t *\n\t\t * @type {{source?: object, endpoint?: string, emptyText?: string, headerActions?: Array<string>, menuActions?: Array<string>, createVariant?: string, createIcon?: string}}\n\t\t */\n\t\tcontent: { type: Object, default: () => ({}) },\n\n\t\t/** Shown when the record has no appointments. */\n\t\temptyLabel: { type: String, default: '' },\n\n\t\t/** Shown when the read failed. */\n\t\tfailedLabel: { type: String, default: '' },\n\t},\n\n\temits: ['header'],\n\n\tdata() {\n\t\treturn {\n\t\t\tevents: [],\n\t\t\tloading: false,\n\t\t\tfailed: false,\n\t\t\tshowCreate: false,\n\t\t\tshowPicker: false,\n\t\t\tlinkError: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The URL the card reads, '' while the record id is unknown.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\turl() {\n\t\t\tconst content = this.content || {}\n\t\t\tconst source = this.source || (content.source && typeof content.source === 'object' ? content.source : null)\n\t\t\tconst endpoint = this.endpoint || (typeof content.endpoint === 'string' ? content.endpoint : '')\n\t\t\treturn leafSourceUrl({ source, endpoint, apiBase: this.apiBase, objectId: this.objectId }, 'events') || ''\n\t\t},\n\n\t\t/** @return {string} The empty line. */\n\t\temptyText() {\n\t\t\tif (this.emptyLabel) {\n\t\t\t\treturn this.emptyLabel\n\t\t\t}\n\t\t\tconst override = this.content && this.content.emptyText\n\t\t\tif (typeof override === 'string' && override !== '') {\n\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(override) : override\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'No appointments yet')\n\t\t},\n\n\t\t/** @return {string} The failure line. */\n\t\tfailedText() {\n\t\t\treturn this.failedLabel || t('nextcloud-vue', 'The appointments could not be loaded')\n\t\t},\n\t},\n\n\twatch: {\n\t\turl: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t\tthis.reportHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Report the title-row buttons (`content.headerActions`) to the host\n\t\t * card: \"Bestaande koppelen\" opens the event picker, \"Afspraak maken\"\n\t\t * the meeting form. Nothing while the record is not known yet.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treportHeader() {\n\t\t\tconst asked = Array.isArray(this.content?.headerActions) ? this.content.headerActions : []\n\t\t\tconst known = {\n\t\t\t\tlink: { label: t('nextcloud-vue', 'Link an existing one'), icon: 'LinkVariant', run: () => { this.showPicker = true } },\n\t\t\t\tcreate: {\n\t\t\t\t\tlabel: t('nextcloud-vue', 'Make an appointment'),\n\t\t\t\t\ticon: typeof this.content?.createIcon === 'string' && this.content.createIcon !== '' ? this.content.createIcon : 'Plus',\n\t\t\t\t\t...(this.content?.createVariant === 'secondary' ? {} : { variant: 'primary' }),\n\t\t\t\t\trun: () => { this.showCreate = true },\n\t\t\t\t},\n\t\t\t}\n\t\t\tconst pick = (keys) => (this.url === '' ? [] : keys.filter((key, index) => known[key] && keys.indexOf(key) === index).map((key) => known[key]))\n\t\t\tconst actions = pick(asked)\n\t\t\tconst inMenu = Array.isArray(this.content?.menuActions) ? this.content.menuActions.filter((key) => !asked.includes(key)) : []\n\t\t\t/**\n\t\t\t * @event header The card's title-row actions and Actions-menu items, `{ actions, menuActions }`, for the host to draw.\n\t\t\t */\n\t\t\tthis.$emit('header', { actions, menuActions: pick(inMenu) })\n\t\t},\n\n\t\t/**\n\t\t * A meeting was made on the record: close the form, read the list again.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonEventCreated() {\n\t\t\tthis.showCreate = false\n\t\t\tthis.load()\n\t\t},\n\n\t\t/**\n\t\t * Link the picked meeting to the record, then read the list again, or say why not.\n\t\t *\n\t\t * @param {{calendarUri: string, eventUid: string}} payload The picked event.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onPickerLink(payload) {\n\t\t\tthis.showPicker = false\n\t\t\tconst result = await linkCalendarEvent(prefixUrl(this.url), payload)\n\t\t\tthis.linkError = result.ok ? '' : result.error\n\t\t\tif (result.ok) {\n\t\t\t\tthis.load()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the events. A refused or failed read says so, rather than\n\t\t * showing an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst url = this.url\n\t\t\tif (url === '') {\n\t\t\t\tthis.events = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthis.events = []\n\t\t\t\t\tthis.failed = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst data = await response.json()\n\t\t\t\tconst list = data && Array.isArray(data.results) ? data.results : data\n\t\t\t\tthis.events = Array.isArray(list) ? list : []\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure the card already shows\n\t\t\t\tconsole.error('[CnEventsCard] Loading the events failed', error)\n\t\t\t\tthis.events = []\n\t\t\t\tthis.failed = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @param {number} index Its position, for an event that carries no id.\n\t\t * @return {string} A stable key.\n\t\t */\n\t\tkeyOf(event, index) {\n\t\t\treturn String(event?.id ?? event?.uid ?? `${index}-${event?.summary ?? ''}`)\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @return {string} Its summary, or \"Appointment\" when it has none.\n\t\t */\n\t\tsummaryOf(event) {\n\t\t\tconst summary = String(event?.summary ?? '').trim()\n\t\t\treturn summary === '' ? t('nextcloud-vue', 'Appointment') : summary\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @return {string} When it is: its start and end, or whichever it has.\n\t\t */\n\t\twhenOf(event) {\n\t\t\tconst start = formatMoment(event?.dtstart)\n\t\t\tconst end = formatMoment(event?.dtend)\n\t\t\tif (start === '' || end === '') {\n\t\t\t\treturn start || end\n\t\t\t}\n\t\t\treturn `${start} - ${end}`\n\t\t},\n\t},\n}\n\n/**\n * @param {unknown} value An ISO date-time.\n * @return {string} It in the user's locale, as written when unreadable, '' when empty.\n */\nfunction formatMoment(value) {\n\tconst raw = String(value ?? '').trim()\n\tif (raw === '') {\n\t\treturn ''\n\t}\n\tconst parsed = new Date(raw)\n\treturn Number.isNaN(parsed.getTime()) ? raw : parsed.toLocaleString(userLocale())\n}\n</script>\n\n<style scoped>\n.cn-events-card__list {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-events-card__event {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tpadding-bottom: 8px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-events-card__event:last-child {\n\tpadding-bottom: 0;\n\tborder-bottom: none;\n}\n\n.cn-events-card__summary {\n\tfont-weight: 600;\n}\n\n.cn-events-card__when,\n.cn-events-card__where,\n.cn-events-card__muted {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-events-card__muted {\n\tmargin: 0;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createBlock","_toDisplayString","_Fragment","_renderList","_createElementVNode"],"mappings":";;;AA6BM,EAAA,KAAK,EAAC,gBAAgB;AAAC,EAAA,aAAS,EAAA,gBAAA;;;;AAInC,EAAA,KAAI,EAAA,uBAAA;AACJ,EAAA,IAAG,EAAA,OAAA;AACH,EAAA,aAAW,EAAC,uBAAuB;;;;AAGnC,EAAA,KAAA,EAAA,sBAAA;;AAME,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,yBAAA,GAAA;AACK,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,sBAAO,GAAA;;;AACJ,EAAA,KAAA,EAAA,uBAAA;;;;AAGP,EAAA,KAAK,EAAC,uBAAc;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AAK7B,EAAA,KAAK,EAAC,uBAAS;AACf,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAW,EAAC,2BAAiB;;;;;;;;AA3B5B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;KACmB,KAAO,CAAA,OAAA;sBAA1BC,WAAyC,CAAA,wBAAA,EAAA;;AAAZ,UAAA,IAAI,EAAE,EAAE;;SAE3B,KAAM,CAAA,MAAA;0CAKf,CAAA,GAAA,YAAA,EAAAC,eAAA,CADC,QAAU,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;AAEb,UAAA,CAAA,KAAA,CAAA,MAAA,CAAA,MAAA,GAAA,CAAA;AAAD,aAAAH,SAAA,EAAA,EAAAC,kBAAA,CAUK,MAVL,UAUK,EAAA;AATJ,eAAAD,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CAQKG,QAPqB,EAAA,IAAA,EAAAC,UAAA,CAAA,KAAA,CAAA,MAAE,EAAnB,CAAA,KAAK,EAAE,KAAK,KAAA;qCADrBJ,kBAQK,CAAA,IAAA,EAAA;AANH,kBAAA,GAAG,EAAE,QAAA,CAAA,KAAI,CAAA,KAAA,EAAA,KAAA,CAAA;AACV,kBAAA,KAAK,EAAC,uBAAuB;AAC7B,kBAAA,aAAU,EAAE,sBAAC;;AACb,kBAAAK,kBAAA,CAAC,MAAD,EAAA,UAAC,EAAAH,eAAA,CAAA,QAAA,CAAA,SAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACD,kBAAAG,kBAAA,CAAmB,MAAnB,EAAA,UAAmB,EAAAH,eAAA,CAAA,QAAA,CAAA,MAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACP,kBAAA,CAAA,KAAG,CAAA,QAAA;AAAf,uBAAAH,SAAA,EAAA,EAAAC,kBAAA,CAAe,MAAf,EAAA,UAAe,EAAAE,eAAA,CAAA,KAAA,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;;;4CAKb,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAS,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA,CAAA;KAGN,KAAS,CAAA,SAAA;0EAIZ,KAAA,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnEventsCard.vue3.js","sources":["../../../../src/components/CnEventsCard/CnEventsCard.vue?vue&type=template&id=bb3ee7b4&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<!--\n CnEventsCard — the appointments booked on a record, as a list\n (`type: \"events\"`). Each row is a calendar event as OpenRegister answers\n it: `summary`, `dtstart` / `dtend` and `location`. It reads a `source`\n (`{ kind: \"flow-task\" }` reads `/api/flow-tasks/{id}/events`) or a plain\n `endpoint`. With `content.headerActions` (`link`, `create`) it reports\n \"Bestaande koppelen\" and \"Afspraak maken\" for its card's title row and runs\n the calendar leaf's own flows against that same address.\n-->\n<template>\n\t<div class=\"cn-events-card\" data-testid=\"cn-events-card\">\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"20\" />\n\t\t<p\n\t\t\tv-else-if=\"failed\"\n\t\t\tclass=\"cn-events-card__muted\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-events-card-failed\">\n\t\t\t{{ failedText }}\n\t\t</p>\n\t\t<ul v-else-if=\"events.length > 0\" class=\"cn-events-card__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"(event, index) in events\"\n\t\t\t\t:key=\"keyOf(event, index)\"\n\t\t\t\tclass=\"cn-events-card__event\"\n\t\t\t\tdata-testid=\"cn-events-card-event\">\n\t\t\t\t<span v-if=\"dayOf(event)\" class=\"cn-events-card__day\" aria-hidden=\"true\">{{ dayOf(event) }}</span>\n\t\t\t\t<span class=\"cn-events-card__text\">\n\t\t\t\t\t<span class=\"cn-events-card__summary\">{{ summaryOf(event) }}</span>\n\t\t\t\t\t<span class=\"cn-events-card__when\">{{ whenOf(event) }}</span>\n\t\t\t\t\t<span v-if=\"event.location\" class=\"cn-events-card__where\">{{ event.location }}</span>\n\t\t\t\t</span>\n\t\t\t</li>\n\t\t</ul>\n\t\t<p v-else class=\"cn-events-card__muted\" data-testid=\"cn-events-card-empty\">\n\t\t\t{{ emptyText }}\n\t\t</p>\n\t\t<p\n\t\t\tv-if=\"linkError\"\n\t\t\tclass=\"cn-events-card__muted\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-events-card-link-error\">\n\t\t\t{{ linkError }}\n\t\t</p>\n\t\t<CnCalendarEventPicker\n\t\t\tv-if=\"showPicker\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@link=\"onPickerLink\"\n\t\t\t@close=\"showPicker = false\" />\n\t\t<CnCalendarEventCreate\n\t\t\tv-if=\"showCreate\"\n\t\t\t:objectId=\"String(objectId)\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t:eventsUrl=\"url\"\n\t\t\t@created=\"onEventCreated\"\n\t\t\t@close=\"showCreate = false\" />\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport CnCalendarEventCreate from '../CnCalendarEventCreate/CnCalendarEventCreate.vue'\nimport CnCalendarEventPicker from '../CnCalendarEventPicker/CnCalendarEventPicker.vue'\nimport { linkCalendarEvent } from '../../integrations/builtin/calendar/calendarEventLink.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { leafSourceUrl } from '../../utils/leafSourceUrl.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\n/**\n * CnEventsCard — the appointments on a record, read from a `source` or an\n * `endpoint`, as a list of summary, time and place.\n *\n * ```vue\n * <CnEventsCard :source=\"{ kind: 'flow-task' }\" :objectId=\"taskUuid\" />\n * ```\n *\n * As a manifest widget:\n *\n * ```json\n * { \"id\": \"task-appointments\", \"type\": \"events\", \"title\": \"Appointments\", \"content\": { \"source\": { \"kind\": \"flow-task\" } } }\n * ```\n *\n * @spec openspec/changes/leaf-cards-read-a-source/specs/leaf-cards-read-a-source/spec.md#requirement-a-leaf-card-reads-a-source-or-an-endpoint\n */\nexport default {\n\tname: 'CnEventsCard',\n\n\tcomponents: { CnCalendarEventCreate, CnCalendarEventPicker, NcLoadingIcon },\n\n\tinject: {\n\t\t/** The host app's translate function (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Where the events come from: `{ kind: 'flow-task', id? }`. `id` may be\n\t\t * left out or written `@objectId` for `objectId`. Also read from\n\t\t * `content.source`.\n\t\t *\n\t\t * @type {{kind: string, id?: (string|number)}|null}\n\t\t */\n\t\tsource: { type: Object, default: null },\n\n\t\t/** An events URL of its own; `{id}` is filled with the record id. Also read from `content.endpoint`. */\n\t\tendpoint: { type: String, default: '' },\n\n\t\t/** The page's record id. */\n\t\tobjectId: { type: [String, Number], default: '' },\n\n\t\t/** The OpenRegister API base. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\n\t\t/**\n\t\t * The widget's manifest content: `source`, `endpoint`, `emptyText`\n\t\t * (translated through the host) and `headerActions` (`link`, `create`):\n\t\t * the buttons it reports for its card's title row.\n\t\t *\n\t\t * `createVariant: \"secondary\"` and `createIcon` draw the create action\n\t\t * as the quiet header action with its own icon. `menuActions` (`link`,\n\t\t * `create`) puts those flows in the card's Actions menu instead.\n\t\t *\n\t\t * @type {{source?: object, endpoint?: string, emptyText?: string, headerActions?: Array<string>, menuActions?: Array<string>, createVariant?: string, createIcon?: string}}\n\t\t */\n\t\tcontent: { type: Object, default: () => ({}) },\n\n\t\t/** Shown when the record has no appointments. */\n\t\temptyLabel: { type: String, default: '' },\n\n\t\t/** Shown when the read failed. */\n\t\tfailedLabel: { type: String, default: '' },\n\t},\n\n\temits: ['header'],\n\n\tdata() {\n\t\treturn {\n\t\t\tevents: [],\n\t\t\tloading: false,\n\t\t\tfailed: false,\n\t\t\tshowCreate: false,\n\t\t\tshowPicker: false,\n\t\t\tlinkError: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The URL the card reads, '' while the record id is unknown.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\turl() {\n\t\t\tconst content = this.content || {}\n\t\t\tconst source = this.source || (content.source && typeof content.source === 'object' ? content.source : null)\n\t\t\tconst endpoint = this.endpoint || (typeof content.endpoint === 'string' ? content.endpoint : '')\n\t\t\treturn leafSourceUrl({ source, endpoint, apiBase: this.apiBase, objectId: this.objectId }, 'events') || ''\n\t\t},\n\n\t\t/** @return {string} The empty line. */\n\t\temptyText() {\n\t\t\tif (this.emptyLabel) {\n\t\t\t\treturn this.emptyLabel\n\t\t\t}\n\t\t\tconst override = this.content && this.content.emptyText\n\t\t\tif (typeof override === 'string' && override !== '') {\n\t\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(override) : override\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'No appointments yet')\n\t\t},\n\n\t\t/** @return {string} The failure line. */\n\t\tfailedText() {\n\t\t\treturn this.failedLabel || t('nextcloud-vue', 'The appointments could not be loaded')\n\t\t},\n\t},\n\n\twatch: {\n\t\turl: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t\tthis.reportHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Report the title-row buttons (`content.headerActions`) to the host\n\t\t * card: \"Bestaande koppelen\" opens the event picker, \"Afspraak maken\"\n\t\t * the meeting form. Nothing while the record is not known yet.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treportHeader() {\n\t\t\tconst asked = Array.isArray(this.content?.headerActions) ? this.content.headerActions : []\n\t\t\tconst known = {\n\t\t\t\tlink: { label: t('nextcloud-vue', 'Link an existing one'), icon: 'LinkVariant', run: () => { this.showPicker = true } },\n\t\t\t\tcreate: {\n\t\t\t\t\tlabel: t('nextcloud-vue', 'Make an appointment'),\n\t\t\t\t\ticon: typeof this.content?.createIcon === 'string' && this.content.createIcon !== '' ? this.content.createIcon : 'Plus',\n\t\t\t\t\t...(this.content?.createVariant === 'secondary' ? {} : { variant: 'primary' }),\n\t\t\t\t\trun: () => { this.showCreate = true },\n\t\t\t\t},\n\t\t\t}\n\t\t\tconst pick = (keys) => (this.url === '' ? [] : keys.filter((key, index) => known[key] && keys.indexOf(key) === index).map((key) => known[key]))\n\t\t\tconst actions = pick(asked)\n\t\t\tconst inMenu = Array.isArray(this.content?.menuActions) ? this.content.menuActions.filter((key) => !asked.includes(key)) : []\n\t\t\t/**\n\t\t\t * @event header The card's title-row actions and Actions-menu items, `{ actions, menuActions }`, for the host to draw.\n\t\t\t */\n\t\t\tthis.$emit('header', { actions, menuActions: pick(inMenu) })\n\t\t},\n\n\t\t/**\n\t\t * A meeting was made on the record: close the form, read the list again.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonEventCreated() {\n\t\t\tthis.showCreate = false\n\t\t\tthis.load()\n\t\t},\n\n\t\t/**\n\t\t * Link the picked meeting to the record, then read the list again, or say why not.\n\t\t *\n\t\t * @param {{calendarUri: string, eventUid: string}} payload The picked event.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onPickerLink(payload) {\n\t\t\tthis.showPicker = false\n\t\t\tconst result = await linkCalendarEvent(prefixUrl(this.url), payload)\n\t\t\tthis.linkError = result.ok ? '' : result.error\n\t\t\tif (result.ok) {\n\t\t\t\tthis.load()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the events. A refused or failed read says so, rather than\n\t\t * showing an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst url = this.url\n\t\t\tif (url === '') {\n\t\t\t\tthis.events = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthis.events = []\n\t\t\t\t\tthis.failed = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst data = await response.json()\n\t\t\t\tconst list = data && Array.isArray(data.results) ? data.results : data\n\t\t\t\tthis.events = Array.isArray(list) ? list : []\n\t\t\t} catch (error) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure the card already shows\n\t\t\t\tconsole.error('[CnEventsCard] Loading the events failed', error)\n\t\t\t\tthis.events = []\n\t\t\t\tthis.failed = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @param {number} index Its position, for an event that carries no id.\n\t\t * @return {string} A stable key.\n\t\t */\n\t\tkeyOf(event, index) {\n\t\t\treturn String(event?.id ?? event?.uid ?? `${index}-${event?.summary ?? ''}`)\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @return {string} Its summary, or \"Appointment\" when it has none.\n\t\t */\n\t\tsummaryOf(event) {\n\t\t\tconst summary = String(event?.summary ?? '').trim()\n\t\t\treturn summary === '' ? t('nextcloud-vue', 'Appointment') : summary\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @return {string} The day of the month it starts on, '' when it has no readable start.\n\t\t */\n\t\tdayOf(event) {\n\t\t\tconst parsed = new Date(String(event?.dtstart ?? '').trim())\n\t\t\treturn Number.isNaN(parsed.getTime()) ? '' : String(parsed.getDate())\n\t\t},\n\n\t\t/**\n\t\t * @param {object} event The event.\n\t\t * @return {string} When it is: its start and end, or whichever it has.\n\t\t */\n\t\twhenOf(event) {\n\t\t\tconst start = formatMoment(event?.dtstart)\n\t\t\tconst end = formatMoment(event?.dtend)\n\t\t\tif (start === '' || end === '') {\n\t\t\t\treturn start || end\n\t\t\t}\n\t\t\treturn `${start} - ${end}`\n\t\t},\n\t},\n}\n\n/**\n * @param {unknown} value An ISO date-time.\n * @return {string} It in the user's locale, as written when unreadable, '' when empty.\n */\nfunction formatMoment(value) {\n\tconst raw = String(value ?? '').trim()\n\tif (raw === '') {\n\t\treturn ''\n\t}\n\tconst parsed = new Date(raw)\n\treturn Number.isNaN(parsed.getTime()) ? raw : parsed.toLocaleString(userLocale())\n}\n</script>\n\n<style scoped>\n.cn-events-card__list {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-events-card__event {\n\tdisplay: flex;\n\tgap: 12px;\n\tpadding-bottom: 8px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-events-card__event:last-child {\n\tpadding-bottom: 0;\n\tborder-bottom: none;\n}\n\n.cn-events-card__day {\n\tdisplay: none;\n}\n\n.cn-events-card__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-events-card__summary {\n\tfont-weight: 600;\n}\n\n.cn-events-card__when,\n.cn-events-card__where,\n.cn-events-card__muted {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-events-card__muted {\n\tmargin: 0;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createBlock","_toDisplayString","_Fragment","_renderList","_createElementVNode"],"mappings":";;;AA6BM,EAAA,KAAK,EAAC,gBAAgB;AAAC,EAAA,aAAS,EAAA,gBAAA;;;;OAI9B,EAAC,uBAAuB;AAC3B,EAAA,IAAE,EAAC,OAAO;AACX,EAAA,aAAM,EAAA,uBAAA;;;;AAG0B,EAAA,KAAI,EAAE,sBAAsB;;;;AAMlD,EAAA,KAAA,EAAA,qBAAA;AAAA,EAAA,aAAA,EAAA,MAAA;;AACJ,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,sBAAsB,GAAA;AAC3B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAA,GAAA;AACZ,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,sBAAA,GAAA;;;AACkB,EAAA,KAAA,EAAA,uBAAA;;;;AAIX,EAAA,KAAK,EAAC,uBAAc;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AAK7B,EAAA,KAAK,EAAC,uBAAS;AACf,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAW,EAAC,2BAAiB;;;;;;;;AA9B5B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;KACmB,KAAO,CAAA,OAAA;sBAA1BC,WAAyC,CAAA,wBAAA,EAAA;;AAAZ,UAAA,IAAI,EAAE,EAAE;;SAE3B,KAAM,CAAA,MAAA;wBADhBD,kBAME,CAAA,GAAA,EANF,UAME,EAAAE,eAAA,CADA,QAAC,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;AAEU,UAAA,CAAA,KAAA,CAAA,MAAM,CAAC,MAAM,GAAA,CAAA;AAA5B,aAAAH,SAAA,EAAA,EAAAC,kBAAA,CAaK,MAbL,UAaK,EAAA;AAZJ,eAAAD,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CAWKG,QAVH,EAAA,IAAA,EAAAC,UAAA,CAAA,KAAA,CAAA,MAAA,EAAA,CAAA,KAAA,EAAA,KAAA,KAAA;qCADFJ,kBAWK,CAAA,IAAA,EAAA;AATL,kBAAA,GAAA,EAAA,QAAA,CAAA,KAAA,CAAA,KAAA,EAAA,KAAA,CAAA;AACC,kBAAA,KAAK,EAAC,uBAAQ;AACd,kBAAA,aAAW,EAAC,sBAAe;;AACjB,kBAAA,CAAA,QAAA,CAAA,KAAA,CAAA,KAAA,CAAA;AAAV,uBAAAD,SAAA,EAAA,EAAAC,kBAAA,CAAU,MAAV,EAAA,UAAU,EAAAE,eAAA,CAAA,QAAA,CAAA,KAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;AACV,kBAAAG,kBAAA,CAIO,QAJP,UAIO,EAAA;AAHP,oBAAAA,kBAAA,CAAa,MAAb,EAAA,UAAa,EAAAH,eAAA,CAAA,QAAA,CAAA,SAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACZ,oBAAAG,kBAAA,CAAA,MAAA,EAAA,UAAA,EAAAH,eAAA,CAAA,QAAA,CAAA,MAAA,CAAA,KAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AACY,oBAAA,CAAA,KAAK,CAAC,QAAA;AAAlB,yBAAAH,SAAA,EAAA,EAAAC,kBAAA,CAAkB,MAAlB,EAAA,UAAkB,EAAAE,eAAA,CAAA,KAAA,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;;;;4CAMjB,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAS,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA,CAAA;KAGN,KAAS,CAAA,SAAA;2EAIZ,KAAA,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnFilesBrowser.vue2.js';
2
2
  import { render } from './CnFilesBrowser.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-23ff0221";
5
+ script.__scopeId = "data-v-5246192c";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1418,7 +1418,8 @@ var script = {
1418
1418
  const object = {
1419
1419
  ...entry,
1420
1420
  name: node.basename,
1421
- modified: node.mtime,
1421
+ // A template drops an object value, so the Date is handed over as ISO text.
1422
+ modified: node.mtime instanceof Date ? node.mtime.toISOString() : node.mtime,
1422
1423
  size: this.formatSize(node.size),
1423
1424
  type: this.typeLabelOf(node),
1424
1425
  ...(owner ? { owner } : {}),