@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,180 @@
|
|
|
1
|
+
import axios__default from '@nextcloud/axios';
|
|
2
|
+
import { prefixUrl } from './headers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* The OpenRegister translation reads and the one write behind
|
|
9
|
+
* CnObjectTranslationsWidget. The engine is OpenRegister's; nothing here
|
|
10
|
+
* translates or stores anything of its own.
|
|
11
|
+
*
|
|
12
|
+
* GET {apiBase}/registers/{register} languages, source first
|
|
13
|
+
* GET {apiBase}/registers/{register}/schemas property titles
|
|
14
|
+
* GET {apiBase}/objects/{register}/{schema}/{id}?_translations=all&_translationMeta=true
|
|
15
|
+
* GET {apiBase}/translations/object/{uuid}?schema={schema} per-value workflow status
|
|
16
|
+
* PATCH {apiBase}/objects/{register}/{schema}/{id} { property: languageMap }
|
|
17
|
+
*
|
|
18
|
+
* The WHOLE language map is written and no `X-Translation-Target-Language` is
|
|
19
|
+
* sent: OpenRegister refuses a language-keyed body that arrives with that
|
|
20
|
+
* header, and keeps one that arrives without it.
|
|
21
|
+
*
|
|
22
|
+
* The translatable properties come from `_meta.languageMeta`, which OpenRegister
|
|
23
|
+
* builds from the schema it imported, so a property it never read as
|
|
24
|
+
* translatable is not offered.
|
|
25
|
+
*
|
|
26
|
+
* A failed read throws: an outage and an object with nothing translated look
|
|
27
|
+
* the same from the browser, and only one of them is somebody's problem.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The OpenRegister API root a target defaults to. */
|
|
31
|
+
const DEFAULT_API_BASE = '/apps/openregister/api';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A translation row is stale when the source value moved under it; OpenRegister
|
|
35
|
+
* flips a derived row to `outdated`. A stale value does not count as translated.
|
|
36
|
+
*
|
|
37
|
+
* @type {string}
|
|
38
|
+
*/
|
|
39
|
+
const STATUS_OUTDATED = 'outdated';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The slug of a schema given as a slug or as the schema object.
|
|
43
|
+
*
|
|
44
|
+
* @param {string|object} schema The schema.
|
|
45
|
+
* @return {string} The slug (or id).
|
|
46
|
+
*/
|
|
47
|
+
function schemaSlugOf(schema) {
|
|
48
|
+
if (schema && typeof schema === 'object') {
|
|
49
|
+
return String(schema.slug || schema.id || schema.uuid || '')
|
|
50
|
+
}
|
|
51
|
+
return String(schema || '')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalise a target to `{register, schema, apiBase}`.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} target `{register, schema, apiBase?}`.
|
|
58
|
+
* @return {{register: string, schema: string, apiBase: string}} The target.
|
|
59
|
+
*/
|
|
60
|
+
function normalise(target = {}) {
|
|
61
|
+
return {
|
|
62
|
+
register: String(target.register || ''),
|
|
63
|
+
schema: schemaSlugOf(target.schema),
|
|
64
|
+
apiBase: String(target.apiBase || DEFAULT_API_BASE).replace(/\/$/, ''),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The languages the register serves, source language first.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} target The target.
|
|
72
|
+
* @return {Promise<string[]>} The language codes.
|
|
73
|
+
*/
|
|
74
|
+
async function registerLanguages(target) {
|
|
75
|
+
const { apiBase, register } = normalise(target);
|
|
76
|
+
const { data } = await axios__default.get(prefixUrl(`${apiBase}/registers/${register}`));
|
|
77
|
+
const languages = data?.languages ?? data?.register?.languages ?? [];
|
|
78
|
+
return Array.isArray(languages) ? languages.filter((code) => typeof code === 'string' && code !== '') : []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One object with every language of every value, and its label envelope.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} target The target.
|
|
85
|
+
* @param {string} id The object id.
|
|
86
|
+
* @return {Promise<{object: object, languageMeta: object}>} The values and the envelope.
|
|
87
|
+
*/
|
|
88
|
+
async function objectWithTranslations(target, id) {
|
|
89
|
+
const { apiBase, register, schema } = normalise(target);
|
|
90
|
+
const { data } = await axios__default.get(
|
|
91
|
+
prefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`),
|
|
92
|
+
{ params: { _translations: 'all', _translationMeta: 'true' } },
|
|
93
|
+
);
|
|
94
|
+
const object = data?.object ?? data ?? {};
|
|
95
|
+
return { object, languageMeta: object?._meta?.languageMeta ?? {} }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The schema's property definitions, for their titles (`languageMeta` carries
|
|
100
|
+
* none).
|
|
101
|
+
*
|
|
102
|
+
* @param {object} target The target.
|
|
103
|
+
* @return {Promise<object>} Property name to its definition.
|
|
104
|
+
*/
|
|
105
|
+
async function schemaProperties(target) {
|
|
106
|
+
const { apiBase, register, schema } = normalise(target);
|
|
107
|
+
const { data } = await axios__default.get(prefixUrl(`${apiBase}/registers/${register}/schemas`));
|
|
108
|
+
const schemas = data?.results ?? data?.schemas ?? data ?? [];
|
|
109
|
+
const list = Array.isArray(schemas) ? schemas : Object.values(schemas);
|
|
110
|
+
const found = list.find((entry) => [entry?.slug, entry?.id, entry?.uuid, entry?.title].map((v) => (v === undefined || v === null) ? '' : String(v)).includes(schema));
|
|
111
|
+
return found?.properties ?? {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The sidecar rows for one object, so a stale value can be told apart.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} target The target.
|
|
118
|
+
* @param {string} uuid The object uuid.
|
|
119
|
+
* @return {Promise<Array<object>>} Rows naming property, language and status.
|
|
120
|
+
*/
|
|
121
|
+
async function translationRows(target, uuid) {
|
|
122
|
+
const { apiBase, schema } = normalise(target);
|
|
123
|
+
const { data } = await axios__default.get(
|
|
124
|
+
prefixUrl(`${apiBase}/translations/object/${encodeURIComponent(uuid)}`),
|
|
125
|
+
{ params: { schema } },
|
|
126
|
+
);
|
|
127
|
+
const rows = data?.translations ?? [];
|
|
128
|
+
return Array.isArray(rows) ? rows : []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write one property back with every language it now holds.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} target The target.
|
|
135
|
+
* @param {string} id The object id.
|
|
136
|
+
* @param {string} property The property.
|
|
137
|
+
* @param {object} languageMap Every language of it, the edit included.
|
|
138
|
+
* @return {Promise<object>} The saved object.
|
|
139
|
+
*/
|
|
140
|
+
async function saveTranslatedProperty(target, id, property, languageMap) {
|
|
141
|
+
const { apiBase, register, schema } = normalise(target);
|
|
142
|
+
const { data } = await axios__default.patch(
|
|
143
|
+
prefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`),
|
|
144
|
+
{ [property]: languageMap },
|
|
145
|
+
);
|
|
146
|
+
return data?.object ?? data ?? {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* How many of a language's properties are present AND current.
|
|
151
|
+
*
|
|
152
|
+
* Deliberately not OpenRegister's `completeness`, which counts every non-empty
|
|
153
|
+
* row and never reads its status: a stale value is a wrong value. The source
|
|
154
|
+
* language is complete by definition for every non-empty value.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} args The inputs.
|
|
157
|
+
* @param {string[]} args.properties The translatable properties.
|
|
158
|
+
* @param {object} args.values Property to language to value.
|
|
159
|
+
* @param {object} args.statuses Property to language to status.
|
|
160
|
+
* @param {string} args.language The language counted.
|
|
161
|
+
* @param {string} args.sourceLanguage The source language.
|
|
162
|
+
* @return {{translated: number, total: number}} The counts.
|
|
163
|
+
*/
|
|
164
|
+
function completenessOf({ properties, values, statuses, language, sourceLanguage }) {
|
|
165
|
+
const total = properties.length;
|
|
166
|
+
const translated = properties.filter((property) => {
|
|
167
|
+
const value = values?.[property]?.[language] ?? '';
|
|
168
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
if (language === sourceLanguage) {
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
return (statuses?.[property]?.[language] ?? '') !== STATUS_OUTDATED
|
|
175
|
+
}).length;
|
|
176
|
+
return { translated, total }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { DEFAULT_API_BASE, STATUS_OUTDATED, completenessOf, objectWithTranslations, registerLanguages, saveTranslatedProperty, schemaProperties, schemaSlugOf, translationRows };
|
|
180
|
+
//# sourceMappingURL=objectTranslations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"objectTranslations.js","sources":["../../../src/utils/objectTranslations.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The OpenRegister translation reads and the one write behind\n * CnObjectTranslationsWidget. The engine is OpenRegister's; nothing here\n * translates or stores anything of its own.\n *\n * GET {apiBase}/registers/{register} languages, source first\n * GET {apiBase}/registers/{register}/schemas property titles\n * GET {apiBase}/objects/{register}/{schema}/{id}?_translations=all&_translationMeta=true\n * GET {apiBase}/translations/object/{uuid}?schema={schema} per-value workflow status\n * PATCH {apiBase}/objects/{register}/{schema}/{id} { property: languageMap }\n *\n * The WHOLE language map is written and no `X-Translation-Target-Language` is\n * sent: OpenRegister refuses a language-keyed body that arrives with that\n * header, and keeps one that arrives without it.\n *\n * The translatable properties come from `_meta.languageMeta`, which OpenRegister\n * builds from the schema it imported, so a property it never read as\n * translatable is not offered.\n *\n * A failed read throws: an outage and an object with nothing translated look\n * the same from the browser, and only one of them is somebody's problem.\n */\nimport axios from '@nextcloud/axios'\nimport { prefixUrl } from './headers.js'\n\n/** The OpenRegister API root a target defaults to. */\nexport const DEFAULT_API_BASE = '/apps/openregister/api'\n\n/**\n * A translation row is stale when the source value moved under it; OpenRegister\n * flips a derived row to `outdated`. A stale value does not count as translated.\n *\n * @type {string}\n */\nexport const STATUS_OUTDATED = 'outdated'\n\n/**\n * The slug of a schema given as a slug or as the schema object.\n *\n * @param {string|object} schema The schema.\n * @return {string} The slug (or id).\n */\nexport function schemaSlugOf(schema) {\n\tif (schema && typeof schema === 'object') {\n\t\treturn String(schema.slug || schema.id || schema.uuid || '')\n\t}\n\treturn String(schema || '')\n}\n\n/**\n * Normalise a target to `{register, schema, apiBase}`.\n *\n * @param {object} target `{register, schema, apiBase?}`.\n * @return {{register: string, schema: string, apiBase: string}} The target.\n */\nfunction normalise(target = {}) {\n\treturn {\n\t\tregister: String(target.register || ''),\n\t\tschema: schemaSlugOf(target.schema),\n\t\tapiBase: String(target.apiBase || DEFAULT_API_BASE).replace(/\\/$/, ''),\n\t}\n}\n\n/**\n * The languages the register serves, source language first.\n *\n * @param {object} target The target.\n * @return {Promise<string[]>} The language codes.\n */\nexport async function registerLanguages(target) {\n\tconst { apiBase, register } = normalise(target)\n\tconst { data } = await axios.get(prefixUrl(`${apiBase}/registers/${register}`))\n\tconst languages = data?.languages ?? data?.register?.languages ?? []\n\treturn Array.isArray(languages) ? languages.filter((code) => typeof code === 'string' && code !== '') : []\n}\n\n/**\n * One object with every language of every value, and its label envelope.\n *\n * @param {object} target The target.\n * @param {string} id The object id.\n * @return {Promise<{object: object, languageMeta: object}>} The values and the envelope.\n */\nexport async function objectWithTranslations(target, id) {\n\tconst { apiBase, register, schema } = normalise(target)\n\tconst { data } = await axios.get(\n\t\tprefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`),\n\t\t{ params: { _translations: 'all', _translationMeta: 'true' } },\n\t)\n\tconst object = data?.object ?? data ?? {}\n\treturn { object, languageMeta: object?._meta?.languageMeta ?? {} }\n}\n\n/**\n * The schema's property definitions, for their titles (`languageMeta` carries\n * none).\n *\n * @param {object} target The target.\n * @return {Promise<object>} Property name to its definition.\n */\nexport async function schemaProperties(target) {\n\tconst { apiBase, register, schema } = normalise(target)\n\tconst { data } = await axios.get(prefixUrl(`${apiBase}/registers/${register}/schemas`))\n\tconst schemas = data?.results ?? data?.schemas ?? data ?? []\n\tconst list = Array.isArray(schemas) ? schemas : Object.values(schemas)\n\tconst found = list.find((entry) => [entry?.slug, entry?.id, entry?.uuid, entry?.title].map((v) => (v === undefined || v === null) ? '' : String(v)).includes(schema))\n\treturn found?.properties ?? {}\n}\n\n/**\n * The sidecar rows for one object, so a stale value can be told apart.\n *\n * @param {object} target The target.\n * @param {string} uuid The object uuid.\n * @return {Promise<Array<object>>} Rows naming property, language and status.\n */\nexport async function translationRows(target, uuid) {\n\tconst { apiBase, schema } = normalise(target)\n\tconst { data } = await axios.get(\n\t\tprefixUrl(`${apiBase}/translations/object/${encodeURIComponent(uuid)}`),\n\t\t{ params: { schema } },\n\t)\n\tconst rows = data?.translations ?? []\n\treturn Array.isArray(rows) ? rows : []\n}\n\n/**\n * Write one property back with every language it now holds.\n *\n * @param {object} target The target.\n * @param {string} id The object id.\n * @param {string} property The property.\n * @param {object} languageMap Every language of it, the edit included.\n * @return {Promise<object>} The saved object.\n */\nexport async function saveTranslatedProperty(target, id, property, languageMap) {\n\tconst { apiBase, register, schema } = normalise(target)\n\tconst { data } = await axios.patch(\n\t\tprefixUrl(`${apiBase}/objects/${register}/${schema}/${encodeURIComponent(id)}`),\n\t\t{ [property]: languageMap },\n\t)\n\treturn data?.object ?? data ?? {}\n}\n\n/**\n * How many of a language's properties are present AND current.\n *\n * Deliberately not OpenRegister's `completeness`, which counts every non-empty\n * row and never reads its status: a stale value is a wrong value. The source\n * language is complete by definition for every non-empty value.\n *\n * @param {object} args The inputs.\n * @param {string[]} args.properties The translatable properties.\n * @param {object} args.values Property to language to value.\n * @param {object} args.statuses Property to language to status.\n * @param {string} args.language The language counted.\n * @param {string} args.sourceLanguage The source language.\n * @return {{translated: number, total: number}} The counts.\n */\nexport function completenessOf({ properties, values, statuses, language, sourceLanguage }) {\n\tconst total = properties.length\n\tconst translated = properties.filter((property) => {\n\t\tconst value = values?.[property]?.[language] ?? ''\n\t\tif (typeof value !== 'string' || value.trim() === '') {\n\t\t\treturn false\n\t\t}\n\t\tif (language === sourceLanguage) {\n\t\t\treturn true\n\t\t}\n\t\treturn (statuses?.[property]?.[language] ?? '') !== STATUS_OUTDATED\n\t}).length\n\treturn { translated, total }\n}\n"],"names":["axios"],"mappings":"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"}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
5
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
*
|
|
7
|
+
* Structural validation of a state machine: states as nodes, transitions as
|
|
8
|
+
* directed edges between them. Used by CnStateMachineEditor for itemised
|
|
9
|
+
* feedback before a host saves or publishes; a host's server remains the
|
|
10
|
+
* authority on what it accepts.
|
|
11
|
+
*
|
|
12
|
+
* @module utils/stateMachineValidation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/** Rule codes, one per kind of issue. */
|
|
17
|
+
const STATE_MACHINE_RULES = Object.freeze({
|
|
18
|
+
NO_FINAL_STATE: 'NO_FINAL_STATE',
|
|
19
|
+
UNREACHABLE_FINAL: 'UNREACHABLE_FINAL',
|
|
20
|
+
DANGLING_TRANSITION: 'DANGLING_TRANSITION',
|
|
21
|
+
DUPLICATE_TRANSITION: 'DUPLICATE_TRANSITION',
|
|
22
|
+
ORPHAN_STATE: 'ORPHAN_STATE',
|
|
23
|
+
CYCLE_NO_EXIT: 'CYCLE_NO_EXIT',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The record keys the validator reads, by role. A host whose records use other
|
|
28
|
+
* names passes its own (`{ final: 'isFinal', from: 'fromStatus' }`).
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_STATE_MACHINE_KEYS = Object.freeze({
|
|
31
|
+
id: 'id',
|
|
32
|
+
label: 'label',
|
|
33
|
+
final: 'final',
|
|
34
|
+
from: 'from',
|
|
35
|
+
to: 'to',
|
|
36
|
+
transitionLabel: 'label',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The default message of each rule, in the library's own domain.
|
|
41
|
+
*
|
|
42
|
+
* @return {object} Messages by rule code.
|
|
43
|
+
*/
|
|
44
|
+
function defaultMessages() {
|
|
45
|
+
return {
|
|
46
|
+
NO_FINAL_STATE: translate('nextcloud-vue', 'No state is marked as final'),
|
|
47
|
+
UNREACHABLE_FINAL: translate('nextcloud-vue', 'Final state "{name}" cannot be reached from any starting state'),
|
|
48
|
+
DANGLING_TRANSITION: translate('nextcloud-vue', 'Transition "{label}" points at a state that no longer exists'),
|
|
49
|
+
DUPLICATE_TRANSITION: translate('nextcloud-vue', 'Duplicate transition from "{from}" to "{to}"'),
|
|
50
|
+
ORPHAN_STATE: translate('nextcloud-vue', 'State "{name}" has no transitions connecting it to the others'),
|
|
51
|
+
CYCLE_NO_EXIT: translate('nextcloud-vue', 'Cycle with no way out to a final state: {names}'),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalise a list that may arrive as a JSON string or an array.
|
|
57
|
+
*
|
|
58
|
+
* @param {string|Array|null|undefined} raw The value.
|
|
59
|
+
* @return {Array} The array, or [] when it is neither.
|
|
60
|
+
*/
|
|
61
|
+
function asList(raw) {
|
|
62
|
+
if (Array.isArray(raw)) {
|
|
63
|
+
return raw
|
|
64
|
+
}
|
|
65
|
+
if (typeof raw === 'string' && raw !== '') {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
return Array.isArray(parsed) ? parsed : []
|
|
69
|
+
} catch {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fill `{name}` placeholders.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} text The text.
|
|
80
|
+
* @param {object} vars Values by placeholder.
|
|
81
|
+
* @return {string} The filled text.
|
|
82
|
+
*/
|
|
83
|
+
function fill(text, vars) {
|
|
84
|
+
return String(text).replace(/\{(\w+)\}/g, (match, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : match))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate a state machine.
|
|
89
|
+
*
|
|
90
|
+
* Errors block (no final state, a final state no start reaches, a transition to
|
|
91
|
+
* a state that is gone); warnings inform (duplicates, isolated states, cycles
|
|
92
|
+
* that never reach a final state). An empty machine has no issues: it is
|
|
93
|
+
* unfinished, not wrong.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} graph The machine.
|
|
96
|
+
* @param {Array|string} graph.states The states.
|
|
97
|
+
* @param {Array|string} graph.transitions The transitions.
|
|
98
|
+
* @param {object} [options] Options.
|
|
99
|
+
* @param {object} [options.keys] Record keys by role, see DEFAULT_STATE_MACHINE_KEYS.
|
|
100
|
+
* @param {object} [options.messages] Message templates by rule code, overriding the defaults.
|
|
101
|
+
* @param {(text: string) => string} [options.translate] Applied to an overriding message before its placeholders are filled.
|
|
102
|
+
* @return {Array<{type: string, code: string, message: string}>} The issues.
|
|
103
|
+
*/
|
|
104
|
+
function validateStateMachine({ states, transitions } = {}, options = {}) {
|
|
105
|
+
const keys = { ...DEFAULT_STATE_MACHINE_KEYS, ...(options.keys || {}) };
|
|
106
|
+
const translate = typeof options.translate === 'function' ? options.translate : (text) => text;
|
|
107
|
+
const own = options.messages || {};
|
|
108
|
+
const defaults = defaultMessages();
|
|
109
|
+
const message = (code, vars) => {
|
|
110
|
+
const template = typeof own[code] === 'string' && own[code] !== '' ? translate(own[code]) : defaults[code];
|
|
111
|
+
return fill(template, vars)
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const nodes = asList(states);
|
|
115
|
+
const edges = asList(transitions);
|
|
116
|
+
const issues = [];
|
|
117
|
+
if (nodes.length === 0) {
|
|
118
|
+
return issues
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const idOf = (node) => node[keys.id];
|
|
122
|
+
const nodeIds = new Set(nodes.map(idOf));
|
|
123
|
+
const nameOf = (id) => {
|
|
124
|
+
const node = nodes.find((candidate) => idOf(candidate) === id);
|
|
125
|
+
return (node && node[keys.label]) || id
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const validEdges = [];
|
|
129
|
+
edges.forEach((edge) => {
|
|
130
|
+
const from = edge[keys.from];
|
|
131
|
+
const to = edge[keys.to];
|
|
132
|
+
if (!nodeIds.has(from) || !nodeIds.has(to)) {
|
|
133
|
+
issues.push({
|
|
134
|
+
type: 'error',
|
|
135
|
+
code: STATE_MACHINE_RULES.DANGLING_TRANSITION,
|
|
136
|
+
message: message('DANGLING_TRANSITION', { label: edge[keys.transitionLabel] || `${from} → ${to}` }),
|
|
137
|
+
});
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
validEdges.push(edge);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const seenPairs = new Set();
|
|
144
|
+
validEdges.forEach((edge) => {
|
|
145
|
+
const pair = `${edge[keys.from]}::${edge[keys.to]}`;
|
|
146
|
+
if (seenPairs.has(pair)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
type: 'warning',
|
|
149
|
+
code: STATE_MACHINE_RULES.DUPLICATE_TRANSITION,
|
|
150
|
+
message: message('DUPLICATE_TRANSITION', { from: nameOf(edge[keys.from]), to: nameOf(edge[keys.to]) }),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
seenPairs.add(pair);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const finals = nodes.filter((node) => node[keys.final] === true);
|
|
157
|
+
if (finals.length === 0) {
|
|
158
|
+
issues.push({
|
|
159
|
+
type: 'error',
|
|
160
|
+
code: STATE_MACHINE_RULES.NO_FINAL_STATE,
|
|
161
|
+
message: message('NO_FINAL_STATE', {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const forward = new Map(nodes.map((node) => [idOf(node), []]));
|
|
166
|
+
const reverse = new Map(nodes.map((node) => [idOf(node), []]));
|
|
167
|
+
validEdges.forEach((edge) => {
|
|
168
|
+
forward.get(edge[keys.from]).push(edge[keys.to]);
|
|
169
|
+
reverse.get(edge[keys.to]).push(edge[keys.from]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (nodes.length > 1) {
|
|
173
|
+
nodes.forEach((node) => {
|
|
174
|
+
if (reverse.get(idOf(node)).length === 0 && forward.get(idOf(node)).length === 0) {
|
|
175
|
+
issues.push({
|
|
176
|
+
type: 'warning',
|
|
177
|
+
code: STATE_MACHINE_RULES.ORPHAN_STATE,
|
|
178
|
+
message: message('ORPHAN_STATE', { name: node[keys.label] || idOf(node) }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Starting states are those nothing leads into. When every state has an
|
|
185
|
+
// incoming transition there is no entry point, so nothing is reachable.
|
|
186
|
+
const reachable = new Set();
|
|
187
|
+
const queue = nodes.filter((node) => reverse.get(idOf(node)).length === 0).map(idOf);
|
|
188
|
+
while (queue.length > 0) {
|
|
189
|
+
const current = queue.shift();
|
|
190
|
+
if (!reachable.has(current)) {
|
|
191
|
+
reachable.add(current);
|
|
192
|
+
forward.get(current).forEach((next) => queue.push(next));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
finals.forEach((node) => {
|
|
196
|
+
if (!reachable.has(idOf(node))) {
|
|
197
|
+
issues.push({
|
|
198
|
+
type: 'error',
|
|
199
|
+
code: STATE_MACHINE_RULES.UNREACHABLE_FINAL,
|
|
200
|
+
message: message('UNREACHABLE_FINAL', { name: node[keys.label] || idOf(node) }),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const reachesFinal = new Set(finals.map(idOf));
|
|
206
|
+
const back = finals.map(idOf);
|
|
207
|
+
while (back.length > 0) {
|
|
208
|
+
reverse.get(back.shift()).forEach((previous) => {
|
|
209
|
+
if (!reachesFinal.has(previous)) {
|
|
210
|
+
reachesFinal.add(previous);
|
|
211
|
+
back.push(previous);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const WHITE = 0;
|
|
217
|
+
const GREY = 1;
|
|
218
|
+
const BLACK = 2;
|
|
219
|
+
const colour = new Map(nodes.map((node) => [idOf(node), WHITE]));
|
|
220
|
+
const stack = [];
|
|
221
|
+
const reported = new Set();
|
|
222
|
+
const visit = (id) => {
|
|
223
|
+
colour.set(id, GREY);
|
|
224
|
+
stack.push(id);
|
|
225
|
+
forward.get(id).forEach((next) => {
|
|
226
|
+
if (colour.get(next) === GREY) {
|
|
227
|
+
const cycle = stack.slice(stack.indexOf(next));
|
|
228
|
+
const cycleKey = [...cycle].sort().join(',');
|
|
229
|
+
if (!reported.has(cycleKey) && !cycle.some((member) => reachesFinal.has(member))) {
|
|
230
|
+
reported.add(cycleKey);
|
|
231
|
+
issues.push({
|
|
232
|
+
type: 'warning',
|
|
233
|
+
code: STATE_MACHINE_RULES.CYCLE_NO_EXIT,
|
|
234
|
+
message: message('CYCLE_NO_EXIT', { names: cycle.map(nameOf).join(' → ') }),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} else if (colour.get(next) === WHITE) {
|
|
238
|
+
visit(next);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
stack.pop();
|
|
242
|
+
colour.set(id, BLACK);
|
|
243
|
+
};
|
|
244
|
+
nodes.forEach((node) => {
|
|
245
|
+
if (colour.get(idOf(node)) === WHITE) {
|
|
246
|
+
visit(idOf(node));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return issues
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { DEFAULT_STATE_MACHINE_KEYS, STATE_MACHINE_RULES, asList, validateStateMachine };
|
|
254
|
+
//# sourceMappingURL=stateMachineValidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateMachineValidation.js","sources":["../../../src/utils/stateMachineValidation.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Structural validation of a state machine: states as nodes, transitions as\n * directed edges between them. Used by CnStateMachineEditor for itemised\n * feedback before a host saves or publishes; a host's server remains the\n * authority on what it accepts.\n *\n * @module utils/stateMachineValidation\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** Rule codes, one per kind of issue. */\nexport const STATE_MACHINE_RULES = Object.freeze({\n\tNO_FINAL_STATE: 'NO_FINAL_STATE',\n\tUNREACHABLE_FINAL: 'UNREACHABLE_FINAL',\n\tDANGLING_TRANSITION: 'DANGLING_TRANSITION',\n\tDUPLICATE_TRANSITION: 'DUPLICATE_TRANSITION',\n\tORPHAN_STATE: 'ORPHAN_STATE',\n\tCYCLE_NO_EXIT: 'CYCLE_NO_EXIT',\n})\n\n/**\n * The record keys the validator reads, by role. A host whose records use other\n * names passes its own (`{ final: 'isFinal', from: 'fromStatus' }`).\n */\nexport const DEFAULT_STATE_MACHINE_KEYS = Object.freeze({\n\tid: 'id',\n\tlabel: 'label',\n\tfinal: 'final',\n\tfrom: 'from',\n\tto: 'to',\n\ttransitionLabel: 'label',\n})\n\n/**\n * The default message of each rule, in the library's own domain.\n *\n * @return {object} Messages by rule code.\n */\nfunction defaultMessages() {\n\treturn {\n\t\tNO_FINAL_STATE: t('nextcloud-vue', 'No state is marked as final'),\n\t\tUNREACHABLE_FINAL: t('nextcloud-vue', 'Final state \"{name}\" cannot be reached from any starting state'),\n\t\tDANGLING_TRANSITION: t('nextcloud-vue', 'Transition \"{label}\" points at a state that no longer exists'),\n\t\tDUPLICATE_TRANSITION: t('nextcloud-vue', 'Duplicate transition from \"{from}\" to \"{to}\"'),\n\t\tORPHAN_STATE: t('nextcloud-vue', 'State \"{name}\" has no transitions connecting it to the others'),\n\t\tCYCLE_NO_EXIT: t('nextcloud-vue', 'Cycle with no way out to a final state: {names}'),\n\t}\n}\n\n/**\n * Normalise a list that may arrive as a JSON string or an array.\n *\n * @param {string|Array|null|undefined} raw The value.\n * @return {Array} The array, or [] when it is neither.\n */\nexport function asList(raw) {\n\tif (Array.isArray(raw)) {\n\t\treturn raw\n\t}\n\tif (typeof raw === 'string' && raw !== '') {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw)\n\t\t\treturn Array.isArray(parsed) ? parsed : []\n\t\t} catch {\n\t\t\treturn []\n\t\t}\n\t}\n\treturn []\n}\n\n/**\n * Fill `{name}` placeholders.\n *\n * @param {string} text The text.\n * @param {object} vars Values by placeholder.\n * @return {string} The filled text.\n */\nfunction fill(text, vars) {\n\treturn String(text).replace(/\\{(\\w+)\\}/g, (match, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : match))\n}\n\n/**\n * Validate a state machine.\n *\n * Errors block (no final state, a final state no start reaches, a transition to\n * a state that is gone); warnings inform (duplicates, isolated states, cycles\n * that never reach a final state). An empty machine has no issues: it is\n * unfinished, not wrong.\n *\n * @param {object} graph The machine.\n * @param {Array|string} graph.states The states.\n * @param {Array|string} graph.transitions The transitions.\n * @param {object} [options] Options.\n * @param {object} [options.keys] Record keys by role, see DEFAULT_STATE_MACHINE_KEYS.\n * @param {object} [options.messages] Message templates by rule code, overriding the defaults.\n * @param {(text: string) => string} [options.translate] Applied to an overriding message before its placeholders are filled.\n * @return {Array<{type: string, code: string, message: string}>} The issues.\n */\nexport function validateStateMachine({ states, transitions } = {}, options = {}) {\n\tconst keys = { ...DEFAULT_STATE_MACHINE_KEYS, ...(options.keys || {}) }\n\tconst translate = typeof options.translate === 'function' ? options.translate : (text) => text\n\tconst own = options.messages || {}\n\tconst defaults = defaultMessages()\n\tconst message = (code, vars) => {\n\t\tconst template = typeof own[code] === 'string' && own[code] !== '' ? translate(own[code]) : defaults[code]\n\t\treturn fill(template, vars)\n\t}\n\n\tconst nodes = asList(states)\n\tconst edges = asList(transitions)\n\tconst issues = []\n\tif (nodes.length === 0) {\n\t\treturn issues\n\t}\n\n\tconst idOf = (node) => node[keys.id]\n\tconst nodeIds = new Set(nodes.map(idOf))\n\tconst nameOf = (id) => {\n\t\tconst node = nodes.find((candidate) => idOf(candidate) === id)\n\t\treturn (node && node[keys.label]) || id\n\t}\n\n\tconst validEdges = []\n\tedges.forEach((edge) => {\n\t\tconst from = edge[keys.from]\n\t\tconst to = edge[keys.to]\n\t\tif (!nodeIds.has(from) || !nodeIds.has(to)) {\n\t\t\tissues.push({\n\t\t\t\ttype: 'error',\n\t\t\t\tcode: STATE_MACHINE_RULES.DANGLING_TRANSITION,\n\t\t\t\tmessage: message('DANGLING_TRANSITION', { label: edge[keys.transitionLabel] || `${from} → ${to}` }),\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tvalidEdges.push(edge)\n\t})\n\n\tconst seenPairs = new Set()\n\tvalidEdges.forEach((edge) => {\n\t\tconst pair = `${edge[keys.from]}::${edge[keys.to]}`\n\t\tif (seenPairs.has(pair)) {\n\t\t\tissues.push({\n\t\t\t\ttype: 'warning',\n\t\t\t\tcode: STATE_MACHINE_RULES.DUPLICATE_TRANSITION,\n\t\t\t\tmessage: message('DUPLICATE_TRANSITION', { from: nameOf(edge[keys.from]), to: nameOf(edge[keys.to]) }),\n\t\t\t})\n\t\t}\n\t\tseenPairs.add(pair)\n\t})\n\n\tconst finals = nodes.filter((node) => node[keys.final] === true)\n\tif (finals.length === 0) {\n\t\tissues.push({\n\t\t\ttype: 'error',\n\t\t\tcode: STATE_MACHINE_RULES.NO_FINAL_STATE,\n\t\t\tmessage: message('NO_FINAL_STATE', {}),\n\t\t})\n\t}\n\n\tconst forward = new Map(nodes.map((node) => [idOf(node), []]))\n\tconst reverse = new Map(nodes.map((node) => [idOf(node), []]))\n\tvalidEdges.forEach((edge) => {\n\t\tforward.get(edge[keys.from]).push(edge[keys.to])\n\t\treverse.get(edge[keys.to]).push(edge[keys.from])\n\t})\n\n\tif (nodes.length > 1) {\n\t\tnodes.forEach((node) => {\n\t\t\tif (reverse.get(idOf(node)).length === 0 && forward.get(idOf(node)).length === 0) {\n\t\t\t\tissues.push({\n\t\t\t\t\ttype: 'warning',\n\t\t\t\t\tcode: STATE_MACHINE_RULES.ORPHAN_STATE,\n\t\t\t\t\tmessage: message('ORPHAN_STATE', { name: node[keys.label] || idOf(node) }),\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n\n\t// Starting states are those nothing leads into. When every state has an\n\t// incoming transition there is no entry point, so nothing is reachable.\n\tconst reachable = new Set()\n\tconst queue = nodes.filter((node) => reverse.get(idOf(node)).length === 0).map(idOf)\n\twhile (queue.length > 0) {\n\t\tconst current = queue.shift()\n\t\tif (!reachable.has(current)) {\n\t\t\treachable.add(current)\n\t\t\tforward.get(current).forEach((next) => queue.push(next))\n\t\t}\n\t}\n\tfinals.forEach((node) => {\n\t\tif (!reachable.has(idOf(node))) {\n\t\t\tissues.push({\n\t\t\t\ttype: 'error',\n\t\t\t\tcode: STATE_MACHINE_RULES.UNREACHABLE_FINAL,\n\t\t\t\tmessage: message('UNREACHABLE_FINAL', { name: node[keys.label] || idOf(node) }),\n\t\t\t})\n\t\t}\n\t})\n\n\tconst reachesFinal = new Set(finals.map(idOf))\n\tconst back = finals.map(idOf)\n\twhile (back.length > 0) {\n\t\treverse.get(back.shift()).forEach((previous) => {\n\t\t\tif (!reachesFinal.has(previous)) {\n\t\t\t\treachesFinal.add(previous)\n\t\t\t\tback.push(previous)\n\t\t\t}\n\t\t})\n\t}\n\n\tconst WHITE = 0\n\tconst GREY = 1\n\tconst BLACK = 2\n\tconst colour = new Map(nodes.map((node) => [idOf(node), WHITE]))\n\tconst stack = []\n\tconst reported = new Set()\n\tconst visit = (id) => {\n\t\tcolour.set(id, GREY)\n\t\tstack.push(id)\n\t\tforward.get(id).forEach((next) => {\n\t\t\tif (colour.get(next) === GREY) {\n\t\t\t\tconst cycle = stack.slice(stack.indexOf(next))\n\t\t\t\tconst cycleKey = [...cycle].sort().join(',')\n\t\t\t\tif (!reported.has(cycleKey) && !cycle.some((member) => reachesFinal.has(member))) {\n\t\t\t\t\treported.add(cycleKey)\n\t\t\t\t\tissues.push({\n\t\t\t\t\t\ttype: 'warning',\n\t\t\t\t\t\tcode: STATE_MACHINE_RULES.CYCLE_NO_EXIT,\n\t\t\t\t\t\tmessage: message('CYCLE_NO_EXIT', { names: cycle.map(nameOf).join(' → ') }),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} else if (colour.get(next) === WHITE) {\n\t\t\t\tvisit(next)\n\t\t\t}\n\t\t})\n\t\tstack.pop()\n\t\tcolour.set(id, BLACK)\n\t}\n\tnodes.forEach((node) => {\n\t\tif (colour.get(idOf(node)) === WHITE) {\n\t\t\tvisit(idOf(node))\n\t\t}\n\t})\n\n\treturn issues\n}\n"],"names":["t"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACY,MAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC;AACjD,CAAC,cAAc,EAAE,gBAAgB;AACjC,CAAC,iBAAiB,EAAE,mBAAmB;AACvC,CAAC,mBAAmB,EAAE,qBAAqB;AAC3C,CAAC,oBAAoB,EAAE,sBAAsB;AAC7C,CAAC,YAAY,EAAE,cAAc;AAC7B,CAAC,aAAa,EAAE,eAAe;AAC/B,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACY,MAAC,0BAA0B,GAAG,MAAM,CAAC,MAAM,CAAC;AACxD,CAAC,EAAE,EAAE,IAAI;AACT,CAAC,KAAK,EAAE,OAAO;AACf,CAAC,KAAK,EAAE,OAAO;AACf,CAAC,IAAI,EAAE,MAAM;AACb,CAAC,EAAE,EAAE,IAAI;AACT,CAAC,eAAe,EAAE,OAAO;AACzB,CAAC,EAAC;AACF;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,eAAe,GAAG;AAC3B,CAAC,OAAO;AACR,EAAE,cAAc,EAAEA,SAAC,CAAC,eAAe,EAAE,6BAA6B,CAAC;AACnE,EAAE,iBAAiB,EAAEA,SAAC,CAAC,eAAe,EAAE,gEAAgE,CAAC;AACzG,EAAE,mBAAmB,EAAEA,SAAC,CAAC,eAAe,EAAE,8DAA8D,CAAC;AACzG,EAAE,oBAAoB,EAAEA,SAAC,CAAC,eAAe,EAAE,8CAA8C,CAAC;AAC1F,EAAE,YAAY,EAAEA,SAAC,CAAC,eAAe,EAAE,+DAA+D,CAAC;AACnG,EAAE,aAAa,EAAEA,SAAC,CAAC,eAAe,EAAE,iDAAiD,CAAC;AACtF,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,MAAM,CAAC,GAAG,EAAE;AAC5B,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;AACzB,EAAE,OAAO,GAAG;AACZ,EAAE;AACF,CAAC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,EAAE,EAAE;AAC5C,EAAE,IAAI;AACN,GAAG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAC;AACjC,GAAG,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,EAAE;AAC7C,GAAG,CAAC,MAAM;AACV,GAAG,OAAO,EAAE;AACZ,GAAG;AACH,EAAE;AACF,CAAC,OAAO,EAAE;AACV,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE;AAC1B,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,IAAI,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;AACrH,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,oBAAoB,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE;AACjF,CAAC,MAAM,IAAI,GAAG,EAAE,GAAG,0BAA0B,EAAE,IAAI,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,GAAE;AACxE,CAAC,MAAM,SAAS,GAAG,OAAO,OAAO,CAAC,SAAS,KAAK,UAAU,GAAG,OAAO,CAAC,SAAS,GAAG,CAAC,IAAI,KAAK,KAAI;AAC/F,CAAC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,IAAI,GAAE;AACnC,CAAC,MAAM,QAAQ,GAAG,eAAe,GAAE;AACnC,CAAC,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK;AACjC,EAAE,MAAM,QAAQ,GAAG,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAC;AAC5G,EAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC;AAC7B,GAAE;AACF;AACA,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAC;AAC7B,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAC;AAClC,CAAC,MAAM,MAAM,GAAG,GAAE;AAClB,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AACzB,EAAE,OAAO,MAAM;AACf,EAAE;AACF;AACA,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,EAAC;AACrC,CAAC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAC;AACzC,CAAC,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK;AACxB,EAAE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAC;AAChE,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;AACzC,GAAE;AACF;AACA,CAAC,MAAM,UAAU,GAAG,GAAE;AACtB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AACzB,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAC;AAC9B,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAC;AAC1B,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;AAC9C,GAAG,MAAM,CAAC,IAAI,CAAC;AACf,IAAI,IAAI,EAAE,OAAO;AACjB,IAAI,IAAI,EAAE,mBAAmB,CAAC,mBAAmB;AACjD,IAAI,OAAO,EAAE,OAAO,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;AACvG,IAAI,EAAC;AACL,GAAG,MAAM;AACT,GAAG;AACH,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,EAAC;AACvB,EAAE,EAAC;AACH;AACA,CAAC,MAAM,SAAS,GAAG,IAAI,GAAG,GAAE;AAC5B,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AAC9B,EAAE,MAAM,IAAI,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAC;AACrD,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;AAC3B,GAAG,MAAM,CAAC,IAAI,CAAC;AACf,IAAI,IAAI,EAAE,SAAS;AACnB,IAAI,IAAI,EAAE,mBAAmB,CAAC,oBAAoB;AAClD,IAAI,OAAO,EAAE,OAAO,CAAC,sBAAsB,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAC1G,IAAI,EAAC;AACL,GAAG;AACH,EAAE,SAAS,CAAC,GAAG,CAAC,IAAI,EAAC;AACrB,EAAE,EAAC;AACH;AACA,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,EAAC;AACjE,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE;AAC1B,EAAE,MAAM,CAAC,IAAI,CAAC;AACd,GAAG,IAAI,EAAE,OAAO;AAChB,GAAG,IAAI,EAAE,mBAAmB,CAAC,cAAc;AAC3C,GAAG,OAAO,EAAE,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC;AACzC,GAAG,EAAC;AACJ,EAAE;AACF;AACA,CAAC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAC;AAC/D,CAAC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAC;AAC/D,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AAC9B,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAC;AAClD,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAC;AAClD,EAAE,EAAC;AACH;AACA,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE;AACvB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AAC1B,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE;AACrF,IAAI,MAAM,CAAC,IAAI,CAAC;AAChB,KAAK,IAAI,EAAE,SAAS;AACpB,KAAK,IAAI,EAAE,mBAAmB,CAAC,YAAY;AAC3C,KAAK,OAAO,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC/E,KAAK,EAAC;AACN,IAAI;AACJ,GAAG,EAAC;AACJ,EAAE;AACF;AACA;AACA;AACA,CAAC,MAAM,SAAS,GAAG,IAAI,GAAG,GAAE;AAC5B,CAAC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAC;AACrF,CAAC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE;AAC1B,EAAE,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,GAAE;AAC/B,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;AAC/B,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,EAAC;AACzB,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAC;AAC3D,GAAG;AACH,EAAE;AACF,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AAC1B,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE;AAClC,GAAG,MAAM,CAAC,IAAI,CAAC;AACf,IAAI,IAAI,EAAE,OAAO;AACjB,IAAI,IAAI,EAAE,mBAAmB,CAAC,iBAAiB;AAC/C,IAAI,OAAO,EAAE,OAAO,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACnF,IAAI,EAAC;AACL,GAAG;AACH,EAAE,EAAC;AACH;AACA,CAAC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAC;AAC/C,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAC;AAC9B,CAAC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;AACzB,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,KAAK;AAClD,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;AACpC,IAAI,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAC;AAC9B,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAC;AACvB,IAAI;AACJ,GAAG,EAAC;AACJ,EAAE;AACF;AACA,CAAC,MAAM,KAAK,GAAG,EAAC;AAChB,CAAC,MAAM,IAAI,GAAG,EAAC;AACf,CAAC,MAAM,KAAK,GAAG,EAAC;AAChB,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,EAAC;AACjE,CAAC,MAAM,KAAK,GAAG,GAAE;AACjB,CAAC,MAAM,QAAQ,GAAG,IAAI,GAAG,GAAE;AAC3B,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,KAAK;AACvB,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAC;AACtB,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAC;AAChB,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AACpC,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;AAClC,IAAI,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAC;AAClD,IAAI,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,EAAC;AAChD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE;AACtF,KAAK,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAC;AAC3B,KAAK,MAAM,CAAC,IAAI,CAAC;AACjB,MAAM,IAAI,EAAE,SAAS;AACrB,MAAM,IAAI,EAAE,mBAAmB,CAAC,aAAa;AAC7C,MAAM,OAAO,EAAE,OAAO,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AACjF,MAAM,EAAC;AACP,KAAK;AACL,IAAI,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE;AAC1C,IAAI,KAAK,CAAC,IAAI,EAAC;AACf,IAAI;AACJ,GAAG,EAAC;AACJ,EAAE,KAAK,CAAC,GAAG,GAAE;AACb,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAC;AACvB,GAAE;AACF,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK;AACzB,EAAE,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,EAAE;AACxC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAC;AACpB,GAAG;AACH,EAAE,EAAC;AACH;AACA,CAAC,OAAO,MAAM;AACd;;;;"}
|
|
@@ -48,10 +48,12 @@ import { canonicalWidgetType } from './widgetTypeAliases.js';
|
|
|
48
48
|
* header (link, note) come from the host, and it has no Add.
|
|
49
49
|
* `parties` draws a record's parties by role and `map` a bare Leaflet map:
|
|
50
50
|
* on a card surface both need the titled wrapper, and neither has an Add.
|
|
51
|
+
* `object-translations` is a bare per-language editor that takes the titled
|
|
52
|
+
* wrapper and has no Add.
|
|
51
53
|
*
|
|
52
54
|
* @type {string[]}
|
|
53
55
|
*/
|
|
54
|
-
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']);
|
|
56
|
+
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']);
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* The stored content/config blob for a widget definition.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widgetDispatch.js","sources":["../../../src/utils/widgetDispatch.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * widgetDispatch — how a detail-page widget definition picks its renderer.\n *\n * These predicates used to live as methods on `CnDetailPage`, each one calling\n * `findWidget(item)` first. That was fine while the detail grid was the only\n * surface rendering a widget definition. `CnTabsWidget` is a second one: it\n * renders the SAME definitions with no card chrome, because the tab strip owns\n * the title and the Actions menu instead.\n *\n * Two copies of \"is this a card widget?\" is exactly the kind of thing that\n * drifts. One copy grows a type, the other does not, and the symptom is a\n * widget that renders correctly on the page and blank inside a tab, with\n * nothing in the console either way. So the decisions live here as pure\n * functions over a widget DEFINITION, and both surfaces import them.\n *\n * The functions take the resolved definition, not a layout item. Resolving an\n * item to a definition needs the surface's own widget list, which is the one\n * part that genuinely differs between the two.\n *\n * @module utils/widgetDispatch\n */\n\nimport { BUILT_IN_WIDGETS } from '../components/CnWidgetGrid/builtInWidgets.js'\nimport { getWidgetTypeEntry } from '../components/CnWidgetGrid/dashboardWidgetRegistry.js'\nimport { canonicalWidgetType } from './widgetTypeAliases.js'\n\n/**\n * Content-only catalog widgets: bare renderers with no chrome of their own.\n * On a card surface they need the titled wrapper; inside a tab panel the strip\n * already supplies the title, so they render bare.\n *\n * `flow-runs` belongs here for the same reason the lists do: it renders a bare\n * row list, so without the wrapper it appeared on the detail page as loose text\n * with no title and no card, which ADR-062 rule 5 forbids (\"every body widget\n * has card chrome and its manifest title\"). It is NOT in the Add-action list,\n * because a run is started by a flow, never by a person clicking Add.\n * `tasks` follows for the same two reasons: a bare row list, and a task is\n * created by a flow, never by a person clicking Add.\n * `facts` is a bare list of label/value rows about one object: on a side\n * card it needs the same titled wrapper, and there is nothing to Add.\n * `field-list` draws rows of one object field and carries its own add form,\n * so it takes the titled wrapper and not the catalog Add action.\n * `record-list` draws bare rows of another schema's objects; the card and its\n * header (link, note) come from the host, and it has no Add.\n * `parties` draws a record's parties by role and `map` a bare Leaflet map:\n * on a card surface both need the titled wrapper, and neither has an Add.\n *\n * @type {string[]}\n */\nexport 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'])\n\n/**\n * The stored content/config blob for a widget definition.\n *\n * @param {object} def The widget definition.\n * @return {object} The definition's `content`, or an empty object.\n */\nexport function widgetContentOf(def) {\n\treturn (def && def.content && typeof def.content === 'object') ? def.content : {}\n}\n\n/**\n * Whether the definition is the schema-driven `data` widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'data'`.\n */\nexport function isDataWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'data'\n}\n\n/**\n * Whether the definition is the schema-driven `related` widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'related'`.\n */\nexport function isRelatedWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'related'\n}\n\n/**\n * Whether the definition is the `object-geo` widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'object-geo'`.\n */\nexport function isGeoWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'object-geo'\n}\n\n/**\n * Whether the definition names a registered integration leaf.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'integration'` with an `integrationId`.\n */\nexport function isIntegrationWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'integration' && typeof def.integrationId === 'string'\n}\n\n/**\n * Whether the definition is a content-only catalog widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true when the type is in {@link CONTENT_ONLY_TYPES}.\n */\nexport function isContentOnlyWidgetDef(def) {\n\treturn Boolean(def) && CONTENT_ONLY_TYPES.includes(def.type)\n}\n\n/**\n * Whether the definition is a registry \"card\" widget: a self-contained KPI,\n * gauge or delta tile that headlines itself (registry entry `card: true`).\n *\n * @param {object} def The widget definition.\n * @return {boolean} true when the registry entry declares `card`.\n */\nexport function isCardWidgetDef(def) {\n\tif (!def || !def.type) {\n\t\treturn false\n\t}\n\tconst entry = getWidgetTypeEntry(def.type)\n\treturn Boolean(entry && entry.card === true)\n}\n\n/**\n * Resolve the registered RENDERER for a content-driven catalog widget type.\n *\n * Integration widgets resolve separately, and `data` needs object context the\n * caller supplies, so both are excluded from this generic fallback.\n *\n * Resolution order is consumer registry, then the dashboard catalog, then\n * `BUILT_IN_WIDGETS`. Those last two were near-disjoint vocabularies until they\n * were unified here: the dashboard catalog held chart / map / object-list /\n * related / stats-block / table, `BUILT_IN_WIDGETS` held audit-trail / banner /\n * card-grid / data / divider / form-renderer / header / integration /\n * map-viewer / metadata / nav-card-grid / object-geo / related / text, and only\n * `related` was in both. A widget authored against one was invisible to the\n * other, and the surface rendered nothing while saying nothing about why. On\n * hrmq that was 101 of 236 detail widgets.\n *\n * With `typeAsWritten`, a type registered in its own right wins over its\n * alias, as on CnDashboardPage: there `object-table` is the host adapter that\n * reads `content.source`, not the `table` widget its alias names. A detail\n * page keeps the alias first, as its manifests were written against it.\n *\n * @param {object} def The widget definition.\n * @param {object} [cnRegistry] The consumer's component registry, if any.\n * @param {{typeAsWritten?: boolean}} [options] Resolution options.\n * @return {object|null} The renderer component, or null.\n */\nexport function resolveRegistryRenderer(def, cnRegistry = {}, options = {}) {\n\tif (!def || !def.type || def.type === 'integration' || def.type === 'data') {\n\t\treturn null\n\t}\n\t// Consumer registry FIRST — the order REQ-MVR-005 mandates (\"Custom widget\n\t// overrides built-in\"), and the order CnWidgetGrid already uses.\n\tconst consumer = (cnRegistry || {})[def.type]\n\tif (consumer) {\n\t\treturn consumer.component ?? consumer\n\t}\n\tconst entry = (options.typeAsWritten === true && getWidgetTypeEntry(def.type)) || getWidgetTypeEntry(canonicalWidgetType(def.type))\n\tif (entry && entry.renderer) {\n\t\treturn entry.renderer\n\t}\n\treturn BUILT_IN_WIDGETS[canonicalWidgetType(def.type)] || BUILT_IN_WIDGETS[def.type] || null\n}\n\n/**\n * The effective title for a widget definition.\n *\n * A title-owning type (registry `ownsTitle`, e.g. `related`) keeps its editable\n * title in `content.title`, and that MUST win over the chrome `def.title`.\n * Otherwise a non-empty seed title permanently shadows the user's edit and the\n * title can never be changed. Without a `content.title` it falls back to\n * `def.title`. Every other type reads `def.title` first.\n *\n * @param {object} def The widget definition.\n * @return {string|undefined} The title, or undefined to let the widget default.\n */\nexport function widgetTitleOf(def) {\n\tif (!def) {\n\t\treturn undefined\n\t}\n\tconst content = widgetContentOf(def)\n\tconst entry = getWidgetTypeEntry(def.type)\n\tif (entry && entry.ownsTitle) {\n\t\t// A manifest that never set content.title keeps its def.title.\n\t\tif (typeof content.title !== 'string') {\n\t\t\treturn def.title || undefined\n\t\t}\n\t\treturn content.title || undefined\n\t}\n\treturn def.title || content.title || undefined\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"widgetDispatch.js","sources":["../../../src/utils/widgetDispatch.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * widgetDispatch — how a detail-page widget definition picks its renderer.\n *\n * These predicates used to live as methods on `CnDetailPage`, each one calling\n * `findWidget(item)` first. That was fine while the detail grid was the only\n * surface rendering a widget definition. `CnTabsWidget` is a second one: it\n * renders the SAME definitions with no card chrome, because the tab strip owns\n * the title and the Actions menu instead.\n *\n * Two copies of \"is this a card widget?\" is exactly the kind of thing that\n * drifts. One copy grows a type, the other does not, and the symptom is a\n * widget that renders correctly on the page and blank inside a tab, with\n * nothing in the console either way. So the decisions live here as pure\n * functions over a widget DEFINITION, and both surfaces import them.\n *\n * The functions take the resolved definition, not a layout item. Resolving an\n * item to a definition needs the surface's own widget list, which is the one\n * part that genuinely differs between the two.\n *\n * @module utils/widgetDispatch\n */\n\nimport { BUILT_IN_WIDGETS } from '../components/CnWidgetGrid/builtInWidgets.js'\nimport { getWidgetTypeEntry } from '../components/CnWidgetGrid/dashboardWidgetRegistry.js'\nimport { canonicalWidgetType } from './widgetTypeAliases.js'\n\n/**\n * Content-only catalog widgets: bare renderers with no chrome of their own.\n * On a card surface they need the titled wrapper; inside a tab panel the strip\n * already supplies the title, so they render bare.\n *\n * `flow-runs` belongs here for the same reason the lists do: it renders a bare\n * row list, so without the wrapper it appeared on the detail page as loose text\n * with no title and no card, which ADR-062 rule 5 forbids (\"every body widget\n * has card chrome and its manifest title\"). It is NOT in the Add-action list,\n * because a run is started by a flow, never by a person clicking Add.\n * `tasks` follows for the same two reasons: a bare row list, and a task is\n * created by a flow, never by a person clicking Add.\n * `facts` is a bare list of label/value rows about one object: on a side\n * card it needs the same titled wrapper, and there is nothing to Add.\n * `field-list` draws rows of one object field and carries its own add form,\n * so it takes the titled wrapper and not the catalog Add action.\n * `record-list` draws bare rows of another schema's objects; the card and its\n * header (link, note) come from the host, and it has no Add.\n * `parties` draws a record's parties by role and `map` a bare Leaflet map:\n * on a card surface both need the titled wrapper, and neither has an Add.\n * `object-translations` is a bare per-language editor that takes the titled\n * wrapper and has no Add.\n *\n * @type {string[]}\n */\nexport 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'])\n\n/**\n * The stored content/config blob for a widget definition.\n *\n * @param {object} def The widget definition.\n * @return {object} The definition's `content`, or an empty object.\n */\nexport function widgetContentOf(def) {\n\treturn (def && def.content && typeof def.content === 'object') ? def.content : {}\n}\n\n/**\n * Whether the definition is the schema-driven `data` widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'data'`.\n */\nexport function isDataWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'data'\n}\n\n/**\n * Whether the definition is the schema-driven `related` widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'related'`.\n */\nexport function isRelatedWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'related'\n}\n\n/**\n * Whether the definition is the `object-geo` widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'object-geo'`.\n */\nexport function isGeoWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'object-geo'\n}\n\n/**\n * Whether the definition names a registered integration leaf.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true for `type: 'integration'` with an `integrationId`.\n */\nexport function isIntegrationWidgetDef(def) {\n\treturn Boolean(def) && def.type === 'integration' && typeof def.integrationId === 'string'\n}\n\n/**\n * Whether the definition is a content-only catalog widget.\n *\n * @param {object} def The widget definition.\n * @return {boolean} true when the type is in {@link CONTENT_ONLY_TYPES}.\n */\nexport function isContentOnlyWidgetDef(def) {\n\treturn Boolean(def) && CONTENT_ONLY_TYPES.includes(def.type)\n}\n\n/**\n * Whether the definition is a registry \"card\" widget: a self-contained KPI,\n * gauge or delta tile that headlines itself (registry entry `card: true`).\n *\n * @param {object} def The widget definition.\n * @return {boolean} true when the registry entry declares `card`.\n */\nexport function isCardWidgetDef(def) {\n\tif (!def || !def.type) {\n\t\treturn false\n\t}\n\tconst entry = getWidgetTypeEntry(def.type)\n\treturn Boolean(entry && entry.card === true)\n}\n\n/**\n * Resolve the registered RENDERER for a content-driven catalog widget type.\n *\n * Integration widgets resolve separately, and `data` needs object context the\n * caller supplies, so both are excluded from this generic fallback.\n *\n * Resolution order is consumer registry, then the dashboard catalog, then\n * `BUILT_IN_WIDGETS`. Those last two were near-disjoint vocabularies until they\n * were unified here: the dashboard catalog held chart / map / object-list /\n * related / stats-block / table, `BUILT_IN_WIDGETS` held audit-trail / banner /\n * card-grid / data / divider / form-renderer / header / integration /\n * map-viewer / metadata / nav-card-grid / object-geo / related / text, and only\n * `related` was in both. A widget authored against one was invisible to the\n * other, and the surface rendered nothing while saying nothing about why. On\n * hrmq that was 101 of 236 detail widgets.\n *\n * With `typeAsWritten`, a type registered in its own right wins over its\n * alias, as on CnDashboardPage: there `object-table` is the host adapter that\n * reads `content.source`, not the `table` widget its alias names. A detail\n * page keeps the alias first, as its manifests were written against it.\n *\n * @param {object} def The widget definition.\n * @param {object} [cnRegistry] The consumer's component registry, if any.\n * @param {{typeAsWritten?: boolean}} [options] Resolution options.\n * @return {object|null} The renderer component, or null.\n */\nexport function resolveRegistryRenderer(def, cnRegistry = {}, options = {}) {\n\tif (!def || !def.type || def.type === 'integration' || def.type === 'data') {\n\t\treturn null\n\t}\n\t// Consumer registry FIRST — the order REQ-MVR-005 mandates (\"Custom widget\n\t// overrides built-in\"), and the order CnWidgetGrid already uses.\n\tconst consumer = (cnRegistry || {})[def.type]\n\tif (consumer) {\n\t\treturn consumer.component ?? consumer\n\t}\n\tconst entry = (options.typeAsWritten === true && getWidgetTypeEntry(def.type)) || getWidgetTypeEntry(canonicalWidgetType(def.type))\n\tif (entry && entry.renderer) {\n\t\treturn entry.renderer\n\t}\n\treturn BUILT_IN_WIDGETS[canonicalWidgetType(def.type)] || BUILT_IN_WIDGETS[def.type] || null\n}\n\n/**\n * The effective title for a widget definition.\n *\n * A title-owning type (registry `ownsTitle`, e.g. `related`) keeps its editable\n * title in `content.title`, and that MUST win over the chrome `def.title`.\n * Otherwise a non-empty seed title permanently shadows the user's edit and the\n * title can never be changed. Without a `content.title` it falls back to\n * `def.title`. Every other type reads `def.title` first.\n *\n * @param {object} def The widget definition.\n * @return {string|undefined} The title, or undefined to let the widget default.\n */\nexport function widgetTitleOf(def) {\n\tif (!def) {\n\t\treturn undefined\n\t}\n\tconst content = widgetContentOf(def)\n\tconst entry = getWidgetTypeEntry(def.type)\n\tif (entry && entry.ownsTitle) {\n\t\t// A manifest that never set content.title keeps its def.title.\n\t\tif (typeof content.title !== 'string') {\n\t\t\treturn def.title || undefined\n\t\t}\n\t\treturn content.title || undefined\n\t}\n\treturn def.title || content.title || undefined\n}\n"],"names":[],"mappings":"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"}
|