@conduction/nextcloud-vue 2.62.0 → 2.64.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 (209) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +425 -39
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
  10. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +186 -9
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +65 -42
  14. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  25. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  26. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  27. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +152 -135
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  35. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  36. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  37. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +253 -9
  39. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +126 -50
  41. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  47. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  48. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  50. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  52. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  54. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  57. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
  58. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
  60. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +81 -2
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +7 -3
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  65. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  66. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  67. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +41 -2
  68. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +8 -3
  70. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  72. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  73. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  74. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  76. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  78. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
  84. package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
  88. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  89. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
  90. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
  92. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  99. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  100. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  106. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
  110. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  111. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
  112. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  113. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  114. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  115. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  116. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  117. package/dist/esm/composables/useDependencyCheck.js +65 -0
  118. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  119. package/dist/esm/composables/useWalkthrough.js +218 -6
  120. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  121. package/dist/esm/index.js +2 -0
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/l10n/en.json.js +45 -1
  124. package/dist/esm/l10n/en.json.js.map +1 -1
  125. package/dist/esm/l10n/nl.json.js +45 -1
  126. package/dist/esm/l10n/nl.json.js.map +1 -1
  127. package/dist/esm/nextcloud-vue.css +780 -81
  128. package/dist/esm/store/useObjectStore.js +8 -0
  129. package/dist/esm/store/useObjectStore.js.map +1 -1
  130. package/dist/esm/utils/columnFilters.js +311 -0
  131. package/dist/esm/utils/columnFilters.js.map +1 -0
  132. package/dist/esm/utils/groupAutocomplete.js +123 -0
  133. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  134. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  135. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  136. package/dist/esm/utils/pickerOptions.js +198 -0
  137. package/dist/esm/utils/pickerOptions.js.map +1 -0
  138. package/dist/esm/utils/schema.js +83 -7
  139. package/dist/esm/utils/schema.js.map +1 -1
  140. package/dist/esm/utils/schemaRefSlug.js +19 -3
  141. package/dist/esm/utils/schemaRefSlug.js.map +1 -1
  142. package/dist/esm/utils/timelineEvents.js +174 -0
  143. package/dist/esm/utils/timelineEvents.js.map +1 -0
  144. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  145. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  146. package/dist/esm/utils/walkthroughSignals.js +88 -0
  147. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  148. package/dist/nextcloud-vue.cjs.js +13088 -7937
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +780 -81
  151. package/l10n/en.json +45 -0
  152. package/l10n/nl.json +45 -0
  153. package/package.json +1 -1
  154. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  155. package/src/components/CnAdminActionCard/index.js +4 -0
  156. package/src/components/CnAppNav/CnAppNav.vue +568 -43
  157. package/src/components/CnAppRoot/CnAppRoot.md +4 -0
  158. package/src/components/CnAppRoot/CnAppRoot.vue +223 -16
  159. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
  160. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
  161. package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
  162. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  163. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -4
  164. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  165. package/src/components/CnDataTable/CnDataTable.md +3 -0
  166. package/src/components/CnDataTable/CnDataTable.vue +305 -5
  167. package/src/components/CnDetailPage/CnDetailPage.md +4 -0
  168. package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
  169. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  170. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  171. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  172. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
  173. package/src/components/CnIndexPage/CnIndexPage.vue +87 -4
  174. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  175. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +46 -2
  176. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  177. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  178. package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
  179. package/src/components/CnStagesWidget/stagesModel.js +5 -2
  180. package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
  182. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  183. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  184. package/src/components/CnTimelineWidget/index.js +4 -0
  185. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  186. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  187. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  188. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
  189. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
  190. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  191. package/src/components/index.js +2 -0
  192. package/src/composables/useDependencyCheck.js +61 -0
  193. package/src/composables/useWalkthrough.js +217 -5
  194. package/src/css/dashboard.css +49 -0
  195. package/src/css/detail-page.css +29 -4
  196. package/src/css/kpi-card.css +24 -1
  197. package/src/css/table.css +99 -6
  198. package/src/index.js +2 -0
  199. package/src/schemas/app-manifest-v2.schema.json +135 -3
  200. package/src/store/useObjectStore.js +8 -0
  201. package/src/utils/columnFilters.js +324 -0
  202. package/src/utils/groupAutocomplete.js +119 -0
  203. package/src/utils/libraryWidgetKeys.js +2 -0
  204. package/src/utils/pickerOptions.js +194 -0
  205. package/src/utils/schema.js +83 -7
  206. package/src/utils/schemaRefSlug.js +19 -3
  207. package/src/utils/timelineEvents.js +171 -0
  208. package/src/utils/validateManifestV2.compiled.js +2 -2
  209. package/src/utils/walkthroughSignals.js +94 -0
