@conduction/nextcloud-vue 2.119.0 → 2.120.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 (126) hide show
  1. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
  2. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
  3. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
  4. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
  6. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
  8. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
  9. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
  10. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
  11. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
  12. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
  13. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  14. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  15. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
  16. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
  17. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
  18. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
  19. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
  20. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
  21. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
  22. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
  23. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
  24. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
  25. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
  26. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
  28. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
  30. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
  32. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
  33. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
  34. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
  35. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
  36. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
  37. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
  38. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
  39. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
  40. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
  41. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
  42. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
  43. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
  44. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
  45. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
  46. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
  47. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
  48. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
  49. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
  50. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
  51. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
  52. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
  53. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
  54. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
  55. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
  56. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
  57. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
  58. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
  60. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
  65. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
  67. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
  69. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  70. package/dist/esm/index.js +5 -0
  71. package/dist/esm/index.js.map +1 -1
  72. package/dist/esm/l10n/en.json.js +166 -1
  73. package/dist/esm/l10n/en.json.js.map +1 -1
  74. package/dist/esm/l10n/nl.json.js +166 -1
  75. package/dist/esm/l10n/nl.json.js.map +1 -1
  76. package/dist/esm/library-style.js +1 -1
  77. package/dist/esm/nextcloud-vue.css +889 -101
  78. package/dist/esm/utils/aiFeatures.js +245 -0
  79. package/dist/esm/utils/aiFeatures.js.map +1 -0
  80. package/dist/esm/utils/aiSuggestionTracker.js +182 -0
  81. package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
  82. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  83. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  84. package/dist/esm/utils/objectPermissions.js +552 -0
  85. package/dist/esm/utils/objectPermissions.js.map +1 -0
  86. package/dist/esm/utils/objectTranslations.js +180 -0
  87. package/dist/esm/utils/objectTranslations.js.map +1 -0
  88. package/dist/esm/utils/stateMachineValidation.js +254 -0
  89. package/dist/esm/utils/stateMachineValidation.js.map +1 -0
  90. package/dist/esm/utils/widgetDispatch.js +3 -1
  91. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  92. package/dist/nextcloud-vue.cjs.js +20052 -13138
  93. package/dist/nextcloud-vue.cjs.js.map +2 -2
  94. package/dist/nextcloud-vue.css +889 -101
  95. package/l10n/en.json +166 -1
  96. package/l10n/nl.json +166 -1
  97. package/package.json +1 -1
  98. package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
  99. package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
  100. package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
  101. package/src/components/CnAiFeaturesWidget/index.js +14 -0
  102. package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
  103. package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
  104. package/src/components/CnObjectPermissionsTab/index.js +13 -0
  105. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
  106. package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
  107. package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
  108. package/src/components/CnObjectTranslationsWidget/index.js +13 -0
  109. package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
  110. package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
  111. package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
  112. package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
  113. package/src/components/CnStateMachineEditor/index.js +8 -0
  114. package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
  115. package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
  116. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
  117. package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
  118. package/src/components/index.js +4 -0
  119. package/src/index.js +5 -0
  120. package/src/utils/aiFeatures.js +241 -0
  121. package/src/utils/aiSuggestionTracker.js +178 -0
  122. package/src/utils/libraryWidgetKeys.js +1 -0
  123. package/src/utils/objectPermissions.js +548 -0
  124. package/src/utils/objectTranslations.js +176 -0
  125. package/src/utils/stateMachineValidation.js +250 -0
  126. package/src/utils/widgetDispatch.js +3 -1
@@ -1,7 +1,16 @@
1
1
  <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
2
  <!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->
3
3
  <template>
4
- <section class="cn-timeline-widget" data-testid="cn-timeline-widget" :aria-busy="loading ? 'true' : 'false'">
4
+ <CnTimelineManage v-if="manage"
5
+ :register="resolvedRegister"
6
+ :schema="resolvedSchema"
7
+ :objectId="resolvedObjectId"
8
+ :apiBase="apiBase"
9
+ :content="content" />
10
+ <section v-else
11
+ class="cn-timeline-widget"
12
+ data-testid="cn-timeline-widget"
13
+ :aria-busy="loading ? 'true' : 'false'">
5
14
  <h3 v-if="resolvedTitle" class="cn-timeline-widget__title">
6
15
  {{ resolvedTitle }}
7
16
  </h3>
