@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The OpenRegister translation reads and the one write behind
|
|
6
|
+
* CnObjectTranslationsWidget. The engine is OpenRegister's; nothing here
|
|
7
|
+
* translates or stores anything of its own.
|
|
8
|
+
*
|
|
9
|
+
* GET {apiBase}/registers/{register} languages, source first
|
|
10
|
+
* GET {apiBase}/registers/{register}/schemas property titles
|
|
11
|
+
* GET {apiBase}/objects/{register}/{schema}/{id}?_translations=all&_translationMeta=true
|
|
12
|
+
* GET {apiBase}/translations/object/{uuid}?schema={schema} per-value workflow status
|
|
13
|
+
* PATCH {apiBase}/objects/{register}/{schema}/{id} { property: languageMap }
|
|
14
|
+
*
|
|
15
|
+
* The WHOLE language map is written and no `X-Translation-Target-Language` is
|
|
16
|
+
* sent: OpenRegister refuses a language-keyed body that arrives with that
|
|
17
|
+
* header, and keeps one that arrives without it.
|
|
18
|
+
*
|
|
19
|
+
* The translatable properties come from `_meta.languageMeta`, which OpenRegister
|
|
20
|
+
* builds from the schema it imported, so a property it never read as
|
|
21
|
+
* translatable is not offered.
|
|
22
|
+
*
|
|
23
|
+
* A failed read throws: an outage and an object with nothing translated look
|
|
24
|
+
* the same from the browser, and only one of them is somebody's problem.
|
|
25
|
+
*/
|
|
26
|
+
import axios from '@nextcloud/axios'
|
|
27
|
+
import { prefixUrl } from './headers.js'
|
|
28
|
+
|
|
29
|
+
/** The OpenRegister API root a target defaults to. */
|
|
30
|
+
export const DEFAULT_API_BASE = '/apps/openregister/api'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A translation row is stale when the source value moved under it; OpenRegister
|
|
34
|
+
* flips a derived row to `outdated`. A stale value does not count as translated.
|
|
35
|
+
*
|
|
36
|
+
* @type {string}
|
|
37
|
+
*/
|
|
38
|
+
export const STATUS_OUTDATED = 'outdated'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The slug of a schema given as a slug or as the schema object.
|
|
42
|
+
*
|
|
43
|
+
* @param {string|object} schema The schema.
|
|
44
|
+
* @return {string} The slug (or id).
|
|
45
|
+
*/
|
|
46
|
+
export function schemaSlugOf(schema) {
|
|
47
|
+
if (schema && typeof schema === 'object') {
|
|
48
|
+
return String(schema.slug || schema.id || schema.uuid || '')
|
|
49
|
+
}
|
|
50
|
+
return String(schema || '')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalise a target to `{register, schema, apiBase}`.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} target `{register, schema, apiBase?}`.
|
|
57
|
+
* @return {{register: string, schema: string, apiBase: string}} The target.
|
|
58
|
+
*/
|
|
59
|
+
function normalise(target = {}) {
|
|
60
|
+
return {
|
|
61
|
+
register: String(target.register || ''),
|
|
62
|
+
schema: schemaSlugOf(target.schema),
|
|
63
|
+
apiBase: String(target.apiBase || DEFAULT_API_BASE).replace(/\/$/, ''),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The languages the register serves, source language first.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} target The target.
|
|
71
|
+
* @return {Promise<string[]>} The language codes.
|
|
72
|
+
*/
|
|
73
|
+
export async function registerLanguages(target) {
|
|
74
|
+
const { apiBase, register } = normalise(target)
|
|
75
|
+
const { data } = await axios.get(prefixUrl(`${apiBase}/registers/${register}`))
|
|
76
|
+
const languages = data?.languages ?? data?.register?.languages ?? []
|
|
77
|
+
return Array.isArray(languages) ? languages.filter((code) => typeof code === 'string' && code !== '') : []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One object with every language of every value, and its label envelope.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} target The target.
|
|
84
|
+
* @param {string} id The object id.
|
|
85
|
+
* @return {Promise<{object: object, languageMeta: object}>} The values and the envelope.
|
|
86
|
+
*/
|
|
87
|
+
export async function objectWithTranslations(target, id) {
|
|
88
|
+
const { apiBase, register, schema } = normalise(target)
|
|
89
|
+
const { data } = await axios.get(
|
|
90
|
+
prefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`),
|
|
91
|
+
{ params: { _translations: 'all', _translationMeta: 'true' } },
|
|
92
|
+
)
|
|
93
|
+
const object = data?.object ?? data ?? {}
|
|
94
|
+
return { object, languageMeta: object?._meta?.languageMeta ?? {} }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The schema's property definitions, for their titles (`languageMeta` carries
|
|
99
|
+
* none).
|
|
100
|
+
*
|
|
101
|
+
* @param {object} target The target.
|
|
102
|
+
* @return {Promise<object>} Property name to its definition.
|
|
103
|
+
*/
|
|
104
|
+
export async function schemaProperties(target) {
|
|
105
|
+
const { apiBase, register, schema } = normalise(target)
|
|
106
|
+
const { data } = await axios.get(prefixUrl(`${apiBase}/registers/${register}/schemas`))
|
|
107
|
+
const schemas = data?.results ?? data?.schemas ?? data ?? []
|
|
108
|
+
const list = Array.isArray(schemas) ? schemas : Object.values(schemas)
|
|
109
|
+
const found = list.find((entry) => [entry?.slug, entry?.id, entry?.uuid, entry?.title].map((v) => (v === undefined || v === null) ? '' : String(v)).includes(schema))
|
|
110
|
+
return found?.properties ?? {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The sidecar rows for one object, so a stale value can be told apart.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} target The target.
|
|
117
|
+
* @param {string} uuid The object uuid.
|
|
118
|
+
* @return {Promise<Array<object>>} Rows naming property, language and status.
|
|
119
|
+
*/
|
|
120
|
+
export async function translationRows(target, uuid) {
|
|
121
|
+
const { apiBase, schema } = normalise(target)
|
|
122
|
+
const { data } = await axios.get(
|
|
123
|
+
prefixUrl(`${apiBase}/translations/object/${encodeURIComponent(uuid)}`),
|
|
124
|
+
{ params: { schema } },
|
|
125
|
+
)
|
|
126
|
+
const rows = data?.translations ?? []
|
|
127
|
+
return Array.isArray(rows) ? rows : []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Write one property back with every language it now holds.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} target The target.
|
|
134
|
+
* @param {string} id The object id.
|
|
135
|
+
* @param {string} property The property.
|
|
136
|
+
* @param {object} languageMap Every language of it, the edit included.
|
|
137
|
+
* @return {Promise<object>} The saved object.
|
|
138
|
+
*/
|
|
139
|
+
export async function saveTranslatedProperty(target, id, property, languageMap) {
|
|
140
|
+
const { apiBase, register, schema } = normalise(target)
|
|
141
|
+
const { data } = await axios.patch(
|
|
142
|
+
prefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`),
|
|
143
|
+
{ [property]: languageMap },
|
|
144
|
+
)
|
|
145
|
+
return data?.object ?? data ?? {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* How many of a language's properties are present AND current.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately not OpenRegister's `completeness`, which counts every non-empty
|
|
152
|
+
* row and never reads its status: a stale value is a wrong value. The source
|
|
153
|
+
* language is complete by definition for every non-empty value.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} args The inputs.
|
|
156
|
+
* @param {string[]} args.properties The translatable properties.
|
|
157
|
+
* @param {object} args.values Property to language to value.
|
|
158
|
+
* @param {object} args.statuses Property to language to status.
|
|
159
|
+
* @param {string} args.language The language counted.
|
|
160
|
+
* @param {string} args.sourceLanguage The source language.
|
|
161
|
+
* @return {{translated: number, total: number}} The counts.
|
|
162
|
+
*/
|
|
163
|
+
export function completenessOf({ properties, values, statuses, language, sourceLanguage }) {
|
|
164
|
+
const total = properties.length
|
|
165
|
+
const translated = properties.filter((property) => {
|
|
166
|
+
const value = values?.[property]?.[language] ?? ''
|
|
167
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
if (language === sourceLanguage) {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
return (statuses?.[property]?.[language] ?? '') !== STATUS_OUTDATED
|
|
174
|
+
}).length
|
|
175
|
+
return { translated, total }
|
|
176
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Structural validation of a state machine: states as nodes, transitions as
|
|
6
|
+
* directed edges between them. Used by CnStateMachineEditor for itemised
|
|
7
|
+
* feedback before a host saves or publishes; a host's server remains the
|
|
8
|
+
* authority on what it accepts.
|
|
9
|
+
*
|
|
10
|
+
* @module utils/stateMachineValidation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
14
|
+
|
|
15
|
+
/** Rule codes, one per kind of issue. */
|
|
16
|
+
export const STATE_MACHINE_RULES = Object.freeze({
|
|
17
|
+
NO_FINAL_STATE: 'NO_FINAL_STATE',
|
|
18
|
+
UNREACHABLE_FINAL: 'UNREACHABLE_FINAL',
|
|
19
|
+
DANGLING_TRANSITION: 'DANGLING_TRANSITION',
|
|
20
|
+
DUPLICATE_TRANSITION: 'DUPLICATE_TRANSITION',
|
|
21
|
+
ORPHAN_STATE: 'ORPHAN_STATE',
|
|
22
|
+
CYCLE_NO_EXIT: 'CYCLE_NO_EXIT',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The record keys the validator reads, by role. A host whose records use other
|
|
27
|
+
* names passes its own (`{ final: 'isFinal', from: 'fromStatus' }`).
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_STATE_MACHINE_KEYS = Object.freeze({
|
|
30
|
+
id: 'id',
|
|
31
|
+
label: 'label',
|
|
32
|
+
final: 'final',
|
|
33
|
+
from: 'from',
|
|
34
|
+
to: 'to',
|
|
35
|
+
transitionLabel: 'label',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The default message of each rule, in the library's own domain.
|
|
40
|
+
*
|
|
41
|
+
* @return {object} Messages by rule code.
|
|
42
|
+
*/
|
|
43
|
+
function defaultMessages() {
|
|
44
|
+
return {
|
|
45
|
+
NO_FINAL_STATE: t('nextcloud-vue', 'No state is marked as final'),
|
|
46
|
+
UNREACHABLE_FINAL: t('nextcloud-vue', 'Final state "{name}" cannot be reached from any starting state'),
|
|
47
|
+
DANGLING_TRANSITION: t('nextcloud-vue', 'Transition "{label}" points at a state that no longer exists'),
|
|
48
|
+
DUPLICATE_TRANSITION: t('nextcloud-vue', 'Duplicate transition from "{from}" to "{to}"'),
|
|
49
|
+
ORPHAN_STATE: t('nextcloud-vue', 'State "{name}" has no transitions connecting it to the others'),
|
|
50
|
+
CYCLE_NO_EXIT: t('nextcloud-vue', 'Cycle with no way out to a final state: {names}'),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalise a list that may arrive as a JSON string or an array.
|
|
56
|
+
*
|
|
57
|
+
* @param {string|Array|null|undefined} raw The value.
|
|
58
|
+
* @return {Array} The array, or [] when it is neither.
|
|
59
|
+
*/
|
|
60
|
+
export function asList(raw) {
|
|
61
|
+
if (Array.isArray(raw)) {
|
|
62
|
+
return raw
|
|
63
|
+
}
|
|
64
|
+
if (typeof raw === 'string' && raw !== '') {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(raw)
|
|
67
|
+
return Array.isArray(parsed) ? parsed : []
|
|
68
|
+
} catch {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fill `{name}` placeholders.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} text The text.
|
|
79
|
+
* @param {object} vars Values by placeholder.
|
|
80
|
+
* @return {string} The filled text.
|
|
81
|
+
*/
|
|
82
|
+
function fill(text, vars) {
|
|
83
|
+
return String(text).replace(/\{(\w+)\}/g, (match, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : match))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate a state machine.
|
|
88
|
+
*
|
|
89
|
+
* Errors block (no final state, a final state no start reaches, a transition to
|
|
90
|
+
* a state that is gone); warnings inform (duplicates, isolated states, cycles
|
|
91
|
+
* that never reach a final state). An empty machine has no issues: it is
|
|
92
|
+
* unfinished, not wrong.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} graph The machine.
|
|
95
|
+
* @param {Array|string} graph.states The states.
|
|
96
|
+
* @param {Array|string} graph.transitions The transitions.
|
|
97
|
+
* @param {object} [options] Options.
|
|
98
|
+
* @param {object} [options.keys] Record keys by role, see DEFAULT_STATE_MACHINE_KEYS.
|
|
99
|
+
* @param {object} [options.messages] Message templates by rule code, overriding the defaults.
|
|
100
|
+
* @param {(text: string) => string} [options.translate] Applied to an overriding message before its placeholders are filled.
|
|
101
|
+
* @return {Array<{type: string, code: string, message: string}>} The issues.
|
|
102
|
+
*/
|
|
103
|
+
export function validateStateMachine({ states, transitions } = {}, options = {}) {
|
|
104
|
+
const keys = { ...DEFAULT_STATE_MACHINE_KEYS, ...(options.keys || {}) }
|
|
105
|
+
const translate = typeof options.translate === 'function' ? options.translate : (text) => text
|
|
106
|
+
const own = options.messages || {}
|
|
107
|
+
const defaults = defaultMessages()
|
|
108
|
+
const message = (code, vars) => {
|
|
109
|
+
const template = typeof own[code] === 'string' && own[code] !== '' ? translate(own[code]) : defaults[code]
|
|
110
|
+
return fill(template, vars)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nodes = asList(states)
|
|
114
|
+
const edges = asList(transitions)
|
|
115
|
+
const issues = []
|
|
116
|
+
if (nodes.length === 0) {
|
|
117
|
+
return issues
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const idOf = (node) => node[keys.id]
|
|
121
|
+
const nodeIds = new Set(nodes.map(idOf))
|
|
122
|
+
const nameOf = (id) => {
|
|
123
|
+
const node = nodes.find((candidate) => idOf(candidate) === id)
|
|
124
|
+
return (node && node[keys.label]) || id
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const validEdges = []
|
|
128
|
+
edges.forEach((edge) => {
|
|
129
|
+
const from = edge[keys.from]
|
|
130
|
+
const to = edge[keys.to]
|
|
131
|
+
if (!nodeIds.has(from) || !nodeIds.has(to)) {
|
|
132
|
+
issues.push({
|
|
133
|
+
type: 'error',
|
|
134
|
+
code: STATE_MACHINE_RULES.DANGLING_TRANSITION,
|
|
135
|
+
message: message('DANGLING_TRANSITION', { label: edge[keys.transitionLabel] || `${from} → ${to}` }),
|
|
136
|
+
})
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
validEdges.push(edge)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const seenPairs = new Set()
|
|
143
|
+
validEdges.forEach((edge) => {
|
|
144
|
+
const pair = `${edge[keys.from]}::${edge[keys.to]}`
|
|
145
|
+
if (seenPairs.has(pair)) {
|
|
146
|
+
issues.push({
|
|
147
|
+
type: 'warning',
|
|
148
|
+
code: STATE_MACHINE_RULES.DUPLICATE_TRANSITION,
|
|
149
|
+
message: message('DUPLICATE_TRANSITION', { from: nameOf(edge[keys.from]), to: nameOf(edge[keys.to]) }),
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
seenPairs.add(pair)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const finals = nodes.filter((node) => node[keys.final] === true)
|
|
156
|
+
if (finals.length === 0) {
|
|
157
|
+
issues.push({
|
|
158
|
+
type: 'error',
|
|
159
|
+
code: STATE_MACHINE_RULES.NO_FINAL_STATE,
|
|
160
|
+
message: message('NO_FINAL_STATE', {}),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const forward = new Map(nodes.map((node) => [idOf(node), []]))
|
|
165
|
+
const reverse = new Map(nodes.map((node) => [idOf(node), []]))
|
|
166
|
+
validEdges.forEach((edge) => {
|
|
167
|
+
forward.get(edge[keys.from]).push(edge[keys.to])
|
|
168
|
+
reverse.get(edge[keys.to]).push(edge[keys.from])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
if (nodes.length > 1) {
|
|
172
|
+
nodes.forEach((node) => {
|
|
173
|
+
if (reverse.get(idOf(node)).length === 0 && forward.get(idOf(node)).length === 0) {
|
|
174
|
+
issues.push({
|
|
175
|
+
type: 'warning',
|
|
176
|
+
code: STATE_MACHINE_RULES.ORPHAN_STATE,
|
|
177
|
+
message: message('ORPHAN_STATE', { name: node[keys.label] || idOf(node) }),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Starting states are those nothing leads into. When every state has an
|
|
184
|
+
// incoming transition there is no entry point, so nothing is reachable.
|
|
185
|
+
const reachable = new Set()
|
|
186
|
+
const queue = nodes.filter((node) => reverse.get(idOf(node)).length === 0).map(idOf)
|
|
187
|
+
while (queue.length > 0) {
|
|
188
|
+
const current = queue.shift()
|
|
189
|
+
if (!reachable.has(current)) {
|
|
190
|
+
reachable.add(current)
|
|
191
|
+
forward.get(current).forEach((next) => queue.push(next))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finals.forEach((node) => {
|
|
195
|
+
if (!reachable.has(idOf(node))) {
|
|
196
|
+
issues.push({
|
|
197
|
+
type: 'error',
|
|
198
|
+
code: STATE_MACHINE_RULES.UNREACHABLE_FINAL,
|
|
199
|
+
message: message('UNREACHABLE_FINAL', { name: node[keys.label] || idOf(node) }),
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const reachesFinal = new Set(finals.map(idOf))
|
|
205
|
+
const back = finals.map(idOf)
|
|
206
|
+
while (back.length > 0) {
|
|
207
|
+
reverse.get(back.shift()).forEach((previous) => {
|
|
208
|
+
if (!reachesFinal.has(previous)) {
|
|
209
|
+
reachesFinal.add(previous)
|
|
210
|
+
back.push(previous)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const WHITE = 0
|
|
216
|
+
const GREY = 1
|
|
217
|
+
const BLACK = 2
|
|
218
|
+
const colour = new Map(nodes.map((node) => [idOf(node), WHITE]))
|
|
219
|
+
const stack = []
|
|
220
|
+
const reported = new Set()
|
|
221
|
+
const visit = (id) => {
|
|
222
|
+
colour.set(id, GREY)
|
|
223
|
+
stack.push(id)
|
|
224
|
+
forward.get(id).forEach((next) => {
|
|
225
|
+
if (colour.get(next) === GREY) {
|
|
226
|
+
const cycle = stack.slice(stack.indexOf(next))
|
|
227
|
+
const cycleKey = [...cycle].sort().join(',')
|
|
228
|
+
if (!reported.has(cycleKey) && !cycle.some((member) => reachesFinal.has(member))) {
|
|
229
|
+
reported.add(cycleKey)
|
|
230
|
+
issues.push({
|
|
231
|
+
type: 'warning',
|
|
232
|
+
code: STATE_MACHINE_RULES.CYCLE_NO_EXIT,
|
|
233
|
+
message: message('CYCLE_NO_EXIT', { names: cycle.map(nameOf).join(' → ') }),
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
} else if (colour.get(next) === WHITE) {
|
|
237
|
+
visit(next)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
stack.pop()
|
|
241
|
+
colour.set(id, BLACK)
|
|
242
|
+
}
|
|
243
|
+
nodes.forEach((node) => {
|
|
244
|
+
if (colour.get(idOf(node)) === WHITE) {
|
|
245
|
+
visit(idOf(node))
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return issues
|
|
250
|
+
}
|
|
@@ -47,10 +47,12 @@ import { canonicalWidgetType } from './widgetTypeAliases.js'
|
|
|
47
47
|
* header (link, note) come from the host, and it has no Add.
|
|
48
48
|
* `parties` draws a record's parties by role and `map` a bare Leaflet map:
|
|
49
49
|
* on a card surface both need the titled wrapper, and neither has an Add.
|
|
50
|
+
* `object-translations` is a bare per-language editor that takes the titled
|
|
51
|
+
* wrapper and has no Add.
|
|
50
52
|
*
|
|
51
53
|
* @type {string[]}
|
|
52
54
|
*/
|
|
53
|
-
export const CONTENT_ONLY_TYPES = Object.freeze(['object-list', 'table', 'files', 'flow-runs', 'tasks', 'events', 'facts', 'linked', 'field-list', 'record-list', 'parties', 'map', 'settings-form', 'switch-rows', 'radio-cards', 'chips-field', 'toggle-table', 'choice-picker', 'checklist'])
|
|
55
|
+
export const CONTENT_ONLY_TYPES = Object.freeze(['object-list', 'table', 'files', 'flow-runs', 'tasks', 'events', 'facts', 'linked', 'field-list', 'record-list', 'parties', 'map', 'settings-form', 'switch-rows', 'radio-cards', 'chips-field', 'toggle-table', 'choice-picker', 'checklist', 'object-translations'])
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* The stored content/config blob for a widget definition.
|