@@ -0,0 +1,477 @@
1
+ <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
+ <!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->
3
+ <template>
4
+ <section class="cn-timeline-widget" data-testid="cn-timeline-widget" :aria-busy="loading ? 'true' : 'false'">
5
+ <h3 v-if="resolvedTitle" class="cn-timeline-widget__title">
6
+ {{ resolvedTitle }}
7
+ </h3>
8
+
9
+ <div v-if="loading && events.length === 0" class="cn-timeline-widget__loading">
10
+ <NcLoadingIcon :size="24" aria-hidden="true" />
11
+ <span>{{ loadingLabel }}</span>
12
+ </div>
13
+
14
+ <template v-else>
15
+ <!-- A failed read is not an empty history: say which part is missing. -->
16
+ <NcNoteCard v-if="failedSources.length > 0" type="warning" data-testid="cn-timeline-widget-error">
17
+ {{ failedText }}
18
+ </NcNoteCard>
19
+
20
+ <p v-if="events.length === 0" class="cn-timeline-widget__empty" data-testid="cn-timeline-widget-empty">
21
+ {{ emptyLabel }}
22
+ </p>
23
+
24
+ <ol v-else class="cn-timeline-widget__list">
25
+ <li
26
+ v-for="event in events"
27
+ :key="event.id"
28
+ class="cn-timeline-widget__event"
29
+ :class="{ 'cn-timeline-widget__event--upcoming': event.upcoming }"
30
+ :data-source="event.source"
31
+ data-testid="cn-timeline-widget-event">
32
+ <span class="cn-timeline-widget__dot" aria-hidden="true" />
33
+ <div class="cn-timeline-widget__body">
34
+ <time class="cn-timeline-widget__when" :datetime="event.at.toISOString()">
35
+ {{ formatMoment(event) }}
36
+ </time>
37
+ <span v-if="event.upcoming" class="cn-timeline-widget__badge">
38
+ {{ upcomingLabel }}
39
+ </span>
40
+ <p class="cn-timeline-widget__label">
41
+ {{ translate(event.label) }}
42
+ </p>
43
+ <p v-if="event.detail" class="cn-timeline-widget__detail">
44
+ {{ event.detail }}
45
+ </p>
46
+ </div>
47
+ </li>
48
+ </ol>
49
+ </template>
50
+ </section>
51
+ </template>
52
+
53
+ <script>
54
+ import { getLanguage, translate as t } from '@nextcloud/l10n'
55
+ import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
56
+ import { auditEvents, fieldEvents, relatedEvents, sortEvents, timelineEntryEvents } from '../../utils/timelineEvents.js'
57
+
58
+ /**
59
+ * CnTimelineWidget — an object's dated events in time order (`timeline`
60
+ * widget type for detail pages).
61
+ *
62
+ * Generalises dossiq's case timeline. It merges up to four sources, all
63
+ * optional, into one list:
64
+ *
65
+ * - `fields`: the object's own date properties, `[{ field, label }]`. A
66
+ * dotted path reaches metadata, such as `@self.created`.
67
+ * - `related`: related objects, `[{ register?, schema, field, dateField?,
68
+ * label, titleField? }]`. Rows of `schema` whose `field` holds this object's
69
+ * id become events at their `dateField` (default `@self.created`).
70
+ * - `auditTrail: true`: the object's audit trail, one event per change.
71
+ * - `timeline: true`: OpenRegister's timeline of notes, calls and messages.
72
+ *
73
+ * Oldest first by default (`order: 'desc'` flips it). A moment after now is
74
+ * marked upcoming, in words as well as style.
75
+ *
76
+ * ```json
77
+ * { "type": "timeline", "content": {
78
+ * "title": "Timeline",
79
+ * "fields": [
80
+ * { "field": "@self.created", "label": "Booking created" },
81
+ * { "field": "depositClearedAt", "label": "Deposit cleared" },
82
+ * { "field": "confirmationSentAt", "label": "Confirmation mail sent" },
83
+ * { "field": "startsAt", "label": "Starts" },
84
+ * { "field": "endsAt", "label": "Ends" }
85
+ * ] } }
86
+ * ```
87
+ *
88
+ * The object comes from the detail page's context, like `audit-trail`:
89
+ * explicit props win, then the injects, then `content`.
90
+ *
91
+ * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order
92
+ */
93
+ export default {
94
+ name: 'CnTimelineWidget',
95
+
96
+ components: {
97
+ NcLoadingIcon,
98
+ NcNoteCard,
99
+ },
100
+
101
+ inject: {
102
+ /** Detail-page object context from CnDetailPage (a ref or a raw bag). */
103
+ cnObjectContext: { default: null },
104
+ /** Detail object context holder from CnPageRenderer (`{ value: { objectData, ... } }`). */
105
+ cnDetailObjectContext: { default: null },
106
+ /** Consumer translation function; labels are authored in English. */
107
+ cnTranslate: { default: () => (key) => key },
108
+ },
109
+
110
+ props: {
111
+ /** Register slug; object context spread as props by CnWidgetGrid. */
112
+ register: { type: String, default: '' },
113
+ /**
114
+ * Schema slug, or the schema object the detail context supplies.
115
+ *
116
+ * @type {string|object}
117
+ */
118
+ schema: { type: [String, Object], default: '' },
119
+ /** The object's id. */
120
+ objectId: { type: String, default: '' },
121
+ /** The object itself, when the host already has it. */
122
+ object: { type: Object, default: null },
123
+ /** Card title override. */
124
+ title: { type: String, default: '' },
125
+ /** Stored widget content: `{ title, fields, related, auditTrail, timeline, order }`. */
126
+ content: { type: Object, default: () => ({}) },
127
+ /** OpenRegister API base. */
128
+ apiBase: { type: String, default: '/apps/openregister/api' },
129
+ },
130
+
131
+ data() {
132
+ return {
133
+ loading: false,
134
+ relatedRows: {},
135
+ auditEntries: [],
136
+ timelineEntries: [],
137
+ failedSources: [],
138
+ fetchedObject: null,
139
+ loadSeq: 0,
140
+ }
141
+ },
142
+
143
+ computed: {
144
+ ctx() {
145
+ const raw = this.cnObjectContext
146
+ const inj = raw && (raw.value !== undefined ? raw.value : raw)
147
+ const holder = this.cnDetailObjectContext && this.cnDetailObjectContext.value
148
+ return inj || holder || {}
149
+ },
150
+
151
+ resolvedObjectId() {
152
+ return this.objectId || this.ctx.objectId || this.content.objectId || ''
153
+ },
154
+
155
+ resolvedRegister() {
156
+ return this.register || this.ctx.register || this.content.register || ''
157
+ },
158
+
159
+ resolvedSchema() {
160
+ const s = this.schema || this.ctx.schema || this.content.schema || ''
161
+ return typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''
162
+ },
163
+
164
+ /**
165
+ * The object: explicit prop, then the page context, then a fetch of
166
+ * our own when only the id is known.
167
+ *
168
+ * @return {object|null}
169
+ */
170
+ resolvedObject() {
171
+ return this.object || this.ctx.object || this.ctx.objectData || this.fetchedObject || null
172
+ },
173
+
174
+ resolvedTitle() {
175
+ const raw = this.title || this.content.title || ''
176
+ return raw ? this.translate(raw) : ''
177
+ },
178
+
179
+ fieldsConfig() {
180
+ return Array.isArray(this.content.fields) ? this.content.fields : []
181
+ },
182
+
183
+ relatedConfig() {
184
+ return Array.isArray(this.content.related) ? this.content.related.filter((r) => r && r.schema && r.field) : []
185
+ },
186
+
187
+ /**
188
+ * Every event from every source, in time order.
189
+ *
190
+ * @return {Array<object>}
191
+ */
192
+ events() {
193
+ const now = new Date()
194
+ const all = [
195
+ ...fieldEvents(this.resolvedObject || {}, this.fieldsConfig, now),
196
+ ...this.relatedConfig.flatMap((cfg, i) => relatedEvents(this.relatedRows[i] || [], cfg, now)),
197
+ ...auditEvents(this.auditEntries, this.describeAudit, now),
198
+ ...timelineEntryEvents(this.timelineEntries, t('nextcloud-vue', 'Note'), now),
199
+ ]
200
+ return sortEvents(all, this.content.order === 'desc' ? 'desc' : 'asc')
201
+ },
202
+
203
+ /**
204
+ * A signature of what to fetch, so a change refetches once.
205
+ *
206
+ * @return {string}
207
+ */
208
+ loadKey() {
209
+ return JSON.stringify({
210
+ id: this.resolvedObjectId,
211
+ register: this.resolvedRegister,
212
+ schema: this.resolvedSchema,
213
+ related: this.relatedConfig,
214
+ audit: this.content.auditTrail === true,
215
+ timeline: this.content.timeline === true,
216
+ needObject: this.fieldsConfig.length > 0 && !(this.object || this.ctx.object || this.ctx.objectData),
217
+ })
218
+ },
219
+
220
+ failedText() {
221
+ return t('nextcloud-vue', 'Part of the timeline could not load: {parts}.', {
222
+ parts: this.failedSources.map((s) => this.sourceName(s)).join(', '),
223
+ })
224
+ },
225
+
226
+ loadingLabel() {
227
+ return t('nextcloud-vue', 'Loading…')
228
+ },
229
+
230
+ emptyLabel() {
231
+ return t('nextcloud-vue', 'Nothing has happened yet.')
232
+ },
233
+
234
+ upcomingLabel() {
235
+ return t('nextcloud-vue', 'Upcoming')
236
+ },
237
+ },
238
+
239
+ watch: {
240
+ loadKey: {
241
+ immediate: true,
242
+ handler() {
243
+ this.load()
244
+ },
245
+ },
246
+ },
247
+
248
+ methods: {
249
+ translate(text) {
250
+ return text ? this.cnTranslate(text) : ''
251
+ },
252
+
253
+ sourceName(source) {
254
+ if (source === 'audit') {
255
+ return t('nextcloud-vue', 'changes')
256
+ }
257
+ if (source === 'timeline') {
258
+ return t('nextcloud-vue', 'notes and messages')
259
+ }
260
+ if (source === 'object') {
261
+ return t('nextcloud-vue', 'dates')
262
+ }
263
+ return t('nextcloud-vue', 'related items')
264
+ },
265
+
266
+ /**
267
+ * The label of an audit event, from its action and who did it.
268
+ *
269
+ * @param {string} action The audit action (create, update, delete).
270
+ * @param {string} actor Who did it.
271
+ * @return {string}
272
+ */
273
+ describeAudit(action, actor) {
274
+ const known = {
275
+ create: t('nextcloud-vue', 'Created'),
276
+ update: t('nextcloud-vue', 'Updated'),
277
+ delete: t('nextcloud-vue', 'Deleted'),
278
+ }
279
+ const what = known[action] || action || t('nextcloud-vue', 'Changed')
280
+ return actor ? t('nextcloud-vue', '{action} by {actor}', { action: what, actor }) : what
281
+ },
282
+
283
+ /**
284
+ * Show a moment in the reader's language: a date for a calendar day,
285
+ * date and time otherwise.
286
+ *
287
+ * @param {object} event The event.
288
+ * @return {string}
289
+ */
290
+ formatMoment(event) {
291
+ const locale = (typeof getLanguage === 'function' && getLanguage()) || undefined
292
+ const options = event.dateOnly
293
+ ? { dateStyle: 'medium' }
294
+ : { dateStyle: 'medium', timeStyle: 'short' }
295
+ try {
296
+ return new Intl.DateTimeFormat(locale, options).format(event.at)
297
+ } catch {
298
+ return event.at.toLocaleString()
299
+ }
300
+ },
301
+
302
+ /**
303
+ * Fetch every configured source. Each one fails on its own, so one
304
+ * unreachable source never hides the others.
305
+ *
306
+ * @return {Promise<void>}
307
+ * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order
308
+ */
309
+ async load() {
310
+ const id = this.resolvedObjectId
311
+ const register = this.resolvedRegister
312
+ const schema = this.resolvedSchema
313
+ const seq = ++this.loadSeq
314
+ if (!id || !register || !schema) {
315
+ return
316
+ }
317
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
318
+ import('@nextcloud/axios'),
319
+ import('@nextcloud/router'),
320
+ ])
321
+ const base = `${this.apiBase}/objects/${register}/${schema}/${id}`
322
+ const failed = []
323
+ const jobs = []
324
+
325
+ if (this.fieldsConfig.length > 0 && !(this.object || this.ctx.object || this.ctx.objectData)) {
326
+ jobs.push(axios.get(generateUrl(base))
327
+ .then((r) => {
328
+ this.fetchedObject = (r && r.data) || null
329
+ })
330
+ .catch(() => failed.push('object')))
331
+ }
332
+
333
+ const rows = {}
334
+ this.relatedConfig.forEach((cfg, i) => {
335
+ const url = generateUrl(`${this.apiBase}/objects/${cfg.register || register}/${cfg.schema}`)
336
+ jobs.push(axios.get(url, { params: { [cfg.field]: id, _limit: cfg.limit || 50 } })
337
+ .then((r) => {
338
+ const data = r && r.data
339
+ rows[i] = (data && (data.results || data)) || []
340
+ })
341
+ .catch(() => {
342
+ rows[i] = []
343
+ if (!failed.includes('related')) {
344
+ failed.push('related')
345
+ }
346
+ }))
347
+ })
348
+
349
+ if (this.content.auditTrail === true) {
350
+ jobs.push(axios.get(generateUrl(`${base}/audit-trail`), { params: { limit: this.content.auditLimit || 50 } })
351
+ .then((r) => {
352
+ const data = r && r.data
353
+ this.auditEntries = (data && (data.results || data)) || []
354
+ })
355
+ .catch(() => {
356
+ this.auditEntries = []
357
+ failed.push('audit')
358
+ }))
359
+ }
360
+
361
+ if (this.content.timeline === true) {
362
+ jobs.push(axios.get(generateUrl(`${base}/timeline`))
363
+ .then((r) => {
364
+ const data = r && r.data
365
+ this.timelineEntries = (data && (data.results || data)) || []
366
+ })
367
+ .catch(() => {
368
+ this.timelineEntries = []
369
+ failed.push('timeline')
370
+ }))
371
+ }
372
+
373
+ if (jobs.length === 0) {
374
+ this.failedSources = []
375
+ return
376
+ }
377
+ this.loading = true
378
+ await Promise.all(jobs)
379
+ if (seq !== this.loadSeq) {
380
+ return
381
+ }
382
+ this.relatedRows = rows
383
+ this.failedSources = failed
384
+ this.loading = false
385
+ },
386
+ },
387
+ }
388
+ </script>
389
+
390
+ <style scoped>
391
+ .cn-timeline-widget {
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 8px;
395
+ }
396
+
397
+ .cn-timeline-widget__title {
398
+ margin: 0;
399
+ font-size: var(--font-size-large, 1.1em);
400
+ }
401
+
402
+ .cn-timeline-widget__loading {
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 8px;
406
+ color: var(--color-text-maxcontrast);
407
+ }
408
+
409
+ .cn-timeline-widget__empty {
410
+ margin: 0;
411
+ color: var(--color-text-maxcontrast);
412
+ }
413
+
414
+ .cn-timeline-widget__list {
415
+ margin: 0;
416
+ padding: 0;
417
+ list-style: none;
418
+ }
419
+
420
+ .cn-timeline-widget__event {
421
+ position: relative;
422
+ display: flex;
423
+ gap: 12px;
424
+ padding-block-end: 12px;
425
+ }
426
+
427
+ /* The line between dots: drawn from each dot down to the next event. */
428
+ .cn-timeline-widget__event:not(:last-child)::before {
429
+ content: '';
430
+ position: absolute;
431
+ inset-block: 14px 0;
432
+ inset-inline-start: 5px;
433
+ border-inline-start: 2px solid var(--color-border-dark);
434
+ }
435
+
436
+ .cn-timeline-widget__dot {
437
+ flex: 0 0 auto;
438
+ width: 12px;
439
+ height: 12px;
440
+ margin-block-start: 4px;
441
+ border-radius: 50%;
442
+ background-color: var(--color-primary-element);
443
+ }
444
+
445
+ /* Upcoming: a hollow dot, plus the word "Upcoming" beside the date. */
446
+ .cn-timeline-widget__event--upcoming .cn-timeline-widget__dot {
447
+ box-sizing: border-box;
448
+ border: 2px solid var(--color-primary-element);
449
+ background-color: var(--color-main-background);
450
+ }
451
+
452
+ .cn-timeline-widget__body {
453
+ min-width: 0;
454
+ }
455
+
456
+ .cn-timeline-widget__when {
457
+ font-size: var(--font-size-small, 13px);
458
+ color: var(--color-text-maxcontrast);
459
+ }
460
+
461
+ .cn-timeline-widget__badge {
462
+ margin-inline-start: 8px;
463
+ font-size: var(--font-size-small, 13px);
464
+ font-weight: bold;
465
+ color: var(--color-main-text);
466
+ }
467
+
468
+ .cn-timeline-widget__label {
469
+ margin: 0;
470
+ font-weight: bold;
471
+ }
472
+
473
+ .cn-timeline-widget__detail {
474
+ margin: 0;
475
+ overflow-wrap: anywhere;
476
+ }
477
+ </style>
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Registers the `timeline` widget type (CnTimelineWidget) in the shared
6
+ * dashboardWidgetRegistry. Like `audit-trail` it is a DETAIL-PAGE surface:
7
+ * the object comes from the page's object context. It has no config form
8
+ * yet, so it is placed from the manifest and never offered in the
9
+ * Add-widget picker.
10
+ *
11
+ * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md#requirement-a-timeline-widget-shows-an-objects-dated-events-in-order
12
+ */
13
+
14
+ import CnTimelineWidget from './CnTimelineWidget.vue'
15
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
16
+
17
+ registerDashboardWidget('timeline', {
18
+ renderer: CnTimelineWidget,
19
+ form: null,
20
+ defaultContent: {
21
+ title: '',
22
+ fields: [],
23
+ related: [],
24
+ auditTrail: false,
25
+ timeline: false,
26
+ order: 'asc',
27
+ },
28
+ displayName: 'Timeline',
29
+ icon: 'TimelineClockOutline',
30
+ surfaces: ['detail-page'],
31
+ ownsTitle: true,
32
+ })
@@ -0,0 +1,4 @@
1
+ import CnTimelineWidget from './CnTimelineWidget.vue'
2
+
3
+ export default CnTimelineWidget
4
+ export { CnTimelineWidget }