@conduction/nextcloud-vue 2.119.0 → 2.121.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.
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +116 -16
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +207 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +207 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +951 -16
- package/dist/esm/utils/aiFeatures.js +245 -0
- package/dist/esm/utils/aiFeatures.js.map +1 -0
- package/dist/esm/utils/aiSuggestionTracker.js +182 -0
- package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectPermissions.js +552 -0
- package/dist/esm/utils/objectPermissions.js.map +1 -0
- package/dist/esm/utils/objectTranslations.js +180 -0
- package/dist/esm/utils/objectTranslations.js.map +1 -0
- package/dist/esm/utils/stateMachineValidation.js +254 -0
- package/dist/esm/utils/stateMachineValidation.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +3 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +23038 -14567
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +951 -16
- package/l10n/en.json +207 -1
- package/l10n/nl.json +207 -1
- package/package.json +1 -1
- package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
- package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
- package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
- package/src/components/CnAiFeaturesWidget/index.js +14 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
- package/src/components/CnIndexPage/useNamedSource.js +9 -0
- package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
- package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
- package/src/components/CnObjectPermissionsTab/index.js +13 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
- package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
- package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
- package/src/components/CnObjectTranslationsWidget/index.js +13 -0
- package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
- package/src/components/CnReadinessChecklist/index.js +9 -0
- package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
- package/src/components/CnSchemaRulesWidget/index.js +10 -0
- package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
- package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
- package/src/components/CnSearchIndexStatus/index.js +10 -0
- package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
- package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
- package/src/components/CnShippedConfigurationSection/index.js +9 -0
- package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
- package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
- package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
- package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
- package/src/components/CnStateMachineEditor/index.js +8 -0
- package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
- package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
- package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
- package/src/components/index.js +8 -0
- package/src/composables/indexSources.js +116 -16
- package/src/css/index-page.css +7 -0
- package/src/index.js +9 -0
- package/src/utils/aiFeatures.js +241 -0
- package/src/utils/aiSuggestionTracker.js +178 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/objectPermissions.js +548 -0
- package/src/utils/objectTranslations.js +176 -0
- package/src/utils/stateMachineValidation.js +250 -0
- package/src/utils/widgetDispatch.js +3 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
3
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
4
|
+
*
|
|
5
|
+
* CnAiFeaturesWidget: a refusal is shown naming the feature and hermiq's gate,
|
|
6
|
+
* a transport failure apart from it, a feature hermiq does not offer gets no
|
|
7
|
+
* run, a failed read says the panel is incomplete, prompts are offered and
|
|
8
|
+
* answered, and every suggestion ends in one oversight report.
|
|
9
|
+
*/
|
|
10
|
+
import axios from '@nextcloud/axios'
|
|
11
|
+
import { mount } from '@vue/test-utils'
|
|
12
|
+
import CnAiFeaturesWidget from '../CnAiFeaturesWidget.vue'
|
|
13
|
+
|
|
14
|
+
jest.mock('@nextcloud/axios', () => ({ __esModule: true, default: { get: jest.fn(), post: jest.fn() } }))
|
|
15
|
+
jest.mock('@nextcloud/auth', () => ({ __esModule: true, getRequestToken: () => 'token' }))
|
|
16
|
+
jest.mock('@nextcloud/router', () => ({ __esModule: true, generateUrl: (path) => path }))
|
|
17
|
+
|
|
18
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
19
|
+
|
|
20
|
+
const SUMMARY = { slug: 'document-summary', available: true, provider: 'Hermiq', residency: 'on-premise' }
|
|
21
|
+
|
|
22
|
+
const PROPS = {
|
|
23
|
+
objectId: 'c-1',
|
|
24
|
+
register: 'reg',
|
|
25
|
+
schema: 'case',
|
|
26
|
+
declaration: { typeField: 'caseType', typeSchema: 'caseType', field: 'aiFeatures', surface: 'case' },
|
|
27
|
+
featuresEndpoint: '/apps/app/api/assistant/ai-features',
|
|
28
|
+
featuresQueryKey: 'caseType[aiFeatures]',
|
|
29
|
+
promptsEndpoint: '/apps/app/api/assistant/prompts',
|
|
30
|
+
askEndpoint: '/apps/app/api/assistant/converse',
|
|
31
|
+
decisionEndpoint: '/apps/app/api/ai/record-action',
|
|
32
|
+
objectKey: 'caseId',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stubs = {
|
|
36
|
+
NcLoadingIcon: true,
|
|
37
|
+
NcButton: { props: ['disabled', 'type', 'variant'], template: '<button v-bind="$attrs" :type="type || \'button\'" :disabled="disabled"><slot /></button>' },
|
|
38
|
+
NcNoteCard: { props: ['type'], template: '<div class="note" v-bind="$attrs"><slot /></div>' },
|
|
39
|
+
NcSelect: {
|
|
40
|
+
props: ['options', 'modelValue', 'inputLabel', 'inputId'],
|
|
41
|
+
emits: ['update:modelValue'],
|
|
42
|
+
template: '<label><span>{{ inputLabel }}</span><select v-bind="$attrs" :id="inputId" @change="$emit(\'update:modelValue\', options.find((o) => o.id === $event.target.value) || null)"><option value="" /><option v-for="o in options" :key="o.id" :value="o.id">{{ o.label }}</option></select></label>',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Answer the reads: features and prompts as given, documents as one file.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} answers `{features, prompts, featuresFail}`.
|
|
50
|
+
*/
|
|
51
|
+
function reads({ features = [SUMMARY], prompts = [], featuresFail = false } = {}) {
|
|
52
|
+
axios.get.mockImplementation((url) => {
|
|
53
|
+
if (url.endsWith('/objects/reg/case/c-1')) {
|
|
54
|
+
return featuresFail ? Promise.reject(new Error('down')) : Promise.resolve({ data: { caseType: 'ct-1' } })
|
|
55
|
+
}
|
|
56
|
+
if (url.endsWith('/objects/reg/caseType/ct-1')) {
|
|
57
|
+
return Promise.resolve({ data: { aiFeatures: Object.fromEntries(features.map((f) => [f.slug, 'case'])) } })
|
|
58
|
+
}
|
|
59
|
+
if (url.includes('/assistant/ai-features')) {
|
|
60
|
+
return Promise.resolve({ data: { features } })
|
|
61
|
+
}
|
|
62
|
+
if (url.endsWith('/assistant/prompts')) {
|
|
63
|
+
return Promise.resolve({ data: { prompts } })
|
|
64
|
+
}
|
|
65
|
+
if (url.endsWith('/files')) {
|
|
66
|
+
return Promise.resolve({ data: { results: [{ id: 42, title: 'besluit.pdf' }] } })
|
|
67
|
+
}
|
|
68
|
+
return Promise.reject(new Error(`unexpected ${url}`))
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function mountWidget(props = {}) {
|
|
73
|
+
const wrapper = mount(CnAiFeaturesWidget, { props: { ...PROPS, ...props }, global: { stubs } })
|
|
74
|
+
await flush()
|
|
75
|
+
await flush()
|
|
76
|
+
await flush()
|
|
77
|
+
await wrapper.vm.$nextTick()
|
|
78
|
+
return wrapper
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const decisions = () => axios.post.mock.calls.filter(([url]) => url === '/apps/app/api/ai/record-action').map(([, body]) => body)
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
axios.get.mockReset()
|
|
85
|
+
axios.post.mockReset()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('CnAiFeaturesWidget', () => {
|
|
89
|
+
it("renders a redaction refusal naming the feature and hermiq's gate", async () => {
|
|
90
|
+
reads()
|
|
91
|
+
axios.post.mockRejectedValue({ response: { status: 422, data: { error: 'The document is not redacted.', gate: 'redaction' } } })
|
|
92
|
+
const wrapper = await mountWidget()
|
|
93
|
+
await wrapper.find('select').setValue('42')
|
|
94
|
+
await wrapper.find('form').trigger('submit')
|
|
95
|
+
await flush()
|
|
96
|
+
const note = wrapper.find('[data-testid="cn-ai-feature-refusal-document-summary"]')
|
|
97
|
+
expect(note.text()).toContain('document-summary will not read a document that has not been redacted.')
|
|
98
|
+
expect(note.text()).toContain('Gate: redaction')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('labels the document picker', async () => {
|
|
102
|
+
reads()
|
|
103
|
+
const wrapper = await mountWidget()
|
|
104
|
+
expect(wrapper.find('label').text()).toContain('Document')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('refuses a run with no document before anything is sent', async () => {
|
|
108
|
+
reads()
|
|
109
|
+
const wrapper = await mountWidget()
|
|
110
|
+
await wrapper.find('form').trigger('submit')
|
|
111
|
+
await flush()
|
|
112
|
+
expect(axios.post).not.toHaveBeenCalled()
|
|
113
|
+
expect(wrapper.find('[data-testid="cn-ai-feature-refusal-document-summary"]').text()).toContain('no document was chosen')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('shows a transport failure apart from a refusal', async () => {
|
|
117
|
+
reads()
|
|
118
|
+
axios.post.mockRejectedValue(new Error('Network Error'))
|
|
119
|
+
const wrapper = await mountWidget()
|
|
120
|
+
await wrapper.find('select').setValue('42')
|
|
121
|
+
await wrapper.find('form').trigger('submit')
|
|
122
|
+
await flush()
|
|
123
|
+
expect(wrapper.find('[data-testid="cn-ai-feature-failed-document-summary"]').exists()).toBe(true)
|
|
124
|
+
expect(wrapper.find('[data-testid="cn-ai-feature-refusal-document-summary"]').exists()).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('offers no run for a feature hermiq does not offer, and reads no documents', async () => {
|
|
128
|
+
reads({ features: [{ slug: 'triage', available: false }] })
|
|
129
|
+
const wrapper = await mountWidget()
|
|
130
|
+
expect(wrapper.find('form').exists()).toBe(false)
|
|
131
|
+
expect(wrapper.text()).toContain('Unavailable: hermiq does not offer this feature here.')
|
|
132
|
+
expect(axios.get.mock.calls.some(([url]) => url.endsWith('/files'))).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('says the panel is incomplete when the features could not be read', async () => {
|
|
136
|
+
reads({ featuresFail: true })
|
|
137
|
+
const wrapper = await mountWidget()
|
|
138
|
+
expect(wrapper.find('[data-testid="cn-ai-features-unreadable"]').exists()).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("offers the prompts and shows the answer to one, or the assistant's refusal", async () => {
|
|
142
|
+
reads({ features: [], prompts: [{ id: 'p-1', label: 'Vraag', prompt: 'Vraag?' }] })
|
|
143
|
+
axios.post.mockResolvedValueOnce({ data: { reply: 'Ja.' } })
|
|
144
|
+
const wrapper = await mountWidget()
|
|
145
|
+
await wrapper.find('[data-testid="cn-ai-question-p-1"]').trigger('click')
|
|
146
|
+
await flush()
|
|
147
|
+
expect(axios.post.mock.calls[0]).toEqual(['/apps/app/api/assistant/converse', { caseId: 'c-1', message: 'Vraag?' }])
|
|
148
|
+
expect(wrapper.find('[data-testid="cn-ai-question-answer"]').text()).toBe('Ja.')
|
|
149
|
+
|
|
150
|
+
// The open answer is reported superseded first, then the question is asked.
|
|
151
|
+
axios.post.mockResolvedValueOnce({ data: {} }).mockRejectedValueOnce({ response: { data: { error: 'Not now.' } } })
|
|
152
|
+
await wrapper.find('[data-testid="cn-ai-question-p-1"]').trigger('click')
|
|
153
|
+
await flush()
|
|
154
|
+
expect(wrapper.find('[data-testid="cn-ai-question-failed"]').text()).toBe('Not now.')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('renders no questions when none are offered', async () => {
|
|
158
|
+
reads({ features: [] })
|
|
159
|
+
const wrapper = await mountWidget()
|
|
160
|
+
expect(wrapper.find('[data-testid="cn-ai-questions"]').exists()).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('reports a used run output to the oversight trail', async () => {
|
|
164
|
+
reads()
|
|
165
|
+
axios.post.mockResolvedValue({ data: { output: 'Verleend.' } })
|
|
166
|
+
const wrapper = await mountWidget()
|
|
167
|
+
await wrapper.find('select').setValue('42')
|
|
168
|
+
await wrapper.find('form').trigger('submit')
|
|
169
|
+
await flush()
|
|
170
|
+
expect(wrapper.find('[data-testid="cn-ai-feature-output-document-summary"]').text()).toBe('Verleend.')
|
|
171
|
+
await wrapper.find('[data-testid="cn-ai-feature-decision-document-summary-use"]').trigger('click')
|
|
172
|
+
await flush()
|
|
173
|
+
expect(decisions()).toHaveLength(1)
|
|
174
|
+
expect(decisions()[0]).toMatchObject({ caseId: 'c-1', type: 'decision_support', userAction: 'accepted', suggestion: 'Verleend.', documentId: '42' })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('reports an adapted answer with the version that was used', async () => {
|
|
178
|
+
reads({ features: [], prompts: [{ id: 'p-1', label: 'Vraag', prompt: 'Vraag?' }] })
|
|
179
|
+
axios.post.mockResolvedValue({ data: { reply: 'Ja.' } })
|
|
180
|
+
const wrapper = await mountWidget()
|
|
181
|
+
await wrapper.find('[data-testid="cn-ai-question-p-1"]').trigger('click')
|
|
182
|
+
await flush()
|
|
183
|
+
await wrapper.find('[data-testid="cn-ai-question-decision-adapt"]').trigger('click')
|
|
184
|
+
await wrapper.find('[data-testid="cn-ai-question-decision-draft"]').setValue('Ja, mits.')
|
|
185
|
+
await wrapper.find('[data-testid="cn-ai-question-decision-use-adapted"]').trigger('click')
|
|
186
|
+
await flush()
|
|
187
|
+
expect(decisions()[0]).toMatchObject({ type: 'qa', userAction: 'modified', suggestion: 'Ja.', actualValue: 'Ja, mits.', prompt: 'Vraag?' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('reports an answer nobody acted on as superseded by the next question, then closed', async () => {
|
|
191
|
+
reads({ features: [], prompts: [{ id: 'p-1', label: 'Vraag', prompt: 'Vraag?' }] })
|
|
192
|
+
axios.post.mockResolvedValueOnce({ data: { reply: 'Eerste.' } }).mockResolvedValueOnce({ data: {} }).mockResolvedValue({ data: { reply: 'Tweede.' } })
|
|
193
|
+
const wrapper = await mountWidget()
|
|
194
|
+
await wrapper.find('[data-testid="cn-ai-question-p-1"]').trigger('click')
|
|
195
|
+
await flush()
|
|
196
|
+
await wrapper.find('[data-testid="cn-ai-question-p-1"]').trigger('click')
|
|
197
|
+
await flush()
|
|
198
|
+
wrapper.unmount()
|
|
199
|
+
await flush()
|
|
200
|
+
expect(decisions()).toHaveLength(2)
|
|
201
|
+
expect(decisions()[0]).toMatchObject({ userAction: 'ignored', unusedBecause: 'superseded', suggestion: 'Eerste.' })
|
|
202
|
+
expect(decisions()[1]).toMatchObject({ userAction: 'ignored', unusedBecause: 'closed' })
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('CnObjectSidebar built-in', () => {
|
|
207
|
+
it('resolves the ai-features widget type', () => {
|
|
208
|
+
const fs = require('fs')
|
|
209
|
+
const path = require('path')
|
|
210
|
+
const source = fs.readFileSync(path.resolve(__dirname, '../../CnObjectSidebar/CnObjectSidebar.vue'), 'utf8')
|
|
211
|
+
expect(source).toMatch(/'ai-features': CnAiFeaturesWidget/)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnAiFeaturesWidget — the hermiq AI features a record's type switches on,
|
|
6
|
+
* with where each runs, a run on a record document, refusals in words and an
|
|
7
|
+
* oversight decision on every suggestion. The CnObjectSidebar built-in
|
|
8
|
+
* `ai-features`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnAiFeaturesWidget from './CnAiFeaturesWidget.vue'
|
|
12
|
+
|
|
13
|
+
export { CnAiFeaturesWidget }
|
|
14
|
+
export default CnAiFeaturesWidget
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
:mapLabel="mapLabel"
|
|
211
211
|
:mapIcon="mapIcon"
|
|
212
212
|
:listIcon="listIcon"
|
|
213
|
-
:showSearch="search !== false && (inlineSearch || isBoardLook)"
|
|
213
|
+
:showSearch="search !== false && (inlineSearch || isBoardLook) && !sourceRefusal"
|
|
214
214
|
:searchPlacement="searchPlacement"
|
|
215
215
|
:searchValue="effectiveSearchValue"
|
|
216
216
|
:searchPlaceholder="presented('searchPlaceholder') ? cnTranslate(presented('searchPlaceholder')) : presented('searchPlaceholder')"
|
|
@@ -745,12 +745,31 @@
|
|
|
745
745
|
<span v-if="scopeHeading.subtitle" class="cn-index-page__scope-subtitle" data-testid="cn-index-page-scope-subtitle">{{ scopeHeading.subtitle }}</span>
|
|
746
746
|
</div>
|
|
747
747
|
|
|
748
|
+
<!-- A note the named source read off its answer ("Filters run in
|
|
749
|
+
this order: ..."). -->
|
|
750
|
+
<p v-if="sourceNote && !sourceRefusal"
|
|
751
|
+
class="cn-index-page__source-note"
|
|
752
|
+
data-testid="cn-index-page-source-note">
|
|
753
|
+
{{ sourceNote }}
|
|
754
|
+
</p>
|
|
755
|
+
|
|
748
756
|
<!-- Loading state — initial fetch only; a background refresh keeps the table visible -->
|
|
749
757
|
<div v-if="showInitialLoader" class="cn-index-page__loading">
|
|
750
758
|
<!-- name gives NcLoadingIcon a non-empty aria-label (WCAG role-img-alt); empty name ships an unlabeled role="img" -->
|
|
751
759
|
<NcLoadingIcon :size="32" :name="loadingText" />
|
|
752
760
|
</div>
|
|
753
761
|
|
|
762
|
+
<!-- The named source's server refused this reader: its sentence
|
|
763
|
+
instead of the rows, never an empty list. -->
|
|
764
|
+
<div v-else-if="sourceRefusal"
|
|
765
|
+
class="cn-index-page__source-refusal"
|
|
766
|
+
role="status"
|
|
767
|
+
data-testid="cn-index-page-source-refusal">
|
|
768
|
+
<NcNoteCard type="error">
|
|
769
|
+
{{ sourceRefusal }}
|
|
770
|
+
</NcNoteCard>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
754
773
|
<!-- A search the server refused for its grammar: the term with the
|
|
755
774
|
character it stopped at, the server's reason, and what to do. -->
|
|
756
775
|
<div v-else-if="searchRefusal"
|
|
@@ -774,7 +793,7 @@
|
|
|
774
793
|
</div>
|
|
775
794
|
|
|
776
795
|
<!-- Error state: the latest self-fetch failed (e.g. a search term the server rejects) -->
|
|
777
|
-
<div v-else-if="selfFetchFailed"
|
|
796
|
+
<div v-else-if="selfFetchFailed || sourceFailed"
|
|
778
797
|
class="cn-index-page__empty"
|
|
779
798
|
role="status"
|
|
780
799
|
data-testid="cn-index-page-fetch-error">
|
|
@@ -4107,6 +4126,8 @@ export default {
|
|
|
4107
4126
|
const namedPageSize = Number(props.sourceConfig && props.sourceConfig.limit) > 0 ? Number(props.sourceConfig.limit) : 20
|
|
4108
4127
|
const namedPaging = ref({ page: 1, limit: namedPageSize })
|
|
4109
4128
|
const namedActiveFilters = ref(namedFiltersFromRoute(getCurrentInstance(), props))
|
|
4129
|
+
// The search term on a named source that searches (`searchParam`).
|
|
4130
|
+
const namedSearchTerm = ref('')
|
|
4110
4131
|
|
|
4111
4132
|
const {
|
|
4112
4133
|
isNamedSource,
|
|
@@ -4114,7 +4135,7 @@ export default {
|
|
|
4114
4135
|
namedRows,
|
|
4115
4136
|
namedLoading,
|
|
4116
4137
|
namedQuickFilters,
|
|
4117
|
-
} = useNamedSource(props, { activeQuickFilterIndex, activeFilters: namedActiveFilters, paging: namedPaging })
|
|
4138
|
+
} = useNamedSource(props, { activeQuickFilterIndex, activeFilters: namedActiveFilters, paging: namedPaging, searchTerm: namedSearchTerm })
|
|
4118
4139
|
|
|
4119
4140
|
return {
|
|
4120
4141
|
isBoard: isBoardLook,
|
|
@@ -4127,6 +4148,7 @@ export default {
|
|
|
4127
4148
|
namedQuickFilters,
|
|
4128
4149
|
namedActiveFilters,
|
|
4129
4150
|
namedPaging,
|
|
4151
|
+
namedSearchTerm,
|
|
4130
4152
|
contextMenuOpen,
|
|
4131
4153
|
contextMenuRow,
|
|
4132
4154
|
openContextMenu,
|
|
@@ -4536,6 +4558,46 @@ export default {
|
|
|
4536
4558
|
return this.selfFetchFailed ? readSearchRefusal(this.list.error.value, this.effectiveSearchValue) : null
|
|
4537
4559
|
},
|
|
4538
4560
|
|
|
4561
|
+
/**
|
|
4562
|
+
* Whether the named source sends the search term to its server.
|
|
4563
|
+
*
|
|
4564
|
+
* @return {boolean}
|
|
4565
|
+
*/
|
|
4566
|
+
namedSourceSearches() {
|
|
4567
|
+
return this.isNamedSource && !!this.namedSource && typeof this.namedSource.searchParam === 'string' && this.namedSource.searchParam !== ''
|
|
4568
|
+
},
|
|
4569
|
+
|
|
4570
|
+
/**
|
|
4571
|
+
* The sentence the named source's server refused this reader with, or ''.
|
|
4572
|
+
* While it holds the page shows it instead of the rows, the search and
|
|
4573
|
+
* the tabs: a list nobody may read is not an empty list.
|
|
4574
|
+
*
|
|
4575
|
+
* @return {string}
|
|
4576
|
+
* @spec openspec/changes/index-endpoint-source-states/specs/index-page/spec.md
|
|
4577
|
+
*/
|
|
4578
|
+
sourceRefusal() {
|
|
4579
|
+
return this.isNamedSource && this.namedSource && typeof this.namedSource.refusal === 'function' ? (this.namedSource.refusal() || '') : ''
|
|
4580
|
+
},
|
|
4581
|
+
|
|
4582
|
+
/**
|
|
4583
|
+
* Whether the named source's last load failed (not refused).
|
|
4584
|
+
*
|
|
4585
|
+
* @return {boolean}
|
|
4586
|
+
*/
|
|
4587
|
+
sourceFailed() {
|
|
4588
|
+
return this.isNamedSource && !!this.namedSource && typeof this.namedSource.failed === 'function' && this.namedSource.failed() === true
|
|
4589
|
+
},
|
|
4590
|
+
|
|
4591
|
+
/**
|
|
4592
|
+
* The note the named source read off its last answer, or ''.
|
|
4593
|
+
*
|
|
4594
|
+
* @return {string}
|
|
4595
|
+
* @spec openspec/changes/index-endpoint-source-states/specs/index-page/spec.md
|
|
4596
|
+
*/
|
|
4597
|
+
sourceNote() {
|
|
4598
|
+
return this.isNamedSource && this.namedSource && typeof this.namedSource.note === 'function' ? (this.namedSource.note() || '') : ''
|
|
4599
|
+
},
|
|
4600
|
+
|
|
4539
4601
|
/** @return {string} What to do after a refused search. */
|
|
4540
4602
|
searchRefusalHintText() {
|
|
4541
4603
|
if (this.searchRefusalHint) {
|
|
@@ -5821,6 +5883,9 @@ export default {
|
|
|
5821
5883
|
},
|
|
5822
5884
|
|
|
5823
5885
|
tabStripEntries() {
|
|
5886
|
+
if (this.sourceRefusal) {
|
|
5887
|
+
return []
|
|
5888
|
+
}
|
|
5824
5889
|
if (this.lensTabs.length > 0) {
|
|
5825
5890
|
return this.lensTabs
|
|
5826
5891
|
}
|
|
@@ -6921,6 +6986,9 @@ export default {
|
|
|
6921
6986
|
|
|
6922
6987
|
/** Search term / visible columns / active facet filters for the embedded sidebar. */
|
|
6923
6988
|
effectiveSearchValue() {
|
|
6989
|
+
if (this.namedSourceSearches) {
|
|
6990
|
+
return this.namedSearchTerm || ''
|
|
6991
|
+
}
|
|
6924
6992
|
return this.isSelfFetchMode ? (this.list.searchTerm.value || '') : (this.searchValue || '')
|
|
6925
6993
|
},
|
|
6926
6994
|
|
|
@@ -9691,6 +9759,9 @@ export default {
|
|
|
9691
9759
|
* @return {void}
|
|
9692
9760
|
*/
|
|
9693
9761
|
onSearchEvent(value) {
|
|
9762
|
+
if (this.namedSourceSearches) {
|
|
9763
|
+
this.namedSearchTerm = value || ''
|
|
9764
|
+
}
|
|
9694
9765
|
if (this.isSelfFetchMode && typeof this.list.onSearch === 'function') {
|
|
9695
9766
|
this.list.onSearch(value)
|
|
9696
9767
|
this.persistViewStateToRoute(this.currentViewState())
|
|
@@ -59,6 +59,9 @@ import { searchFieldParams } from '../../utils/searchFieldParams.js'
|
|
|
59
59
|
* chosen values, `{ fieldKey: values }`. Mapped through the source's
|
|
60
60
|
* `searchFields` declaration into loader arguments and merged into every
|
|
61
61
|
* load, so a manifest page's sidebar narrows the SERVER's query.
|
|
62
|
+
* @param {import('vue').Ref<string>} [options.searchTerm] The page's search
|
|
63
|
+
* term. A source that names a `searchParam` gets it as that argument, so the
|
|
64
|
+
* server searches; a new term reloads from the first page.
|
|
62
65
|
*
|
|
63
66
|
* @return {object} { isNamedSource, namedSource, namedRows, namedLoading, namedQuickFilters }
|
|
64
67
|
*/
|
|
@@ -106,6 +109,7 @@ export function useNamedSource(props, options = {}) {
|
|
|
106
109
|
const tabs = manifestTabs || sourceTabs
|
|
107
110
|
const activeIndex = options.activeQuickFilterIndex || null
|
|
108
111
|
const activeFilters = options.activeFilters || null
|
|
112
|
+
const searchTerm = (typeof source.searchParam === 'string' && source.searchParam !== '') ? (options.searchTerm || null) : null
|
|
109
113
|
// A source that pages (`pages: true`) gets the page as `limit` and
|
|
110
114
|
// `offset`, from the pager the page draws for it.
|
|
111
115
|
const paging = source.pages === true ? (options.paging || null) : null
|
|
@@ -169,6 +173,7 @@ export function useNamedSource(props, options = {}) {
|
|
|
169
173
|
activeFilters ? activeFilters.value : null,
|
|
170
174
|
props.entitySource,
|
|
171
175
|
),
|
|
176
|
+
...(searchTerm && searchTerm.value ? { [source.searchParam]: searchTerm.value } : {}),
|
|
172
177
|
...(paging ? { limit: paging.value.limit, offset: (paging.value.page - 1) * paging.value.limit } : {}),
|
|
173
178
|
},
|
|
174
179
|
tokenCtx(),
|
|
@@ -218,6 +223,10 @@ export function useNamedSource(props, options = {}) {
|
|
|
218
223
|
watch(activeFilters, reloadFromFirstPage, { deep: true })
|
|
219
224
|
}
|
|
220
225
|
|
|
226
|
+
if (searchTerm) {
|
|
227
|
+
watch(searchTerm, reloadFromFirstPage)
|
|
228
|
+
}
|
|
229
|
+
|
|
221
230
|
// A new page, or a new page size, is a new request. A new tab or filter
|
|
222
231
|
// first returns to page 1, which reloads through here.
|
|
223
232
|
if (paging) {
|