@@ -53,6 +62,7 @@
53
62
  <script>
54
63
  import { getLanguage, translate as t } from '@nextcloud/l10n'
55
64
  import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
65
+ import CnTimelineManage from './CnTimelineManage.vue'
56
66
  import { auditEvents, fieldEvents, listEvents, relatedEvents, sortEvents, timelineEntryEvents } from '../../utils/timelineEvents.js'
57
67
  import { userLocale } from '../../utils/userLocale.js'
58
68
 
@@ -74,6 +84,12 @@ import { userLocale } from '../../utils/userLocale.js'
74
84
  * Oldest first by default (`order: 'desc'` flips it). A moment after now is
75
85
  * marked upcoming, in words as well as style.
76
86
  *
87
+ * `manage: true` swaps the read view for the manage view (CnTimelineManage)
88
+ * over OpenRegister's timeline alone: kind chips, a visibility filter, pin,
89
+ * follow-up, a note composer with standard text blocks, CSV export, and
90
+ * `entryActions` that open a registry modal on an entry. The manage controls
91
+ * are drawn only when the timeline read answers `canManage: true`.
92
+ *
77
93
  * ```json
78
94
  * { "type": "timeline", "content": {
79
95
  * "title": "Timeline",
@@ -95,6 +111,7 @@ export default {
95
111
  name: 'CnTimelineWidget',
96
112
 
97
113
  components: {
114
+ CnTimelineManage,
98
115
  NcLoadingIcon,
99
116
  NcNoteCard,
100
117
  },
@@ -123,7 +140,7 @@ export default {
123
140
  object: { type: Object, default: null },
124
141
  /** Card title override. */
125
142
  title: { type: String, default: '' },
126
- /** Stored widget content: `{ title, fields, related, auditTrail, timeline, order }`. */
143
+ /** Stored widget content: `{ title, fields, related, auditTrail, timeline, order, manage, entryActions, ... }`. */
127
144
  content: { type: Object, default: () => ({}) },
128
145
  /** OpenRegister API base. */
129
146
  apiBase: { type: String, default: '/apps/openregister/api' },
@@ -172,6 +189,15 @@ export default {
172
189
  return this.object || this.ctx.object || this.ctx.objectData || this.fetchedObject || null
173
190
  },
174
191
 
192
+ /**
193
+ * Whether the manage view renders instead of the read view.
194
+ *
195
+ * @return {boolean}
196
+ */
197
+ manage() {
198
+ return this.content.manage === true
199
+ },
200
+
175
201
  resolvedTitle() {
176
202
  const raw = this.title || this.content.title || ''
177
203
  return raw ? this.translate(raw) : ''
@@ -313,7 +339,7 @@ export default {
313
339
  const register = this.resolvedRegister
314
340
  const schema = this.resolvedSchema
315
341
  const seq = ++this.loadSeq
316
- if (!id || !register || !schema) {
342
+ if (!id || !register || !schema || this.manage) {
317
343
  return
318
344
  }
319
345
  const [{ default: axios }, { generateUrl }] = await Promise.all([
@@ -26,6 +26,7 @@ import CnNoticeWidget from '../CnNoticeWidget/CnNoticeWidget.vue'
26
26
  import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
27
27
  import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
28
28
  import CnObjectMetadataWidget from '../CnObjectMetadataWidget/CnObjectMetadataWidget.vue'
29
+ import CnObjectTranslationsWidget from '../CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue'
29
30
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
30
31
  import CnSavedViewAlertWidget from '../CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue'
31
32
  import CnTextWidget from '../CnTextWidget/CnTextWidget.vue'
@@ -74,6 +75,10 @@ import CnWidgetObjectTable from '../CnWidgetObjectTable/CnWidgetObjectTable.vue'
74
75
  * data — the intended remedy for a deep menu group collapsed into one
75
76
  * top-level entry via `menu-layout.json` relocations.
76
77
  *
78
+ * The `object-translations` key places CnObjectTranslationsWidget: the
79
+ * object's translatable properties per register language, with completeness
80
+ * chips and one save (register / schema / objectId via the detail-context merge).
81
+ *
77
82
  * @type {Record<string, import('vue').Component>}
78
83
  */
79
84
  export const BUILT_IN_WIDGETS = {
@@ -97,4 +102,5 @@ export const BUILT_IN_WIDGETS = {
97
102
  divider: CnDividerWidget,
98
103
  'saved-view-alert': CnSavedViewAlertWidget,
99
104
  exports: CnExportsWidget,
105
+ 'object-translations': CnObjectTranslationsWidget,
100
106
  }
@@ -77,6 +77,7 @@ export { CnRegisterSchemaSelect } from './CnRegisterSchemaSelect/index.js'
77
77
  export { CnThemePreview } from './CnThemePreview/index.js'
78
78
  export { CnRelationshipGraph } from './CnRelationshipGraph/index.js'
79
79
  export { CnGraphCanvas } from './CnGraphCanvas/index.js'
80
+ export { CnStateMachineEditor } from './CnStateMachineEditor/index.js'
80
81
  export { CnFlowDetail, CnFlowIndexPage, CnFlowSidebar } from './CnFlowDetail/index.js'
81
82
  export { CnIcon, ICON_MAP, registerIcons } from './CnIcon/index.js'
82
83
  export {
@@ -167,7 +168,10 @@ export { CnJourneyDialog } from './CnJourneyDialog/index.js'
167
168
  export { CnProcessSteps } from './CnProcessSteps/index.js'
168
169
  export { CnSubObjectsField } from './CnSubObjectsField/index.js'
169
170
  export { CnIntegrationTab } from './CnIntegrationTab/index.js'
171
+ export { CnAiFeaturesWidget } from './CnAiFeaturesWidget/index.js'
170
172
  export { CnObjectAccessTab } from './CnObjectAccessTab/index.js'
173
+ export { CnObjectPermissionsTab } from './CnObjectPermissionsTab/index.js'
174
+ export { CnObjectTranslationsWidget } from './CnObjectTranslationsWidget/index.js'
171
175
  export { CnIntegrationCard } from './CnIntegrationCard/index.js'
172
176
  export { CnIntegrationWidgetGrid } from './CnIntegrationWidgetGrid/index.js'
173
177
  export { CnIntegrationWidget } from './CnIntegrationWidget/index.js'
package/src/index.js CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  CnAdminActionCard,
41
41
  CnAdminSettingsShell,
42
42
  CnAdvancedFormDialog,
43
+ CnAiFeaturesWidget,
43
44
  CnApiReference,
44
45
  CnAppLoading,
45
46
  CnAppNav,
@@ -249,9 +250,11 @@ export {
249
250
  CnObjectMetadataModal,
250
251
  CnObjectMetadataWidget,
251
252
  CnObjectPartiesWidget,
253
+ CnObjectPermissionsTab,
252
254
  CnObjectPresenceWidget,
253
255
  CnObjectRow,
254
256
  CnObjectSidebar,
257
+ CnObjectTranslationsWidget,
255
258
  CnOfflineQueue,
256
259
  CnOrderedListField,
257
260
  CnPageHeader,
@@ -317,6 +320,7 @@ export {
317
320
  CnSpendAnalyticsWidgetForm,
318
321
  CnStagesWidget,
319
322
  CnStagesWidgetForm,
323
+ CnStateMachineEditor,
320
324
  CnStatsBlock,
321
325
  CnStatsBlockWidget,
322
326
  CnStatsBlockWidgetForm,
@@ -618,6 +622,7 @@ export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData
618
622
  // drift on what a 409 means (breaking change / schema still has objects).
619
623
  export { deleteSchema, describeSchemaChange, saveSchema, SchemaBreakingChangeError, SchemaHasObjectsError } from './utils/index.js'
620
624
  export { validateManifest, validateManifestV2 } from './utils/validateManifest.js'
625
+ export { validateStateMachine } from './utils/stateMachineValidation.js'
621
626
  export { clearResolveCache, resolveManifestSentinels } from './utils/resolveManifestSentinels.js'
622
627
  export { clearRouteSentinelWarnings, resolveRouteSentinels } from './utils/resolveRouteSentinels.js'
623
628
  export {
@@ -0,0 +1,241 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * The reads and the run behind CnAiFeaturesWidget: the AI features a record's
6
+ * TYPE object places on a surface, where hermiq runs each one, a run on one of
7
+ * the record's documents, and the sentence a refusal is shown as.
8
+ *
9
+ * An undeclared type asks nobody: the declaration is read first and the
10
+ * features endpoint is called only when something is placed on the surface.
11
+ * Every read answers null when it failed, so the widget can say it is
12
+ * incomplete instead of drawing an empty list.
13
+ *
14
+ * The run goes to hermiq in the reader's own session, so their own access
15
+ * decides which document can be read. A request without a usable document
16
+ * reference is refused before it is sent: hermiq's redaction gate sits on the
17
+ * document, and a request without one would run unchecked.
18
+ */
19
+ import axios from '@nextcloud/axios'
20
+ import { translate as t } from '@nextcloud/l10n'
21
+ import { prefixUrl } from './headers.js'
22
+
23
+ /** The gate name for a run refused before it was sent. */
24
+ export const MISSING_REFERENCE = 'missing-reference'
25
+
26
+ /** The hermiq endpoint a feature runs on a document through; `{slug}` is filled in. */
27
+ export const DEFAULT_RUN_ENDPOINT = '/apps/hermiq/api/ai-features/{slug}/run-on-document'
28
+
29
+ /**
30
+ * A run that was refused, before sending or by one of hermiq's gates.
31
+ */
32
+ export class AiFeatureRefusal extends Error {
33
+ /**
34
+ * @param {string} feature The feature slug.
35
+ * @param {string} reason The refusing side's own sentence.
36
+ * @param {string|null} gate The gate that refused, when named.
37
+ */
38
+ constructor(feature, reason, gate) {
39
+ super(reason)
40
+ this.name = 'AiFeatureRefusal'
41
+ this.feature = feature
42
+ this.reason = reason
43
+ this.gate = gate
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The id inside a reference that may be a plain id or an object carrying one.
49
+ *
50
+ * @param {string|object|null|undefined} value The reference.
51
+ * @return {string} The id, or ''.
52
+ */
53
+ export function referenceId(value) {
54
+ if (value !== null && typeof value === 'object') {
55
+ return String(value.id ?? value.uuid ?? '').trim()
56
+ }
57
+ return String(value ?? '').trim()
58
+ }
59
+
60
+ /**
61
+ * The slugs a type object places on one surface. The declaration is a map of
62
+ * feature slug to surface name.
63
+ *
64
+ * @param {object} typeObject The type object.
65
+ * @param {string} field The declaration field.
66
+ * @param {string} surface The surface.
67
+ * @return {Array<string>} The slugs, in declaration order.
68
+ */
69
+ export function surfaceSlugs(typeObject, field, surface) {
70
+ const declared = typeObject?.[field]
71
+ if (declared === null || typeof declared !== 'object' || Array.isArray(declared)) {
72
+ return []
73
+ }
74
+ return Object.keys(declared).filter((slug) => declared[slug] === surface)
75
+ }
76
+
77
+ /**
78
+ * Read one OpenRegister object.
79
+ *
80
+ * @param {string} apiBase API root.
81
+ * @param {string} register Register.
82
+ * @param {string} schema Schema.
83
+ * @param {string} id Object id.
84
+ * @return {Promise<object>} The object.
85
+ */
86
+ async function readObject(apiBase, register, schema, id) {
87
+ const { data } = await axios.get(prefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`))
88
+ return data?.object ?? data ?? {}
89
+ }
90
+
91
+ /**
92
+ * The AI features the record's type places on the surface, each with where it
93
+ * runs as the features endpoint answers it.
94
+ *
95
+ * @param {object} options The options.
96
+ * @param {string} options.apiBase OpenRegister API root.
97
+ * @param {string} options.register Register.
98
+ * @param {string} options.schema Schema of the record.
99
+ * @param {string} options.objectId The record id.
100
+ * @param {object} options.declaration `{typeField, typeSchema, field, surface}`.
101
+ * @param {string} options.featuresEndpoint The endpoint that asks hermiq.
102
+ * @param {string} options.featuresQueryKey Query key the slugs are sent under.
103
+ * @return {Promise<Array<object>|null>} The features, or null when they could not be read.
104
+ */
105
+ export async function fetchFeatures({ apiBase, register, schema, objectId, declaration, featuresEndpoint, featuresQueryKey }) {
106
+ try {
107
+ const record = await readObject(apiBase, register, schema, objectId)
108
+ const typeId = referenceId(record[declaration.typeField])
109
+ if (typeId === '') {
110
+ return []
111
+ }
112
+ const typeObject = await readObject(apiBase, register, declaration.typeSchema, typeId)
113
+ const slugs = surfaceSlugs(typeObject, declaration.field || 'aiFeatures', declaration.surface)
114
+ if (slugs.length === 0) {
115
+ return []
116
+ }
117
+
118
+ const query = new URLSearchParams({ surface: declaration.surface })
119
+ for (const slug of slugs) {
120
+ query.append(`${featuresQueryKey}[${slug}]`, declaration.surface)
121
+ }
122
+ const { data } = await axios.get(`${prefixUrl(featuresEndpoint)}?${query.toString()}`)
123
+ return Array.isArray(data?.features) ? data.features : []
124
+ } catch {
125
+ return null
126
+ }
127
+ }
128
+
129
+ /**
130
+ * The record's documents a feature can be run on.
131
+ *
132
+ * @param {string} url The files endpoint.
133
+ * @return {Promise<Array<{id: string, title: string}>|null>} The documents, or null.
134
+ */
135
+ export async function fetchDocuments(url) {
136
+ try {
137
+ const { data } = await axios.get(prefixUrl(url))
138
+ const rows = Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : [])
139
+ return rows
140
+ .filter((row) => row && row.id !== undefined && row.id !== null)
141
+ .map((row) => ({ id: String(row.id), title: String(row.title ?? row.name ?? row.id) }))
142
+ } catch {
143
+ return null
144
+ }
145
+ }
146
+
147
+ /**
148
+ * The prompts offered on the record, as the endpoint holds them.
149
+ *
150
+ * @param {string} url The prompts endpoint.
151
+ * @return {Promise<Array<{id: string, label: string, prompt: string}>|null>} The prompts, or null.
152
+ */
153
+ export async function fetchPrompts(url) {
154
+ try {
155
+ const { data } = await axios.get(prefixUrl(url))
156
+ return Array.isArray(data?.prompts) ? data.prompts : []
157
+ } catch {
158
+ return null
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Ask one prompt about the record.
164
+ *
165
+ * @param {string} url The ask endpoint.
166
+ * @param {string} objectKey Body key naming the record.
167
+ * @param {string} objectId The record id.
168
+ * @param {string} message The prompt.
169
+ * @return {Promise<object>} `{reply, model?}`.
170
+ */
171
+ export async function askAbout(url, objectKey, objectId, message) {
172
+ const { data } = await axios.post(prefixUrl(url), { [objectKey]: objectId, message })
173
+ return data
174
+ }
175
+
176
+ /**
177
+ * Where a feature runs, in words. A feature hermiq does not offer reads
178
+ * unavailable, never local.
179
+ *
180
+ * @param {object} feature One feature row.
181
+ * @return {string} The sentence.
182
+ */
183
+ export function whereItRuns(feature) {
184
+ if (feature?.available !== true) {
185
+ return t('nextcloud-vue', 'Unavailable: hermiq does not offer this feature here.')
186
+ }
187
+ const provider = String(feature.provider ?? '').trim()
188
+ const residency = String(feature.residency ?? '').trim()
189
+ if (provider === '' || residency === '') {
190
+ return t('nextcloud-vue', 'hermiq did not say which provider runs this, or where.')
191
+ }
192
+ return t('nextcloud-vue', '{provider}, runs in {residency}', { provider, residency })
193
+ }
194
+
195
+ /**
196
+ * Run one AI feature on one document, through hermiq.
197
+ *
198
+ * @param {object} request The run.
199
+ * @param {string} request.feature The feature slug.
200
+ * @param {string|number} request.documentReference The document's file id.
201
+ * @param {string} request.instruction What to do with it.
202
+ * @param {string} [request.runEndpoint] Endpoint template with `{slug}`.
203
+ * @return {Promise<object>} hermiq's answer.
204
+ * @throws {AiFeatureRefusal} When the reference is missing, or hermiq refuses.
205
+ */
206
+ export async function runFeatureOnDocument({ feature, documentReference, instruction, runEndpoint = DEFAULT_RUN_ENDPOINT }) {
207
+ const reference = String(documentReference ?? '').trim()
208
+ if (/^\d+$/.test(reference) === false) {
209
+ throw new AiFeatureRefusal(feature, t('nextcloud-vue', 'No document was chosen.'), MISSING_REFERENCE)
210
+ }
211
+
212
+ const url = prefixUrl(runEndpoint.replace('{slug}', encodeURIComponent(feature)))
213
+ try {
214
+ const response = await axios.post(url, { documentReference: reference, instruction })
215
+ return response.data
216
+ } catch (error) {
217
+ const status = error?.response?.status
218
+ const data = error?.response?.data ?? {}
219
+ if ([400, 403, 404, 422].includes(status) === true) {
220
+ throw new AiFeatureRefusal(feature, String(data.error ?? ''), data.gate ?? null)
221
+ }
222
+ throw error
223
+ }
224
+ }
225
+
226
+ /**
227
+ * The sentence a reader sees for a refusal, naming the feature and the reason.
228
+ *
229
+ * @param {AiFeatureRefusal} refusal The refusal.
230
+ * @param {string} featureLabel The feature's name.
231
+ * @return {string} The sentence.
232
+ */
233
+ export function refusalSentence(refusal, featureLabel) {
234
+ if (refusal.gate === MISSING_REFERENCE) {
235
+ return t('nextcloud-vue', '{feature} reads a document, and no document was chosen.', { feature: featureLabel })
236
+ }
237
+ if (refusal.gate === 'redaction') {
238
+ return t('nextcloud-vue', '{feature} will not read a document that has not been redacted.', { feature: featureLabel })
239
+ }
240
+ return t('nextcloud-vue', '{feature} did not run: {reason}', { feature: featureLabel, reason: refusal.reason })
241
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Reports what a person did with each AI suggestion on a record, for the AI
6
+ * oversight trail (EU AI Act art. 14). Every suggestion that is shown ends in
7
+ * exactly one report: accepted, modified or rejected when the person acts on
8
+ * it, and ignored when they do not. An ignored suggestion says why: a newer
9
+ * suggestion replaced it (superseded), its surface was closed (closed), or it
10
+ * stayed open past its lifetime (expired).
11
+ *
12
+ * The app's record-action endpoint writes the local audit entry and hands the
13
+ * decision to hermiq; without hermiq it keeps only the local entry.
14
+ */
15
+ import { getRequestToken } from '@nextcloud/auth'
16
+ import axios from '@nextcloud/axios'
17
+ import { prefixUrl } from './headers.js'
18
+
19
+ /** How long a shown suggestion waits for a decision before it expires. */
20
+ export const SUGGESTION_LIFETIME_MS = 30 * 60 * 1000
21
+
22
+ /**
23
+ * Post one decision to the record-action endpoint.
24
+ *
25
+ * @param {string} endpoint The record-action endpoint.
26
+ * @param {object} body The record-action body.
27
+ * @param {object} options Options.
28
+ * @param {boolean} options.leaving True while the page unloads: the request
29
+ * then uses `fetch` with `keepalive`, which outlives the page.
30
+ * @return {Promise<void>}
31
+ */
32
+ export async function postDecision(endpoint, body, { leaving = false } = {}) {
33
+ const url = prefixUrl(endpoint)
34
+ if (leaving === true) {
35
+ await fetch(url, {
36
+ method: 'POST',
37
+ keepalive: true,
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ requesttoken: getRequestToken() ?? '',
41
+ },
42
+ body: JSON.stringify(body),
43
+ })
44
+ return
45
+ }
46
+
47
+ await axios.post(url, body)
48
+ }
49
+
50
+ /**
51
+ * Track the suggestions shown on one record.
52
+ *
53
+ * A suggestion is shown under a slot key (one feature, or the questions); a
54
+ * newer suggestion in the same slot supersedes an undecided older one.
55
+ *
56
+ * @param {object} options Options.
57
+ * @param {string} options.objectId The record id.
58
+ * @param {string} [options.objectKey] Body key naming the record (default `objectId`).
59
+ * @param {string} [options.endpoint] The record-action endpoint; empty reports nothing.
60
+ * @param {function(object, object): Promise<void>} [options.report] Sends one decision; defaults to posting to `endpoint`.
61
+ * @param {number} [options.lifetimeMs] Lifetime of an undecided suggestion.
62
+ * @param {function(function(): void, number): number} [options.setTimer] Timer factory, for tests.
63
+ * @param {function(number): void} [options.clearTimer] Timer cancel, for tests.
64
+ * @return {object} The tracker: show, decide, dismiss, closeAll, pending.
65
+ */
66
+ export function createSuggestionTracker({
67
+ objectId,
68
+ objectKey = 'objectId',
69
+ endpoint = '',
70
+ report = (body, options) => (endpoint ? postDecision(endpoint, body, options) : Promise.resolve()),
71
+ lifetimeMs = SUGGESTION_LIFETIME_MS,
72
+ setTimer = setTimeout,
73
+ clearTimer = clearTimeout,
74
+ }) {
75
+ const pending = new Map()
76
+ let counter = 0
77
+
78
+ /**
79
+ * Send the report for one slot's suggestion and forget it.
80
+ *
81
+ * @param {string} slot The slot key.
82
+ * @param {object} decision `{userAction, unusedBecause?, actualValue?, reason?}`.
83
+ * @param {object} options Passed to report.
84
+ * @return {Promise<void>}
85
+ */
86
+ function settle(slot, decision, options = {}) {
87
+ const shown = pending.get(slot)
88
+ if (shown === undefined) {
89
+ return Promise.resolve()
90
+ }
91
+
92
+ pending.delete(slot)
93
+ clearTimer(shown.timer)
94
+ const body = {
95
+ [objectKey]: objectId,
96
+ type: shown.type,
97
+ suggestion: shown.text,
98
+ prompt: shown.prompt ?? '',
99
+ model: shown.model ?? '',
100
+ documentId: shown.documentId ?? '',
101
+ suggestionId: shown.id,
102
+ ...decision,
103
+ }
104
+
105
+ return Promise.resolve(report(body, options)).catch(() => {
106
+ // The person's work must not fail because the oversight trail
107
+ // could not be written; the server logs its own failures.
108
+ })
109
+ }
110
+
111
+ return {
112
+ /**
113
+ * Register a suggestion as shown in a slot.
114
+ *
115
+ * @param {string} slot The slot key.
116
+ * @param {object} suggestion `{type, text, prompt?, model?, documentId?}`.
117
+ * @return {string} The suggestion id.
118
+ */
119
+ show(slot, suggestion) {
120
+ settle(slot, { userAction: 'ignored', unusedBecause: 'superseded' })
121
+ counter += 1
122
+ const id = `${Date.now().toString(36)}-${counter}`
123
+ const timer = setTimer(() => {
124
+ settle(slot, { userAction: 'ignored', unusedBecause: 'expired' })
125
+ }, lifetimeMs)
126
+ pending.set(slot, { ...suggestion, id, timer })
127
+ return id
128
+ },
129
+
130
+ /**
131
+ * Report the person's decision on a slot's suggestion.
132
+ *
133
+ * @param {string} slot The slot key.
134
+ * @param {string} userAction accepted, modified or rejected.
135
+ * @param {object} [extra] `{actualValue?, reason?}`.
136
+ * @return {Promise<void>}
137
+ */
138
+ decide(slot, userAction, extra = {}) {
139
+ return settle(slot, { userAction, ...extra })
140
+ },
141
+
142
+ /**
143
+ * Report a slot's undecided suggestion as ignored, for a reason.
144
+ *
145
+ * @param {string} slot The slot key.
146
+ * @param {string} because closed, expired or superseded.
147
+ * @return {Promise<void>}
148
+ */
149
+ dismiss(slot, because) {
150
+ return settle(slot, { userAction: 'ignored', unusedBecause: because })
151
+ },
152
+
153
+ /**
154
+ * Report every undecided suggestion as closed.
155
+ *
156
+ * @param {object} [options] `{leaving}` while the page unloads.
157
+ * @return {Promise<void>}
158
+ */
159
+ closeAll(options = {}) {
160
+ const slots = [...pending.keys()]
161
+ return Promise.all(slots.map((slot) => settle(
162
+ slot,
163
+ { userAction: 'ignored', unusedBecause: 'closed' },
164
+ options,
165
+ ))).then(() => undefined)
166
+ },
167
+
168
+ /**
169
+ * Whether a slot holds an undecided suggestion.
170
+ *
171
+ * @param {string} slot The slot key.
172
+ * @return {boolean} True while undecided.
173
+ */
174
+ pending(slot) {
175
+ return pending.has(slot)
176
+ },
177
+ }
178
+ }
@@ -75,6 +75,7 @@ export const BUILT_IN_WIDGET_KEYS = Object.freeze([
75
75
  'divider',
76
76
  'saved-view-alert',
77
77
  'exports',
78
+ 'object-translations',
78
79
  ])
79
80
 
80
81
  /**