@conduction/nextcloud-vue 1.0.0-beta.20 → 1.0.0-beta.22
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/nextcloud-vue.cjs.js +831 -110
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +59 -7
- package/dist/nextcloud-vue.esm.js +830 -111
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +305 -26
- package/src/composables/useAppManifest.js +37 -9
- package/src/index.js +1 -0
- package/src/schemas/app-manifest.schema.json +40 -2
- package/src/utils/resolveManifestSentinels.js +268 -0
- package/src/utils/validateManifest.js +216 -53
|
@@ -43033,7 +43033,7 @@ var isObject = function isObject(obj) {
|
|
|
43033
43033
|
}; // Strict object type check
|
|
43034
43034
|
// Only returns true for plain JavaScript objects
|
|
43035
43035
|
|
|
43036
|
-
var isPlainObject$
|
|
43036
|
+
var isPlainObject$3 = function isPlainObject(obj) {
|
|
43037
43037
|
return Object.prototype.toString.call(obj) === '[object Object]';
|
|
43038
43038
|
};
|
|
43039
43039
|
var isDate = function isDate(value) {
|
|
@@ -43115,7 +43115,7 @@ var cloneDeep = function cloneDeep(obj) {
|
|
|
43115
43115
|
}, []);
|
|
43116
43116
|
}
|
|
43117
43117
|
|
|
43118
|
-
if (isPlainObject$
|
|
43118
|
+
if (isPlainObject$3(obj)) {
|
|
43119
43119
|
return keys(obj).reduce(function (result, key) {
|
|
43120
43120
|
return _objectSpread$8(_objectSpread$8({}, result), {}, _defineProperty$b({}, key, cloneDeep(obj[key], obj[key])));
|
|
43121
43121
|
}, {});
|
|
@@ -43323,7 +43323,7 @@ var kebabCase = function kebabCase(str) {
|
|
|
43323
43323
|
|
|
43324
43324
|
var toString = function toString(val) {
|
|
43325
43325
|
var spaces = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
|
|
43326
|
-
return isUndefinedOrNull(val) ? '' : isArray(val) || isPlainObject$
|
|
43326
|
+
return isUndefinedOrNull(val) ? '' : isArray(val) || isPlainObject$3(val) && val.toString === Object.prototype.toString ? JSON.stringify(val, null, spaces) : String(val);
|
|
43327
43327
|
}; // Remove leading white space from a string
|
|
43328
43328
|
|
|
43329
43329
|
var ELEMENT_PROTO = Element$1.prototype;
|
|
@@ -43591,7 +43591,7 @@ var BVTransition = /*#__PURE__*/extend({
|
|
|
43591
43591
|
props = _ref.props;
|
|
43592
43592
|
var transProps = props.transProps;
|
|
43593
43593
|
|
|
43594
|
-
if (!isPlainObject$
|
|
43594
|
+
if (!isPlainObject$3(transProps)) {
|
|
43595
43595
|
transProps = props.noFade ? NO_FADE_PROPS : FADE_PROPS;
|
|
43596
43596
|
|
|
43597
43597
|
if (props.appear) {
|
|
@@ -43652,7 +43652,7 @@ var encode = function encode(str) {
|
|
|
43652
43652
|
// See: https://github.com/vuejs/vue-router/blob/dev/src/util/query.js
|
|
43653
43653
|
|
|
43654
43654
|
var stringifyQueryObj = function stringifyQueryObj(obj) {
|
|
43655
|
-
if (!isPlainObject$
|
|
43655
|
+
if (!isPlainObject$3(obj)) {
|
|
43656
43656
|
return '';
|
|
43657
43657
|
}
|
|
43658
43658
|
|
|
@@ -43743,7 +43743,7 @@ var computeHref = function computeHref() {
|
|
|
43743
43743
|
} // Fallback to `to.path' + `to.query` + `to.hash` prop (if `to` is an object)
|
|
43744
43744
|
|
|
43745
43745
|
|
|
43746
|
-
if (isPlainObject$
|
|
43746
|
+
if (isPlainObject$3(to) && (to.path || to.query || to.hash)) {
|
|
43747
43747
|
var path = toString(to.path);
|
|
43748
43748
|
var query = stringifyQueryObj(to.query);
|
|
43749
43749
|
var hash = toString(to.hash);
|
|
@@ -55925,6 +55925,29 @@ function useContextMenu() {
|
|
|
55925
55925
|
}
|
|
55926
55926
|
}
|
|
55927
55927
|
|
|
55928
|
+
/**
|
|
55929
|
+
* Pattern matching the `manifest-resolve-sentinel` capability's
|
|
55930
|
+
* sentinel — `@resolve:<key>` where `<key>` is lowercase alphanumeric
|
|
55931
|
+
* with `_` / `-` separators. The full string IS the sentinel; partial
|
|
55932
|
+
* substitution like `prefix-@resolve:foo` is NOT supported and is left
|
|
55933
|
+
* as a plain string for downstream renderers.
|
|
55934
|
+
*
|
|
55935
|
+
* Build-time validation accepts this pattern as a valid `string` for
|
|
55936
|
+
* any `string`-typed field UNDER `pages[].config`, regardless of any
|
|
55937
|
+
* narrower per-field constraint. Other paths reject it explicitly.
|
|
55938
|
+
*/
|
|
55939
|
+
const SENTINEL_PATTERN$1 = /^@resolve:[a-z][a-z0-9_-]*$/;
|
|
55940
|
+
|
|
55941
|
+
/**
|
|
55942
|
+
* Test whether a string is a manifest `@resolve:` sentinel.
|
|
55943
|
+
*
|
|
55944
|
+
* @param {*} value Candidate value.
|
|
55945
|
+
* @return {boolean} True when the value is a fully-matched sentinel.
|
|
55946
|
+
*/
|
|
55947
|
+
function isSentinel(value) {
|
|
55948
|
+
return typeof value === 'string' && SENTINEL_PATTERN$1.test(value)
|
|
55949
|
+
}
|
|
55950
|
+
|
|
55928
55951
|
/**
|
|
55929
55952
|
* Validate a manifest object against the manifest JSON Schema.
|
|
55930
55953
|
*
|
|
@@ -55942,6 +55965,13 @@ function useContextMenu() {
|
|
|
55942
55965
|
* - `pages[].component` is required when `type` is "custom".
|
|
55943
55966
|
* - Per-type `config` shape rules for the built-in types `logs`,
|
|
55944
55967
|
* `settings`, `chat`, `files` (REQ from manifest-page-type-extensions).
|
|
55968
|
+
* - The `manifest-resolve-sentinel` sentinel `@resolve:<key>` is
|
|
55969
|
+
* permissively accepted under `pages[].config.*` and explicitly
|
|
55970
|
+
* REJECTED in `version`, `dependencies[]`, `menu[].route`,
|
|
55971
|
+
* `menu[].id`, `pages[].id`, `pages[].route`, `pages[].component`,
|
|
55972
|
+
* `pages[].headerComponent`, `pages[].actionsComponent`,
|
|
55973
|
+
* `pages[].slots.*` — those are router invariants or registry
|
|
55974
|
+
* keys.
|
|
55945
55975
|
*
|
|
55946
55976
|
* The richer schema constraints (`additionalProperties: false`, `format`
|
|
55947
55977
|
* URI, etc.) are enforced by the BE / hydra CI validators that consume
|
|
@@ -55961,7 +55991,7 @@ function useContextMenu() {
|
|
|
55961
55991
|
function validateManifest(manifest, options = {}) {
|
|
55962
55992
|
const errors = [];
|
|
55963
55993
|
|
|
55964
|
-
if (!isPlainObject$
|
|
55994
|
+
if (!isPlainObject$2(manifest)) {
|
|
55965
55995
|
return { valid: false, errors: ['manifest must be an object'] }
|
|
55966
55996
|
}
|
|
55967
55997
|
|
|
@@ -55969,6 +55999,10 @@ function validateManifest(manifest, options = {}) {
|
|
|
55969
55999
|
|
|
55970
56000
|
if (typeof manifest.version !== 'string') {
|
|
55971
56001
|
errors.push('/version must be a string');
|
|
56002
|
+
} else if (isSentinel(manifest.version)) {
|
|
56003
|
+
// `manifest-resolve-sentinel` REQ-MRS-004: sentinel is a router /
|
|
56004
|
+
// registry invariant violation when used here.
|
|
56005
|
+
errors.push(`/version "${manifest.version}" must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
55972
56006
|
} else if (!versionPattern.test(manifest.version)) {
|
|
55973
56007
|
errors.push(`/version "${manifest.version}" must match semver pattern`);
|
|
55974
56008
|
}
|
|
@@ -55977,12 +56011,19 @@ function validateManifest(manifest, options = {}) {
|
|
|
55977
56011
|
errors.push('/menu must be an array');
|
|
55978
56012
|
} else {
|
|
55979
56013
|
manifest.menu.forEach((item, index) => {
|
|
55980
|
-
if (!isPlainObject$
|
|
56014
|
+
if (!isPlainObject$2(item)) {
|
|
55981
56015
|
errors.push(`/menu/${index} must be an object`);
|
|
55982
56016
|
return
|
|
55983
56017
|
}
|
|
55984
|
-
if (typeof item.id !== 'string')
|
|
56018
|
+
if (typeof item.id !== 'string') {
|
|
56019
|
+
errors.push(`/menu/${index}/id must be a string`);
|
|
56020
|
+
} else if (isSentinel(item.id)) {
|
|
56021
|
+
errors.push(`/menu/${index}/id must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56022
|
+
}
|
|
55985
56023
|
if (typeof item.label !== 'string') errors.push(`/menu/${index}/label must be a string`);
|
|
56024
|
+
if (item.route !== undefined && isSentinel(item.route)) {
|
|
56025
|
+
errors.push(`/menu/${index}/route must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56026
|
+
}
|
|
55986
56027
|
if (item.children !== undefined && !Array.isArray(item.children)) {
|
|
55987
56028
|
errors.push(`/menu/${index}/children must be an array`);
|
|
55988
56029
|
}
|
|
@@ -55998,18 +56039,24 @@ function validateManifest(manifest, options = {}) {
|
|
|
55998
56039
|
} else {
|
|
55999
56040
|
const seenIds = new Set();
|
|
56000
56041
|
manifest.pages.forEach((page, index) => {
|
|
56001
|
-
if (!isPlainObject$
|
|
56042
|
+
if (!isPlainObject$2(page)) {
|
|
56002
56043
|
errors.push(`/pages/${index} must be an object`);
|
|
56003
56044
|
return
|
|
56004
56045
|
}
|
|
56005
56046
|
if (typeof page.id !== 'string') {
|
|
56006
56047
|
errors.push(`/pages/${index}/id must be a string`);
|
|
56048
|
+
} else if (isSentinel(page.id)) {
|
|
56049
|
+
errors.push(`/pages/${index}/id must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56007
56050
|
} else if (seenIds.has(page.id)) {
|
|
56008
56051
|
errors.push(`/pages/${index}/id "${page.id}" must be unique within pages[]`);
|
|
56009
56052
|
} else {
|
|
56010
56053
|
seenIds.add(page.id);
|
|
56011
56054
|
}
|
|
56012
|
-
if (typeof page.route !== 'string')
|
|
56055
|
+
if (typeof page.route !== 'string') {
|
|
56056
|
+
errors.push(`/pages/${index}/route must be a string`);
|
|
56057
|
+
} else if (isSentinel(page.route)) {
|
|
56058
|
+
errors.push(`/pages/${index}/route must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56059
|
+
}
|
|
56013
56060
|
if (typeof page.title !== 'string') errors.push(`/pages/${index}/title must be a string`);
|
|
56014
56061
|
if (typeof page.type !== 'string' || page.type.length === 0) {
|
|
56015
56062
|
errors.push(`/pages/${index}/type must be a non-empty string`);
|
|
@@ -56019,6 +56066,25 @@ function validateManifest(manifest, options = {}) {
|
|
|
56019
56066
|
if (page.type === 'custom' && typeof page.component !== 'string') {
|
|
56020
56067
|
errors.push(`/pages/${index}/component is required when type is "custom"`);
|
|
56021
56068
|
}
|
|
56069
|
+
// `manifest-resolve-sentinel` REQ-MRS-004: registry-key
|
|
56070
|
+
// fields cannot be dynamic — they resolve at module-load
|
|
56071
|
+
// time against `customComponents`, before the loader runs.
|
|
56072
|
+
if (page.component !== undefined && isSentinel(page.component)) {
|
|
56073
|
+
errors.push(`/pages/${index}/component must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56074
|
+
}
|
|
56075
|
+
if (page.headerComponent !== undefined && isSentinel(page.headerComponent)) {
|
|
56076
|
+
errors.push(`/pages/${index}/headerComponent must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56077
|
+
}
|
|
56078
|
+
if (page.actionsComponent !== undefined && isSentinel(page.actionsComponent)) {
|
|
56079
|
+
errors.push(`/pages/${index}/actionsComponent must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56080
|
+
}
|
|
56081
|
+
if (isPlainObject$2(page.slots)) {
|
|
56082
|
+
for (const [slotName, slotValue] of Object.entries(page.slots)) {
|
|
56083
|
+
if (isSentinel(slotValue)) {
|
|
56084
|
+
errors.push(`/pages/${index}/slots/${slotName} must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56085
|
+
}
|
|
56086
|
+
}
|
|
56087
|
+
}
|
|
56022
56088
|
|
|
56023
56089
|
// Per-type config-shape validation for built-in extended types.
|
|
56024
56090
|
// (`manifest-page-type-extensions` spec — covers logs/settings/chat/files.)
|
|
@@ -56042,6 +56108,8 @@ function validateManifest(manifest, options = {}) {
|
|
|
56042
56108
|
manifest.dependencies.forEach((dep, index) => {
|
|
56043
56109
|
if (typeof dep !== 'string') {
|
|
56044
56110
|
errors.push(`/dependencies/${index} must be a string`);
|
|
56111
|
+
} else if (isSentinel(dep)) {
|
|
56112
|
+
errors.push(`/dependencies/${index} must not be a @resolve: sentinel (sentinels are only valid under pages[].config.*)`);
|
|
56045
56113
|
}
|
|
56046
56114
|
});
|
|
56047
56115
|
}
|
|
@@ -56050,7 +56118,7 @@ function validateManifest(manifest, options = {}) {
|
|
|
56050
56118
|
return { valid: errors.length === 0, errors }
|
|
56051
56119
|
}
|
|
56052
56120
|
|
|
56053
|
-
function isPlainObject$
|
|
56121
|
+
function isPlainObject$2(value) {
|
|
56054
56122
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
56055
56123
|
}
|
|
56056
56124
|
|
|
@@ -56073,7 +56141,7 @@ function isPlainObject$1(value) {
|
|
|
56073
56141
|
*/
|
|
56074
56142
|
function validateTypeConfig(page, index, errors) {
|
|
56075
56143
|
if (!page || typeof page.type !== 'string') return
|
|
56076
|
-
const cfg = isPlainObject$
|
|
56144
|
+
const cfg = isPlainObject$2(page.config) ? page.config : null;
|
|
56077
56145
|
const pathBracket = `pages[${index}].config`;
|
|
56078
56146
|
const pathSlash = `/pages/${index}/config`;
|
|
56079
56147
|
|
|
@@ -56105,7 +56173,70 @@ function validateTypeConfig(page, index, errors) {
|
|
|
56105
56173
|
break
|
|
56106
56174
|
}
|
|
56107
56175
|
case 'settings': {
|
|
56108
|
-
|
|
56176
|
+
// `manifest-settings-orchestration` REQ-MSO-1: a settings page
|
|
56177
|
+
// MUST declare EXACTLY ONE of `sections` | `tabs`. When both
|
|
56178
|
+
// are set, emit the orchestration mutex error. When neither is
|
|
56179
|
+
// set, fall through to the legacy `sections required` error
|
|
56180
|
+
// (back-compat — REQ-MSO-7 / REQ-MSO-1 last scenario).
|
|
56181
|
+
const hasSections = cfg && Array.isArray(cfg.sections);
|
|
56182
|
+
const hasTabs = cfg && Array.isArray(cfg.tabs);
|
|
56183
|
+
|
|
56184
|
+
if (hasSections && hasTabs) {
|
|
56185
|
+
errors.push(`${pathSlash}: ${pathBracket}: must declare exactly one of sections | tabs`);
|
|
56186
|
+
break
|
|
56187
|
+
}
|
|
56188
|
+
|
|
56189
|
+
if (hasTabs) {
|
|
56190
|
+
// `manifest-settings-orchestration` REQ-MSO-2..4: validate
|
|
56191
|
+
// the `tabs[]` orchestration shape.
|
|
56192
|
+
if (cfg.tabs.length === 0) {
|
|
56193
|
+
errors.push(`${pathSlash}/tabs: ${pathBracket}.tabs: must contain at least 1 tab`);
|
|
56194
|
+
break
|
|
56195
|
+
}
|
|
56196
|
+
const seenTabIds = Object.create(null);
|
|
56197
|
+
cfg.tabs.forEach((tab, tIndex) => {
|
|
56198
|
+
if (!isPlainObject$2(tab)) {
|
|
56199
|
+
errors.push(`${pathSlash}/tabs/${tIndex}: must be an object`);
|
|
56200
|
+
return
|
|
56201
|
+
}
|
|
56202
|
+
if (typeof tab.id !== 'string' || tab.id.length === 0) {
|
|
56203
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/id: required, must be a non-empty string`);
|
|
56204
|
+
}
|
|
56205
|
+
if (typeof tab.label !== 'string' || tab.label.length === 0) {
|
|
56206
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/label: required, must be a non-empty string`);
|
|
56207
|
+
}
|
|
56208
|
+
// REQ-MSO-3: tab IDs must be unique within a page.
|
|
56209
|
+
if (typeof tab.id === 'string' && tab.id.length > 0) {
|
|
56210
|
+
if (seenTabIds[tab.id]) {
|
|
56211
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/id: ${pathBracket}.tabs[${tIndex}].id: duplicate id "${tab.id}" — tab IDs must be unique within a page`);
|
|
56212
|
+
}
|
|
56213
|
+
seenTabIds[tab.id] = true;
|
|
56214
|
+
}
|
|
56215
|
+
// `tab.sections` MUST be a non-empty array.
|
|
56216
|
+
if (!Array.isArray(tab.sections)) {
|
|
56217
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/sections: ${pathBracket}.tabs[${tIndex}].sections: required, must be an array`);
|
|
56218
|
+
return
|
|
56219
|
+
}
|
|
56220
|
+
if (tab.sections.length === 0) {
|
|
56221
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/sections: ${pathBracket}.tabs[${tIndex}].sections: must contain at least 1 section`);
|
|
56222
|
+
return
|
|
56223
|
+
}
|
|
56224
|
+
// REQ-MSO-4: each tab's sections follow the same rules
|
|
56225
|
+
// as the flat case — share the per-section validator.
|
|
56226
|
+
tab.sections.forEach((section, sIndex) => {
|
|
56227
|
+
validateSettingsSection(
|
|
56228
|
+
section,
|
|
56229
|
+
`${pathSlash}/tabs/${tIndex}/sections/${sIndex}`,
|
|
56230
|
+
`${pathBracket}.tabs[${tIndex}].sections[${sIndex}]`,
|
|
56231
|
+
errors,
|
|
56232
|
+
);
|
|
56233
|
+
});
|
|
56234
|
+
});
|
|
56235
|
+
break
|
|
56236
|
+
}
|
|
56237
|
+
|
|
56238
|
+
// Flat `sections[]` (existing path — REQ-MSRS-* + back-compat).
|
|
56239
|
+
if (!hasSections) {
|
|
56109
56240
|
errors.push(`${pathSlash}/sections: ${pathBracket}.sections: required, must be an array`);
|
|
56110
56241
|
break
|
|
56111
56242
|
}
|
|
@@ -56114,56 +56245,12 @@ function validateTypeConfig(page, index, errors) {
|
|
|
56114
56245
|
break
|
|
56115
56246
|
}
|
|
56116
56247
|
cfg.sections.forEach((section, sIndex) => {
|
|
56117
|
-
|
|
56118
|
-
|
|
56119
|
-
|
|
56120
|
-
|
|
56121
|
-
|
|
56122
|
-
|
|
56123
|
-
}
|
|
56124
|
-
|
|
56125
|
-
// `manifest-settings-rich-sections` REQ-MSRS-1: each
|
|
56126
|
-
// section MUST declare exactly one of fields | component
|
|
56127
|
-
// | widgets. Mixed bodies confuse the renderer + duplicate
|
|
56128
|
-
// the section chrome; empty bodies render nothing so they
|
|
56129
|
-
// are a manifest-author bug.
|
|
56130
|
-
const hasFields = Array.isArray(section.fields);
|
|
56131
|
-
const hasComponent = typeof section.component === 'string' && section.component.length > 0;
|
|
56132
|
-
const hasWidgets = Array.isArray(section.widgets) && section.widgets.length > 0;
|
|
56133
|
-
const bodyCount = (hasFields ? 1 : 0) + (hasComponent ? 1 : 0) + (hasWidgets ? 1 : 0);
|
|
56134
|
-
|
|
56135
|
-
if (bodyCount !== 1) {
|
|
56136
|
-
errors.push(`${pathSlash}/sections/${sIndex}: ${pathBracket}.sections[${sIndex}]: must declare exactly one of fields | component | widgets`);
|
|
56137
|
-
}
|
|
56138
|
-
|
|
56139
|
-
// `widgets` set but not an array (string / object / etc.)
|
|
56140
|
-
if (section.widgets !== undefined && !Array.isArray(section.widgets)) {
|
|
56141
|
-
errors.push(`${pathSlash}/sections/${sIndex}/widgets: must be an array when set`);
|
|
56142
|
-
}
|
|
56143
|
-
|
|
56144
|
-
// `component` set but not a string.
|
|
56145
|
-
if (section.component !== undefined && typeof section.component !== 'string') {
|
|
56146
|
-
errors.push(`${pathSlash}/sections/${sIndex}/component: must be a string when set`);
|
|
56147
|
-
}
|
|
56148
|
-
|
|
56149
|
-
// Per-widget shape rules.
|
|
56150
|
-
if (hasWidgets) {
|
|
56151
|
-
section.widgets.forEach((widget, wIndex) => {
|
|
56152
|
-
if (!isPlainObject$1(widget)) {
|
|
56153
|
-
errors.push(`${pathSlash}/sections/${sIndex}/widgets/${wIndex}: must be an object`);
|
|
56154
|
-
return
|
|
56155
|
-
}
|
|
56156
|
-
if (typeof widget.type !== 'string' || widget.type.length === 0) {
|
|
56157
|
-
errors.push(`${pathSlash}/sections/${sIndex}/widgets/${wIndex}/type: must be a non-empty string`);
|
|
56158
|
-
}
|
|
56159
|
-
});
|
|
56160
|
-
}
|
|
56161
|
-
|
|
56162
|
-
// `manifest-config-refs` REQ-MCR — when fields[] body is
|
|
56163
|
-
// used, each entry must match the formField $def shape.
|
|
56164
|
-
if (hasFields) {
|
|
56165
|
-
validateFieldsArray(section.fields, `${pathSlash}/sections/${sIndex}/fields`, errors);
|
|
56166
|
-
}
|
|
56248
|
+
validateSettingsSection(
|
|
56249
|
+
section,
|
|
56250
|
+
`${pathSlash}/sections/${sIndex}`,
|
|
56251
|
+
`${pathBracket}.sections[${sIndex}]`,
|
|
56252
|
+
errors,
|
|
56253
|
+
);
|
|
56167
56254
|
});
|
|
56168
56255
|
break
|
|
56169
56256
|
}
|
|
@@ -56251,12 +56338,12 @@ function validateTypeConfig(page, index, errors) {
|
|
|
56251
56338
|
*/
|
|
56252
56339
|
function validateSidebarConfig(page, pageIndex, errors) {
|
|
56253
56340
|
const config = page.config;
|
|
56254
|
-
if (!isPlainObject$
|
|
56341
|
+
if (!isPlainObject$2(config)) return
|
|
56255
56342
|
|
|
56256
56343
|
// --- Index sidebar ---
|
|
56257
56344
|
if (page.type === 'index' && config.sidebar !== undefined) {
|
|
56258
56345
|
const path = `/pages/${pageIndex}/config/sidebar`;
|
|
56259
|
-
if (!isPlainObject$
|
|
56346
|
+
if (!isPlainObject$2(config.sidebar)) {
|
|
56260
56347
|
errors.push(`${path} must be an object`);
|
|
56261
56348
|
} else {
|
|
56262
56349
|
if (config.sidebar.enabled !== undefined && typeof config.sidebar.enabled !== 'boolean') {
|
|
@@ -56269,13 +56356,13 @@ function validateSidebarConfig(page, pageIndex, errors) {
|
|
|
56269
56356
|
if (config.sidebar.columnGroups !== undefined && !Array.isArray(config.sidebar.columnGroups)) {
|
|
56270
56357
|
errors.push(`${path}/columnGroups must be an array`);
|
|
56271
56358
|
}
|
|
56272
|
-
if (config.sidebar.facets !== undefined && !isPlainObject$
|
|
56359
|
+
if (config.sidebar.facets !== undefined && !isPlainObject$2(config.sidebar.facets)) {
|
|
56273
56360
|
errors.push(`${path}/facets must be an object`);
|
|
56274
56361
|
}
|
|
56275
56362
|
if (config.sidebar.showMetadata !== undefined && typeof config.sidebar.showMetadata !== 'boolean') {
|
|
56276
56363
|
errors.push(`${path}/showMetadata must be a boolean`);
|
|
56277
56364
|
}
|
|
56278
|
-
if (config.sidebar.search !== undefined && !isPlainObject$
|
|
56365
|
+
if (config.sidebar.search !== undefined && !isPlainObject$2(config.sidebar.search)) {
|
|
56279
56366
|
errors.push(`${path}/search must be an object`);
|
|
56280
56367
|
}
|
|
56281
56368
|
}
|
|
@@ -56290,7 +56377,7 @@ function validateSidebarConfig(page, pageIndex, errors) {
|
|
|
56290
56377
|
const path = `/pages/${pageIndex}/config/sidebar`;
|
|
56291
56378
|
const sb = config.sidebar;
|
|
56292
56379
|
const isBool = typeof sb === 'boolean';
|
|
56293
|
-
const isObj = isPlainObject$
|
|
56380
|
+
const isObj = isPlainObject$2(sb);
|
|
56294
56381
|
if (!isBool && !isObj) {
|
|
56295
56382
|
errors.push(`${path} must be a boolean (legacy) or object`);
|
|
56296
56383
|
} else if (isObj) {
|
|
@@ -56330,7 +56417,7 @@ function validateSidebarConfig(page, pageIndex, errors) {
|
|
|
56330
56417
|
}
|
|
56331
56418
|
|
|
56332
56419
|
// --- Detail sidebar tabs (legacy sidebarProps.tabs path) ---
|
|
56333
|
-
if (page.type === 'detail' && isPlainObject$
|
|
56420
|
+
if (page.type === 'detail' && isPlainObject$2(config.sidebarProps) && config.sidebarProps.tabs !== undefined) {
|
|
56334
56421
|
const tabsPath = `/pages/${pageIndex}/config/sidebarProps/tabs`;
|
|
56335
56422
|
validateDetailTabsArray(config.sidebarProps.tabs, tabsPath, errors);
|
|
56336
56423
|
}
|
|
@@ -56354,7 +56441,7 @@ function validateDetailTabsArray(tabs, tabsPath, errors) {
|
|
|
56354
56441
|
const seenIds = new Set();
|
|
56355
56442
|
tabs.forEach((tab, tabIndex) => {
|
|
56356
56443
|
const tabPath = `${tabsPath}/${tabIndex}`;
|
|
56357
|
-
if (!isPlainObject$
|
|
56444
|
+
if (!isPlainObject$2(tab)) {
|
|
56358
56445
|
errors.push(`${tabPath} must be an object`);
|
|
56359
56446
|
return
|
|
56360
56447
|
}
|
|
@@ -56405,7 +56492,7 @@ function validateDetailTabsArray(tabs, tabsPath, errors) {
|
|
|
56405
56492
|
function validatePageSidebar(page, pageIndex, errors) {
|
|
56406
56493
|
if (page.sidebar === undefined) return
|
|
56407
56494
|
const path = `/pages/${pageIndex}/sidebar`;
|
|
56408
|
-
if (!isPlainObject$
|
|
56495
|
+
if (!isPlainObject$2(page.sidebar)) {
|
|
56409
56496
|
errors.push(`${path} must be an object`);
|
|
56410
56497
|
return
|
|
56411
56498
|
}
|
|
@@ -56443,7 +56530,7 @@ function validateColumnsArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
56443
56530
|
// Legacy shorthand — accepted as-is.
|
|
56444
56531
|
return
|
|
56445
56532
|
}
|
|
56446
|
-
if (!isPlainObject$
|
|
56533
|
+
if (!isPlainObject$2(col)) {
|
|
56447
56534
|
errors.push(`${colPath}: must be a string (legacy shorthand) or object`);
|
|
56448
56535
|
return
|
|
56449
56536
|
}
|
|
@@ -56475,7 +56562,7 @@ function validateActionsArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
56475
56562
|
}
|
|
56476
56563
|
cfg.actions.forEach((action, aIndex) => {
|
|
56477
56564
|
const actionPath = `${pathSlash}/actions/${aIndex}`;
|
|
56478
|
-
if (!isPlainObject$
|
|
56565
|
+
if (!isPlainObject$2(action)) {
|
|
56479
56566
|
errors.push(`${actionPath}: must be an object`);
|
|
56480
56567
|
return
|
|
56481
56568
|
}
|
|
@@ -56532,7 +56619,7 @@ function validateWidgetsArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
56532
56619
|
}
|
|
56533
56620
|
cfg.widgets.forEach((widget, wIndex) => {
|
|
56534
56621
|
const widgetPath = `${pathSlash}/widgets/${wIndex}`;
|
|
56535
|
-
if (!isPlainObject$
|
|
56622
|
+
if (!isPlainObject$2(widget)) {
|
|
56536
56623
|
errors.push(`${widgetPath}: must be an object`);
|
|
56537
56624
|
return
|
|
56538
56625
|
}
|
|
@@ -56567,7 +56654,7 @@ function validateLayoutArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
56567
56654
|
}
|
|
56568
56655
|
cfg.layout.forEach((item, lIndex) => {
|
|
56569
56656
|
const layoutPath = `${pathSlash}/layout/${lIndex}`;
|
|
56570
|
-
if (!isPlainObject$
|
|
56657
|
+
if (!isPlainObject$2(item)) {
|
|
56571
56658
|
errors.push(`${layoutPath}: must be an object`);
|
|
56572
56659
|
return
|
|
56573
56660
|
}
|
|
@@ -56591,6 +56678,82 @@ function validateLayoutArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
56591
56678
|
});
|
|
56592
56679
|
}
|
|
56593
56680
|
|
|
56681
|
+
/**
|
|
56682
|
+
* Validate a single `sections[]` entry for `type:"settings"` pages.
|
|
56683
|
+
* Shared between the flat `pages[].config.sections[]` path AND the
|
|
56684
|
+
* tab-nested `pages[].config.tabs[].sections[]` path
|
|
56685
|
+
* (`manifest-settings-orchestration` REQ-MSO-4).
|
|
56686
|
+
*
|
|
56687
|
+
* Enforces the rich-sections REQ-MSRS-1 mutex (`fields | component |
|
|
56688
|
+
* widgets` exactly-one-of) plus per-widget shape rules. The new
|
|
56689
|
+
* `widget.type === "component"` discriminator (REQ-MSO-6) requires
|
|
56690
|
+
* `componentName: <non-empty string>`.
|
|
56691
|
+
*
|
|
56692
|
+
* @param {*} section The section under validation
|
|
56693
|
+
* @param {string} pathSlash JSON-pointer-style path prefix for errors
|
|
56694
|
+
* @param {string} pathBracket Human-readable bracket-path for errors
|
|
56695
|
+
* @param {string[]} errors Accumulator
|
|
56696
|
+
*/
|
|
56697
|
+
function validateSettingsSection(section, pathSlash, pathBracket, errors) {
|
|
56698
|
+
if (!isPlainObject$2(section)) {
|
|
56699
|
+
errors.push(`${pathSlash}: must be an object`);
|
|
56700
|
+
return
|
|
56701
|
+
}
|
|
56702
|
+
if (typeof section.title !== 'string') {
|
|
56703
|
+
errors.push(`${pathSlash}/title: required, must be a string`);
|
|
56704
|
+
}
|
|
56705
|
+
|
|
56706
|
+
// `manifest-settings-rich-sections` REQ-MSRS-1: exactly one of
|
|
56707
|
+
// fields | component | widgets.
|
|
56708
|
+
const hasFields = Array.isArray(section.fields);
|
|
56709
|
+
const hasComponent = typeof section.component === 'string' && section.component.length > 0;
|
|
56710
|
+
const hasWidgets = Array.isArray(section.widgets) && section.widgets.length > 0;
|
|
56711
|
+
const bodyCount = (hasFields ? 1 : 0) + (hasComponent ? 1 : 0) + (hasWidgets ? 1 : 0);
|
|
56712
|
+
|
|
56713
|
+
if (bodyCount !== 1) {
|
|
56714
|
+
errors.push(`${pathSlash}: ${pathBracket}: must declare exactly one of fields | component | widgets`);
|
|
56715
|
+
}
|
|
56716
|
+
|
|
56717
|
+
// `widgets` set but not an array (string / object / etc.)
|
|
56718
|
+
if (section.widgets !== undefined && !Array.isArray(section.widgets)) {
|
|
56719
|
+
errors.push(`${pathSlash}/widgets: must be an array when set`);
|
|
56720
|
+
}
|
|
56721
|
+
|
|
56722
|
+
// `component` set but not a string.
|
|
56723
|
+
if (section.component !== undefined && typeof section.component !== 'string') {
|
|
56724
|
+
errors.push(`${pathSlash}/component: must be a string when set`);
|
|
56725
|
+
}
|
|
56726
|
+
|
|
56727
|
+
// Per-widget shape rules.
|
|
56728
|
+
if (hasWidgets) {
|
|
56729
|
+
section.widgets.forEach((widget, wIndex) => {
|
|
56730
|
+
if (!isPlainObject$2(widget)) {
|
|
56731
|
+
errors.push(`${pathSlash}/widgets/${wIndex}: must be an object`);
|
|
56732
|
+
return
|
|
56733
|
+
}
|
|
56734
|
+
if (typeof widget.type !== 'string' || widget.type.length === 0) {
|
|
56735
|
+
errors.push(`${pathSlash}/widgets/${wIndex}/type: must be a non-empty string`);
|
|
56736
|
+
return
|
|
56737
|
+
}
|
|
56738
|
+
// `manifest-settings-orchestration` REQ-MSO-6: when the
|
|
56739
|
+
// discriminator is "component", `componentName` MUST be a
|
|
56740
|
+
// non-empty string. Other widget types ignore
|
|
56741
|
+
// `componentName`.
|
|
56742
|
+
if (widget.type === 'component') {
|
|
56743
|
+
if (typeof widget.componentName !== 'string' || widget.componentName.length === 0) {
|
|
56744
|
+
errors.push(`${pathSlash}/widgets/${wIndex}/componentName: required when type is "component", must be a non-empty string`);
|
|
56745
|
+
}
|
|
56746
|
+
}
|
|
56747
|
+
});
|
|
56748
|
+
}
|
|
56749
|
+
|
|
56750
|
+
// `manifest-config-refs` REQ-MCR — when fields[] body is used,
|
|
56751
|
+
// each entry must match the formField $def shape.
|
|
56752
|
+
if (hasFields) {
|
|
56753
|
+
validateFieldsArray(section.fields, `${pathSlash}/fields`, errors);
|
|
56754
|
+
}
|
|
56755
|
+
}
|
|
56756
|
+
|
|
56594
56757
|
/**
|
|
56595
56758
|
* Validate `config.sections[].fields[]` for settings page type
|
|
56596
56759
|
* (`manifest-config-refs` REQ-MCR). Each field MUST be an object with
|
|
@@ -56607,7 +56770,7 @@ function validateFieldsArray(fields, fieldsPath, errors) {
|
|
|
56607
56770
|
if (!Array.isArray(fields)) return
|
|
56608
56771
|
fields.forEach((field, fIndex) => {
|
|
56609
56772
|
const fieldPath = `${fieldsPath}/${fIndex}`;
|
|
56610
|
-
if (!isPlainObject$
|
|
56773
|
+
if (!isPlainObject$2(field)) {
|
|
56611
56774
|
errors.push(`${fieldPath}: must be an object`);
|
|
56612
56775
|
return
|
|
56613
56776
|
}
|
|
@@ -56626,17 +56789,292 @@ function validateFieldsArray(fields, fieldsPath, errors) {
|
|
|
56626
56789
|
}
|
|
56627
56790
|
|
|
56628
56791
|
/**
|
|
56629
|
-
*
|
|
56792
|
+
* Manifest `@resolve:` sentinel resolver.
|
|
56793
|
+
*
|
|
56794
|
+
* Implements the `manifest-resolve-sentinel` capability: walks the
|
|
56795
|
+
* `pages[].config` subtrees of an assembled manifest and replaces every
|
|
56796
|
+
* fully-matched `@resolve:<key>` string with the result of the consuming
|
|
56797
|
+
* app's `IAppConfig` lookup for `<key>`. Other manifest paths
|
|
56798
|
+
* (`route`, `id`, top-level fields, `menu[]`, `pages[].component` etc.)
|
|
56799
|
+
* are intentionally untouched — sentinels there are router / registry
|
|
56800
|
+
* invariants and the schema validator rejects them.
|
|
56801
|
+
*
|
|
56802
|
+
* Resolution source (canonical, per spec):
|
|
56803
|
+
*
|
|
56804
|
+
* 1. `@nextcloud/initial-state` provisioned key
|
|
56805
|
+
* `app-{appId}-{key}` — zero-network, preferred.
|
|
56806
|
+
* 2. Runtime `GET /index.php/apps/{appId}/api/configs/{key}` — falls
|
|
56807
|
+
* through silently on 4xx / network error.
|
|
56808
|
+
* 3. `null` — unresolved.
|
|
56809
|
+
*
|
|
56810
|
+
* Empty-state semantics: an unset / empty value substitutes `null` (not
|
|
56811
|
+
* empty string) and is accumulated into the returned `unresolved` array
|
|
56812
|
+
* so consumers can render an admin warning. A `console.warn` is emitted
|
|
56813
|
+
* once per unresolved key with the offending sentinel.
|
|
56814
|
+
*
|
|
56815
|
+
* The resolver is intentionally split into a synchronous walk + an
|
|
56816
|
+
* asynchronous batch resolution: every sentinel is collected first, then
|
|
56817
|
+
* each unique `(appId, key)` is resolved exactly once (initial-state
|
|
56818
|
+
* lookup is synchronous; runtime fetch is per-key cached for the page
|
|
56819
|
+
* lifetime), and finally the manifest is rewritten in a second walk
|
|
56820
|
+
* with the resolved values. This makes the substitution deterministic
|
|
56821
|
+
* — five `@resolve:foo` references in five pages share one fetch.
|
|
56822
|
+
*
|
|
56823
|
+
* @module utils/resolveManifestSentinels
|
|
56824
|
+
*/
|
|
56825
|
+
|
|
56826
|
+
const SENTINEL_PATTERN = /^@resolve:([a-z][a-z0-9_-]*)$/;
|
|
56827
|
+
|
|
56828
|
+
/**
|
|
56829
|
+
* Process-wide cache of resolved IAppConfig values, keyed by
|
|
56830
|
+
* `${appId}::${key}`. Cleared via `clearResolveCache()` (test-only).
|
|
56831
|
+
*
|
|
56832
|
+
* @type {Map<string, Promise<*>>}
|
|
56833
|
+
*/
|
|
56834
|
+
const resolveCache = new Map();
|
|
56835
|
+
|
|
56836
|
+
/**
|
|
56837
|
+
* Test-only helper to reset the per-page resolve cache between runs.
|
|
56838
|
+
* Production callers do not need this — the cache is page-lifetime by
|
|
56839
|
+
* design, consistent with the manifest's load-once model.
|
|
56840
|
+
*
|
|
56841
|
+
* @return {void}
|
|
56842
|
+
*/
|
|
56843
|
+
function clearResolveCache() {
|
|
56844
|
+
resolveCache.clear();
|
|
56845
|
+
}
|
|
56846
|
+
|
|
56847
|
+
/**
|
|
56848
|
+
* Walk the manifest's `pages[].config` subtrees and replace every
|
|
56849
|
+
* `@resolve:<key>` sentinel with the resolved IAppConfig value.
|
|
56850
|
+
*
|
|
56851
|
+
* Returns a Promise resolving to `{ manifest, unresolved }`:
|
|
56852
|
+
* - `manifest` — a NEW manifest object with sentinels substituted; the
|
|
56853
|
+
* input is NOT mutated.
|
|
56854
|
+
* - `unresolved` — array of IAppConfig keys whose sentinels resolved
|
|
56855
|
+
* to `null` (unset / empty / fetch failure). Useful for surfacing
|
|
56856
|
+
* "n settings unconfigured" admin warnings.
|
|
56857
|
+
*
|
|
56858
|
+
* Sentinels OUTSIDE `pages[].config` are left intact; the schema
|
|
56859
|
+
* validator rejects them downstream so consumers see a clear error
|
|
56860
|
+
* rather than a silent substitution that breaks routing or registry
|
|
56861
|
+
* lookups.
|
|
56862
|
+
*
|
|
56863
|
+
* @param {object} manifest The merged (bundled + backend) manifest.
|
|
56864
|
+
* Walked but not mutated.
|
|
56865
|
+
* @param {string} appId Nextcloud app ID. Used to scope the
|
|
56866
|
+
* IAppConfig lookup namespace.
|
|
56867
|
+
* @param {object} [options] Resolver overrides.
|
|
56868
|
+
* @param {Function} [options.getAppConfigValue] Async (appId, key) =>
|
|
56869
|
+
* value resolver. Override for tests; defaults to the
|
|
56870
|
+
* initial-state-then-fetch chain documented above.
|
|
56871
|
+
* @param {Function} [options.warn] Override for `console.warn`. Used in
|
|
56872
|
+
* tests to capture warning calls without polluting test output.
|
|
56873
|
+
* @return {Promise<{ manifest: object, unresolved: string[] }>}
|
|
56874
|
+
*/
|
|
56875
|
+
async function resolveManifestSentinels(manifest, appId, options = {}) {
|
|
56876
|
+
if (!isPlainObject$1(manifest)) {
|
|
56877
|
+
return { manifest, unresolved: [] }
|
|
56878
|
+
}
|
|
56879
|
+
|
|
56880
|
+
const getAppConfigValue = options.getAppConfigValue ?? defaultGetAppConfigValue;
|
|
56881
|
+
const warn = options.warn ?? ((...args) => {
|
|
56882
|
+
// eslint-disable-next-line no-console
|
|
56883
|
+
console.warn(...args);
|
|
56884
|
+
});
|
|
56885
|
+
|
|
56886
|
+
// Phase 1: scan for sentinels under pages[].config only. We only
|
|
56887
|
+
// need the unique key set — the second walk does the substitution.
|
|
56888
|
+
const keys = new Set();
|
|
56889
|
+
const pages = Array.isArray(manifest.pages) ? manifest.pages : [];
|
|
56890
|
+
for (const page of pages) {
|
|
56891
|
+
if (!isPlainObject$1(page) || !isPlainObject$1(page.config)) continue
|
|
56892
|
+
collectSentinelKeys(page.config, keys);
|
|
56893
|
+
}
|
|
56894
|
+
|
|
56895
|
+
if (keys.size === 0) {
|
|
56896
|
+
return { manifest, unresolved: [] }
|
|
56897
|
+
}
|
|
56898
|
+
|
|
56899
|
+
// Phase 2: resolve each unique (appId, key) exactly once.
|
|
56900
|
+
const resolved = new Map();
|
|
56901
|
+
const unresolved = [];
|
|
56902
|
+
await Promise.all(Array.from(keys).map(async (key) => {
|
|
56903
|
+
const value = await getAppConfigValue(appId, key);
|
|
56904
|
+
if (value === undefined || value === null || value === '') {
|
|
56905
|
+
resolved.set(key, null);
|
|
56906
|
+
unresolved.push(key);
|
|
56907
|
+
warn(`[resolveManifestSentinels] Manifest sentinel '@resolve:${key}' resolved to null (key unset)`);
|
|
56908
|
+
} else {
|
|
56909
|
+
resolved.set(key, value);
|
|
56910
|
+
}
|
|
56911
|
+
}));
|
|
56912
|
+
|
|
56913
|
+
// Phase 3: rebuild the manifest immutably, substituting sentinels in
|
|
56914
|
+
// pages[].config only. Other fields are passed through by reference.
|
|
56915
|
+
const out = { ...manifest };
|
|
56916
|
+
out.pages = pages.map((page) => {
|
|
56917
|
+
if (!isPlainObject$1(page) || !isPlainObject$1(page.config)) return page
|
|
56918
|
+
return { ...page, config: substituteInTree(page.config, resolved) }
|
|
56919
|
+
});
|
|
56920
|
+
|
|
56921
|
+
return { manifest: out, unresolved }
|
|
56922
|
+
}
|
|
56923
|
+
|
|
56924
|
+
/**
|
|
56925
|
+
* Recursively scan a tree, accumulating every sentinel key into the
|
|
56926
|
+
* provided `keys` Set. Plain objects + arrays are descended; primitive
|
|
56927
|
+
* leaves are checked against the sentinel pattern.
|
|
56928
|
+
*
|
|
56929
|
+
* @param {*} node Current tree node (object, array, or primitive).
|
|
56930
|
+
* @param {Set<string>} keys Accumulator for unique sentinel keys.
|
|
56931
|
+
* @return {void}
|
|
56932
|
+
*/
|
|
56933
|
+
function collectSentinelKeys(node, keys) {
|
|
56934
|
+
if (typeof node === 'string') {
|
|
56935
|
+
const match = node.match(SENTINEL_PATTERN);
|
|
56936
|
+
if (match) keys.add(match[1]);
|
|
56937
|
+
return
|
|
56938
|
+
}
|
|
56939
|
+
if (Array.isArray(node)) {
|
|
56940
|
+
for (const item of node) collectSentinelKeys(item, keys);
|
|
56941
|
+
return
|
|
56942
|
+
}
|
|
56943
|
+
if (isPlainObject$1(node)) {
|
|
56944
|
+
for (const value of Object.values(node)) collectSentinelKeys(value, keys);
|
|
56945
|
+
}
|
|
56946
|
+
}
|
|
56947
|
+
|
|
56948
|
+
/**
|
|
56949
|
+
* Recursively rebuild a tree, replacing each fully-matched sentinel
|
|
56950
|
+
* with its resolved value. Returns a NEW tree; input is unchanged.
|
|
56951
|
+
*
|
|
56952
|
+
* @param {*} node Current tree node.
|
|
56953
|
+
* @param {Map<string,*>} resolved Map of key → resolved value (or null).
|
|
56954
|
+
* @return {*} New tree with sentinels substituted.
|
|
56955
|
+
*/
|
|
56956
|
+
function substituteInTree(node, resolved) {
|
|
56957
|
+
if (typeof node === 'string') {
|
|
56958
|
+
const match = node.match(SENTINEL_PATTERN);
|
|
56959
|
+
if (match && resolved.has(match[1])) {
|
|
56960
|
+
return resolved.get(match[1])
|
|
56961
|
+
}
|
|
56962
|
+
return node
|
|
56963
|
+
}
|
|
56964
|
+
if (Array.isArray(node)) {
|
|
56965
|
+
return node.map((item) => substituteInTree(item, resolved))
|
|
56966
|
+
}
|
|
56967
|
+
if (isPlainObject$1(node)) {
|
|
56968
|
+
const out = {};
|
|
56969
|
+
for (const [key, value] of Object.entries(node)) {
|
|
56970
|
+
out[key] = substituteInTree(value, resolved);
|
|
56971
|
+
}
|
|
56972
|
+
return out
|
|
56973
|
+
}
|
|
56974
|
+
return node
|
|
56975
|
+
}
|
|
56976
|
+
|
|
56977
|
+
/**
|
|
56978
|
+
* Default `getAppConfigValue` implementation: consult
|
|
56979
|
+
* `@nextcloud/initial-state` first (zero-network), fall back to a
|
|
56980
|
+
* runtime fetch with per-(appId, key) caching for the page lifetime.
|
|
56630
56981
|
*
|
|
56631
|
-
*
|
|
56632
|
-
*
|
|
56982
|
+
* Returns `null` when neither source resolves a value. Network / 4xx
|
|
56983
|
+
* errors are swallowed silently — the resolver downgrades to "key
|
|
56984
|
+
* unset" in that case (consistent with the silent-fallback pattern in
|
|
56985
|
+
* `useAppManifest`'s backend-merge step).
|
|
56986
|
+
*
|
|
56987
|
+
* @param {string} appId Nextcloud app ID.
|
|
56988
|
+
* @param {string} key IAppConfig key (already validated as
|
|
56989
|
+
* lowercase + alphanumeric + `_-` by the sentinel regex).
|
|
56990
|
+
* @return {Promise<*>} Resolved value or `null` when unset.
|
|
56991
|
+
*/
|
|
56992
|
+
async function defaultGetAppConfigValue(appId, key) {
|
|
56993
|
+
const cacheKey = `${appId}::${key}`;
|
|
56994
|
+
if (resolveCache.has(cacheKey)) {
|
|
56995
|
+
return resolveCache.get(cacheKey)
|
|
56996
|
+
}
|
|
56997
|
+
const promise = (async () => {
|
|
56998
|
+
// Step 1: initial-state — synchronous, zero-network.
|
|
56999
|
+
const initial = readInitialState(appId, key);
|
|
57000
|
+
if (initial !== undefined && initial !== null && initial !== '') {
|
|
57001
|
+
return initial
|
|
57002
|
+
}
|
|
57003
|
+
// Step 2: runtime fetch — silent fallback on any error.
|
|
57004
|
+
try {
|
|
57005
|
+
const { default: axios } = await import('@nextcloud/axios');
|
|
57006
|
+
const { generateUrl } = await import('@nextcloud/router');
|
|
57007
|
+
const url = generateUrl(`/apps/${appId}/api/configs/${key}`);
|
|
57008
|
+
const response = await axios.get(url);
|
|
57009
|
+
if (response && response.status === 200 && response.data !== undefined) {
|
|
57010
|
+
const data = response.data;
|
|
57011
|
+
// API may return either a raw scalar or `{ value: ... }`.
|
|
57012
|
+
if (isPlainObject$1(data) && 'value' in data) return data.value
|
|
57013
|
+
return data
|
|
57014
|
+
}
|
|
57015
|
+
} catch (e) {
|
|
57016
|
+
// Silent — caller treats as "unset".
|
|
57017
|
+
}
|
|
57018
|
+
return null
|
|
57019
|
+
})();
|
|
57020
|
+
resolveCache.set(cacheKey, promise);
|
|
57021
|
+
return promise
|
|
57022
|
+
}
|
|
57023
|
+
|
|
57024
|
+
/**
|
|
57025
|
+
* Read a key from `@nextcloud/initial-state`. Looks up the conventional
|
|
57026
|
+
* `app-{appId}-{key}` slot. Returns `undefined` when the package is not
|
|
57027
|
+
* installed (e.g. older host) or the key is not provisioned.
|
|
57028
|
+
*
|
|
57029
|
+
* @param {string} appId Nextcloud app ID.
|
|
57030
|
+
* @param {string} key IAppConfig key.
|
|
57031
|
+
* @return {*} Provisioned value or `undefined`.
|
|
57032
|
+
*/
|
|
57033
|
+
function readInitialState(appId, key) {
|
|
57034
|
+
try {
|
|
57035
|
+
// `@nextcloud/initial-state` is an optional peer; the host page
|
|
57036
|
+
// may not provision the slot at all. We resolve via require so
|
|
57037
|
+
// jest mocks the import; bundle-side, the package is treeshaken
|
|
57038
|
+
// when no caller pulls it in.
|
|
57039
|
+
// eslint-disable-next-line global-require, import/no-unresolved, n/no-extraneous-require
|
|
57040
|
+
const mod = require('@nextcloud/initial-state');
|
|
57041
|
+
if (typeof mod.loadState === 'function') {
|
|
57042
|
+
return mod.loadState(appId, key, undefined)
|
|
57043
|
+
}
|
|
57044
|
+
} catch (e) {
|
|
57045
|
+
// Package not installed or no slot provisioned — fall through.
|
|
57046
|
+
}
|
|
57047
|
+
return undefined
|
|
57048
|
+
}
|
|
57049
|
+
|
|
57050
|
+
/**
|
|
57051
|
+
* Type guard — true when value is a plain (non-array, non-null) object.
|
|
57052
|
+
*
|
|
57053
|
+
* @param {*} value Candidate.
|
|
57054
|
+
* @return {boolean} True when value is a plain object.
|
|
57055
|
+
*/
|
|
57056
|
+
function isPlainObject$1(value) {
|
|
57057
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
57058
|
+
}
|
|
57059
|
+
|
|
57060
|
+
/**
|
|
57061
|
+
* Composable that loads, resolves, and validates a Conduction app manifest.
|
|
57062
|
+
*
|
|
57063
|
+
* The composable implements the four-phase flow specified in
|
|
57064
|
+
* REQ-JMR-002 of the json-manifest-renderer capability + the
|
|
57065
|
+
* substitution step from the `manifest-resolve-sentinel` capability:
|
|
56633
57066
|
*
|
|
56634
57067
|
* 1. Synchronous bundled load — `bundledManifest` is the immediate value.
|
|
56635
57068
|
* 2. Async backend merge — fetches `/index.php/apps/{appId}/api/manifest`
|
|
56636
57069
|
* and deep-merges any 200 response over the bundled manifest. 4xx /
|
|
56637
57070
|
* 5xx / network errors are silently ignored so apps work without a
|
|
56638
57071
|
* backend endpoint.
|
|
56639
|
-
* 3.
|
|
57072
|
+
* 3. Sentinel resolution — `@resolve:<key>` strings under
|
|
57073
|
+
* `pages[].config` are substituted with `IAppConfig` values via the
|
|
57074
|
+
* `resolveManifestSentinels` utility (see its module docs for the
|
|
57075
|
+
* resolution source chain). Unresolved keys surface on the
|
|
57076
|
+
* returned `unresolvedSentinels` ref.
|
|
57077
|
+
* 4. Validation — the resolved result is validated against
|
|
56640
57078
|
* `app-manifest.schema.json`. On failure, the bundled manifest is
|
|
56641
57079
|
* kept and a `console.warn` is emitted with the error list.
|
|
56642
57080
|
*
|
|
@@ -56644,7 +57082,8 @@ function validateFieldsArray(fields, fieldsPath, errors) {
|
|
|
56644
57082
|
* can hot-swap the manifest without a page reload.
|
|
56645
57083
|
*
|
|
56646
57084
|
* @param {string} appId Nextcloud app ID. Used to build the default
|
|
56647
|
-
* backend endpoint URL via `@nextcloud/router
|
|
57085
|
+
* backend endpoint URL via `@nextcloud/router` and to scope
|
|
57086
|
+
* IAppConfig lookups for `@resolve:<key>` sentinels.
|
|
56648
57087
|
* @param {object} bundledManifest The manifest shipped with the app (the
|
|
56649
57088
|
* default value, available synchronously).
|
|
56650
57089
|
* @param {object} [options] Configuration options.
|
|
@@ -56654,7 +57093,10 @@ function validateFieldsArray(fields, fieldsPath, errors) {
|
|
|
56654
57093
|
* return a promise resolving to `{ status: number, data: object }`.
|
|
56655
57094
|
* Defaults to `axios.get` from `@nextcloud/axios` (which inherits the
|
|
56656
57095
|
* Nextcloud CSRF token automatically).
|
|
56657
|
-
* @
|
|
57096
|
+
* @param {Function} [options.getAppConfigValue] Override the
|
|
57097
|
+
* IAppConfig resolver consumed by `resolveManifestSentinels`. Useful
|
|
57098
|
+
* for tests that want to mount a fixture-driven config map.
|
|
57099
|
+
* @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}
|
|
56658
57100
|
*
|
|
56659
57101
|
* @example Basic usage (Composition API)
|
|
56660
57102
|
* const { manifest, isLoading } = useAppManifest('decidesk', bundled)
|
|
@@ -56671,11 +57113,17 @@ function validateFieldsArray(fields, fieldsPath, errors) {
|
|
|
56671
57113
|
* endpoint: '/custom/manifest/url',
|
|
56672
57114
|
* fetcher: (url) => Promise.resolve({ status: 200, data: { ... } }),
|
|
56673
57115
|
* })
|
|
57116
|
+
*
|
|
57117
|
+
* @example With sentinel resolution + admin warning surface
|
|
57118
|
+
* const { manifest, unresolvedSentinels } = useAppManifest('softwarecatalog', bundled)
|
|
57119
|
+
* // unresolvedSentinels.value is e.g. ['voorzieningen_register']
|
|
57120
|
+
* // when that IAppConfig key is unset on the tenant.
|
|
56674
57121
|
*/
|
|
56675
57122
|
function useAppManifest(appId, bundledManifest, options = {}) {
|
|
56676
57123
|
const manifest = Vue.ref(bundledManifest);
|
|
56677
57124
|
const isLoading = Vue.ref(true);
|
|
56678
57125
|
const validationErrors = Vue.ref(null);
|
|
57126
|
+
const unresolvedSentinels = Vue.ref([]);
|
|
56679
57127
|
|
|
56680
57128
|
const endpoint = options.endpoint ?? router.generateUrl(`/apps/${appId}/api/manifest`);
|
|
56681
57129
|
const fetcher = options.fetcher ?? ((url) => axios.get(url))
|
|
@@ -56687,7 +57135,18 @@ function useAppManifest(appId, bundledManifest, options = {}) {
|
|
|
56687
57135
|
return
|
|
56688
57136
|
}
|
|
56689
57137
|
const merged = deepMerge(bundledManifest, response.data);
|
|
56690
|
-
|
|
57138
|
+
|
|
57139
|
+
// Sentinel resolution runs BEFORE validation per
|
|
57140
|
+
// REQ-MRS-002: the validator MUST NEVER observe an
|
|
57141
|
+
// unresolved sentinel at runtime. Resolution failures
|
|
57142
|
+
// (unset IAppConfig keys) substitute null and accumulate
|
|
57143
|
+
// on `unresolvedSentinels`; they do NOT block validation.
|
|
57144
|
+
const { manifest: resolved, unresolved } = await resolveManifestSentinels(merged, appId, {
|
|
57145
|
+
getAppConfigValue: options.getAppConfigValue,
|
|
57146
|
+
});
|
|
57147
|
+
unresolvedSentinels.value = unresolved;
|
|
57148
|
+
|
|
57149
|
+
const result = validateManifest(resolved);
|
|
56691
57150
|
if (!result.valid) {
|
|
56692
57151
|
validationErrors.value = result.errors;
|
|
56693
57152
|
// eslint-disable-next-line no-console
|
|
@@ -56697,7 +57156,7 @@ function useAppManifest(appId, bundledManifest, options = {}) {
|
|
|
56697
57156
|
);
|
|
56698
57157
|
return
|
|
56699
57158
|
}
|
|
56700
|
-
manifest.value =
|
|
57159
|
+
manifest.value = resolved;
|
|
56701
57160
|
} catch (err) {
|
|
56702
57161
|
// Silent fallback on 404, network errors, non-200 responses.
|
|
56703
57162
|
// Apps without a backend endpoint should keep working.
|
|
@@ -56706,7 +57165,7 @@ function useAppManifest(appId, bundledManifest, options = {}) {
|
|
|
56706
57165
|
}
|
|
56707
57166
|
})();
|
|
56708
57167
|
|
|
56709
|
-
return { manifest, isLoading, validationErrors }
|
|
57168
|
+
return { manifest, isLoading, validationErrors, unresolvedSentinels }
|
|
56710
57169
|
}
|
|
56711
57170
|
|
|
56712
57171
|
/**
|
|
@@ -107628,7 +108087,45 @@ var CnSettingsWidgetMount = {
|
|
|
107628
108087
|
//
|
|
107629
108088
|
//
|
|
107630
108089
|
//
|
|
108090
|
+
//
|
|
108091
|
+
//
|
|
108092
|
+
//
|
|
108093
|
+
//
|
|
108094
|
+
//
|
|
108095
|
+
//
|
|
108096
|
+
//
|
|
108097
|
+
//
|
|
108098
|
+
//
|
|
108099
|
+
//
|
|
108100
|
+
//
|
|
108101
|
+
//
|
|
108102
|
+
//
|
|
108103
|
+
//
|
|
108104
|
+
//
|
|
108105
|
+
//
|
|
108106
|
+
//
|
|
108107
|
+
//
|
|
108108
|
+
//
|
|
108109
|
+
//
|
|
108110
|
+
//
|
|
108111
|
+
//
|
|
108112
|
+
//
|
|
108113
|
+
//
|
|
108114
|
+
//
|
|
108115
|
+
//
|
|
108116
|
+
//
|
|
108117
|
+
//
|
|
108118
|
+
//
|
|
108119
|
+
|
|
107631
108120
|
|
|
108121
|
+
/**
|
|
108122
|
+
* Sentinel value used in the built-in widget registry to mark the
|
|
108123
|
+
* `'component'` discriminator (manifest-settings-orchestration
|
|
108124
|
+
* REQ-MSO-6). The discriminator does NOT resolve to a fixed
|
|
108125
|
+
* component — instead, the resolver detects this sentinel and looks
|
|
108126
|
+
* up `widget.componentName` in the customComponents registry.
|
|
108127
|
+
*/
|
|
108128
|
+
const COMPONENT_DISCRIMINATOR = Symbol('cn-settings-component-widget');
|
|
107632
108129
|
|
|
107633
108130
|
/**
|
|
107634
108131
|
* Built-in widget registry. Used by `CnSettingsPage` to resolve
|
|
@@ -107638,13 +108135,19 @@ var CnSettingsWidgetMount = {
|
|
|
107638
108135
|
* The order matters — built-ins win on collision so consumers can't
|
|
107639
108136
|
* accidentally shadow `version-info` with their own component. If a
|
|
107640
108137
|
* consumer needs to truly replace one of these, they can render their
|
|
107641
|
-
* own component via `section.component`
|
|
108138
|
+
* own component via `section.component` or
|
|
108139
|
+
* `{ type: "component", componentName: <name> }` instead of `widgets[]`.
|
|
107642
108140
|
*
|
|
107643
|
-
* Spec:
|
|
108141
|
+
* Spec:
|
|
108142
|
+
* - REQ-MSRS-2 (manifest-settings-rich-sections) — fixed-component
|
|
108143
|
+
* built-ins (`version-info`, `register-mapping`).
|
|
108144
|
+
* - REQ-MSO-6 (manifest-settings-orchestration) — `'component'`
|
|
108145
|
+
* discriminator (sentinel value, resolved via componentName).
|
|
107644
108146
|
*/
|
|
107645
108147
|
const BUILTIN_SETTINGS_WIDGETS = Object.freeze({
|
|
107646
108148
|
'version-info': __vue_component__$17,
|
|
107647
108149
|
'register-mapping': __vue_component__$I,
|
|
108150
|
+
component: COMPONENT_DISCRIMINATOR,
|
|
107648
108151
|
});
|
|
107649
108152
|
|
|
107650
108153
|
/**
|
|
@@ -107752,19 +108255,50 @@ var script$8 = {
|
|
|
107752
108255
|
default: '',
|
|
107753
108256
|
},
|
|
107754
108257
|
/**
|
|
107755
|
-
* Section definitions
|
|
108258
|
+
* Section definitions (flat shape — back-compat). Each section
|
|
108259
|
+
* MUST declare EXACTLY ONE of:
|
|
107756
108260
|
* - `fields: Array<Field>` (back-compat flat-field body)
|
|
107757
108261
|
* - `component: <registry-name>` + optional `props`
|
|
107758
|
-
* - `widgets: Array<{ type, props? }>`
|
|
108262
|
+
* - `widgets: Array<{ type, props?, componentName? }>`
|
|
107759
108263
|
*
|
|
107760
108264
|
* Common keys: `{ title, description?, icon?, collapsible?, docUrl? }`.
|
|
107761
108265
|
*
|
|
108266
|
+
* Mutually exclusive with `tabs[]` (XOR — see
|
|
108267
|
+
* manifest-settings-orchestration REQ-MSO-1).
|
|
108268
|
+
*
|
|
107762
108269
|
* @type {Array<object>}
|
|
107763
108270
|
*/
|
|
107764
108271
|
sections: {
|
|
107765
108272
|
type: Array,
|
|
107766
108273
|
default: () => [],
|
|
107767
108274
|
},
|
|
108275
|
+
/**
|
|
108276
|
+
* Tab definitions (orchestration shape — manifest-settings-
|
|
108277
|
+
* orchestration REQ-MSO-2). When set, CnSettingsPage renders
|
|
108278
|
+
* a tab strip above the section area; the active tab's
|
|
108279
|
+
* `sections[]` flow into the same renderer used by the flat
|
|
108280
|
+
* shape. Mutually exclusive with `sections[]`.
|
|
108281
|
+
*
|
|
108282
|
+
* Each tab MUST be `{ id: string, label: string,
|
|
108283
|
+
* icon?: string, sections: array<Section> }`.
|
|
108284
|
+
*
|
|
108285
|
+
* @type {Array<object>}
|
|
108286
|
+
*/
|
|
108287
|
+
tabs: {
|
|
108288
|
+
type: Array,
|
|
108289
|
+
default: () => [],
|
|
108290
|
+
},
|
|
108291
|
+
/**
|
|
108292
|
+
* Optional ID of the tab to activate on mount. When empty AND
|
|
108293
|
+
* `tabs[]` is non-empty, the first tab is active by default.
|
|
108294
|
+
* Unknown IDs fall back to the first tab.
|
|
108295
|
+
*
|
|
108296
|
+
* @type {string}
|
|
108297
|
+
*/
|
|
108298
|
+
initialTab: {
|
|
108299
|
+
type: String,
|
|
108300
|
+
default: '',
|
|
108301
|
+
},
|
|
107768
108302
|
/**
|
|
107769
108303
|
* Initial values keyed by `field.key`. Defaults to an empty
|
|
107770
108304
|
* object; in practice the consumer passes the current
|
|
@@ -107826,14 +108360,30 @@ var script$8 = {
|
|
|
107826
108360
|
},
|
|
107827
108361
|
},
|
|
107828
108362
|
|
|
107829
|
-
emits: ['save', 'error', 'input', 'widget-event'],
|
|
108363
|
+
emits: ['save', 'error', 'input', 'widget-event', 'tab-change'],
|
|
107830
108364
|
|
|
107831
108365
|
data() {
|
|
108366
|
+
// Resolve the initial active-tab id synchronously so the very
|
|
108367
|
+
// first render has the correct tab active (otherwise tests
|
|
108368
|
+
// that mount + assert without an `await tick` see the empty
|
|
108369
|
+
// default). Mirrors the logic in `resolveInitialTabId` (the
|
|
108370
|
+
// watcher path); keep them aligned.
|
|
108371
|
+
let activeTabId = '';
|
|
108372
|
+
const tabs = Array.isArray(this.tabs) ? this.tabs : [];
|
|
108373
|
+
if (tabs.length > 0) {
|
|
108374
|
+
if (typeof this.initialTab === 'string' && this.initialTab.length > 0
|
|
108375
|
+
&& tabs.some(t => t && t.id === this.initialTab)) {
|
|
108376
|
+
activeTabId = this.initialTab;
|
|
108377
|
+
} else if (tabs[0] && typeof tabs[0].id === 'string') {
|
|
108378
|
+
activeTabId = tabs[0].id;
|
|
108379
|
+
}
|
|
108380
|
+
}
|
|
107832
108381
|
return {
|
|
107833
108382
|
formData: this.cloneInitial(),
|
|
107834
108383
|
originalData: this.cloneInitial(),
|
|
107835
108384
|
saving: false,
|
|
107836
108385
|
lastError: null,
|
|
108386
|
+
activeTabId,
|
|
107837
108387
|
}
|
|
107838
108388
|
},
|
|
107839
108389
|
|
|
@@ -107852,6 +108402,35 @@ var script$8 = {
|
|
|
107852
108402
|
effectiveCustomComponents() {
|
|
107853
108403
|
return this.customComponents ?? this.cnCustomComponents ?? {}
|
|
107854
108404
|
},
|
|
108405
|
+
/**
|
|
108406
|
+
* Whether the page is in tabs orchestration mode. True when
|
|
108407
|
+
* `tabs[]` is non-empty — drives the tab-strip render gate
|
|
108408
|
+
* (manifest-settings-orchestration REQ-MSO-5).
|
|
108409
|
+
*
|
|
108410
|
+
* @return {boolean}
|
|
108411
|
+
*/
|
|
108412
|
+
hasTabs() {
|
|
108413
|
+
return Array.isArray(this.tabs) && this.tabs.length > 0
|
|
108414
|
+
},
|
|
108415
|
+
/**
|
|
108416
|
+
* The sections to render right now. In flat mode, this is the
|
|
108417
|
+
* `sections` prop directly. In tabs mode, this is the
|
|
108418
|
+
* `sections[]` array of the currently active tab. Centralising
|
|
108419
|
+
* this in one computed keeps the template's `v-for` simple
|
|
108420
|
+
* and decouples it from the body kind dispatcher (which
|
|
108421
|
+
* applies per-section, not per-mode).
|
|
108422
|
+
*
|
|
108423
|
+
* @return {Array<object>}
|
|
108424
|
+
*/
|
|
108425
|
+
activeSections() {
|
|
108426
|
+
if (!this.hasTabs) return this.sections || []
|
|
108427
|
+
const active = this.tabs.find(t => t && t.id === this.activeTabId);
|
|
108428
|
+
if (active && Array.isArray(active.sections)) return active.sections
|
|
108429
|
+
// Defensive fallback — should not happen because
|
|
108430
|
+
// `resolveInitialTabId` always lands on a known tab.
|
|
108431
|
+
const first = this.tabs[0];
|
|
108432
|
+
return first && Array.isArray(first.sections) ? first.sections : []
|
|
108433
|
+
},
|
|
107855
108434
|
},
|
|
107856
108435
|
|
|
107857
108436
|
watch: {
|
|
@@ -107862,6 +108441,22 @@ var script$8 = {
|
|
|
107862
108441
|
this.originalData = this.cloneInitial();
|
|
107863
108442
|
},
|
|
107864
108443
|
},
|
|
108444
|
+
// When `tabs[]` changes (e.g. consumer swaps manifests at
|
|
108445
|
+
// runtime), re-resolve the active tab so the page doesn't get
|
|
108446
|
+
// stuck on a removed id.
|
|
108447
|
+
tabs: {
|
|
108448
|
+
handler() {
|
|
108449
|
+
this.activeTabId = this.resolveInitialTabId();
|
|
108450
|
+
},
|
|
108451
|
+
},
|
|
108452
|
+
// When `initialTab` changes (consumer-controlled tab
|
|
108453
|
+
// activation), follow it.
|
|
108454
|
+
initialTab(next) {
|
|
108455
|
+
if (typeof next === 'string' && next.length > 0) {
|
|
108456
|
+
const exists = this.tabs.some(t => t && t.id === next);
|
|
108457
|
+
if (exists) this.activeTabId = next;
|
|
108458
|
+
}
|
|
108459
|
+
},
|
|
107865
108460
|
},
|
|
107866
108461
|
|
|
107867
108462
|
methods: {
|
|
@@ -107907,11 +108502,20 @@ var script$8 = {
|
|
|
107907
108502
|
},
|
|
107908
108503
|
cloneInitial() {
|
|
107909
108504
|
const merged = { ...(this.initialValues || {}) };
|
|
107910
|
-
//
|
|
107911
|
-
//
|
|
107912
|
-
//
|
|
107913
|
-
|
|
107914
|
-
|
|
108505
|
+
// Collect every section across both modes (flat
|
|
108506
|
+
// `sections[]` AND `tabs[].sections[]`) so default values
|
|
108507
|
+
// are applied regardless of orchestration shape.
|
|
108508
|
+
// Only flat-field sections contribute defaults; component
|
|
108509
|
+
// and widgets sections own their own state.
|
|
108510
|
+
const allSections = [];
|
|
108511
|
+
for (const section of this.sections || []) allSections.push(section);
|
|
108512
|
+
for (const tab of this.tabs || []) {
|
|
108513
|
+
if (tab && Array.isArray(tab.sections)) {
|
|
108514
|
+
for (const section of tab.sections) allSections.push(section);
|
|
108515
|
+
}
|
|
108516
|
+
}
|
|
108517
|
+
for (const section of allSections) {
|
|
108518
|
+
if (!section || !Array.isArray(section.fields)) continue
|
|
107915
108519
|
for (const field of section.fields) {
|
|
107916
108520
|
if (field.default !== undefined && merged[field.key] === undefined) {
|
|
107917
108521
|
merged[field.key] = field.default;
|
|
@@ -107977,23 +108581,55 @@ var script$8 = {
|
|
|
107977
108581
|
},
|
|
107978
108582
|
|
|
107979
108583
|
/**
|
|
107980
|
-
* Resolve a `widgets[]
|
|
107981
|
-
* order:
|
|
108584
|
+
* Resolve a single `widgets[]` entry to a concrete Vue
|
|
108585
|
+
* component. Lookup order:
|
|
107982
108586
|
*
|
|
107983
108587
|
* 1. Built-in widget map (`version-info`, `register-mapping`).
|
|
107984
|
-
* 2. `
|
|
108588
|
+
* 2. `'component'` discriminator (REQ-MSO-6) — resolves
|
|
108589
|
+
* `widget.componentName` against `effectiveCustomComponents`.
|
|
108590
|
+
* 3. Legacy fallback — looks up `widget.type` against
|
|
108591
|
+
* `effectiveCustomComponents`. Kept for back-compat with
|
|
108592
|
+
* manifest-settings-rich-sections consumers; flagged as
|
|
108593
|
+
* deprecated in JSDoc — manifest authors should migrate to
|
|
108594
|
+
* the explicit `{ type: "component", componentName }` shape.
|
|
107985
108595
|
*
|
|
107986
|
-
* Returns `null` (and warns) when
|
|
107987
|
-
* win on collision so consumers can't accidentally shadow them
|
|
107988
|
-
* (REQ-MSRS-2).
|
|
108596
|
+
* Returns `null` (and warns) when nothing resolves. Built-ins
|
|
108597
|
+
* win on collision so consumers can't accidentally shadow them.
|
|
107989
108598
|
*
|
|
107990
|
-
* @param {
|
|
108599
|
+
* @param {object} widget A `widgets[]` entry, e.g. `{ type, props?, componentName? }`.
|
|
107991
108600
|
* @return {object|null} Vue component or null.
|
|
107992
108601
|
*/
|
|
107993
|
-
resolveWidgetComponent(
|
|
108602
|
+
resolveWidgetComponent(widget) {
|
|
108603
|
+
const type = widget && typeof widget.type === 'string' ? widget.type : '';
|
|
108604
|
+
if (!type) return null
|
|
107994
108605
|
if (Object.prototype.hasOwnProperty.call(BUILTIN_SETTINGS_WIDGETS, type)) {
|
|
107995
|
-
|
|
108606
|
+
const builtin = BUILTIN_SETTINGS_WIDGETS[type];
|
|
108607
|
+
if (builtin === COMPONENT_DISCRIMINATOR) {
|
|
108608
|
+
// REQ-MSO-6: discriminator — look up `componentName`.
|
|
108609
|
+
const name = widget.componentName;
|
|
108610
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
108611
|
+
// eslint-disable-next-line no-console
|
|
108612
|
+
console.warn(
|
|
108613
|
+
'[CnSettingsPage] Widget {type:"component"} requires a non-empty `componentName`. Widget will be skipped.',
|
|
108614
|
+
);
|
|
108615
|
+
return null
|
|
108616
|
+
}
|
|
108617
|
+
const resolved = this.effectiveCustomComponents[name];
|
|
108618
|
+
if (!resolved) {
|
|
108619
|
+
// eslint-disable-next-line no-console
|
|
108620
|
+
console.warn(
|
|
108621
|
+
`[CnSettingsPage] Widget component "${name}" not found in customComponents registry. Widget will be skipped.`,
|
|
108622
|
+
);
|
|
108623
|
+
return null
|
|
108624
|
+
}
|
|
108625
|
+
return resolved
|
|
108626
|
+
}
|
|
108627
|
+
return builtin
|
|
107996
108628
|
}
|
|
108629
|
+
// Legacy fallback (manifest-settings-rich-sections REQ-MSRS-2).
|
|
108630
|
+
// Deprecated — manifest authors should migrate to
|
|
108631
|
+
// `{ type: "component", componentName: <X> }`. Kept here so
|
|
108632
|
+
// existing consumers continue working unchanged.
|
|
107997
108633
|
const resolved = this.effectiveCustomComponents[type];
|
|
107998
108634
|
if (!resolved) {
|
|
107999
108635
|
// eslint-disable-next-line no-console
|
|
@@ -108012,6 +108648,11 @@ var script$8 = {
|
|
|
108012
108648
|
* has already logged a warn. The filter happens here so the
|
|
108013
108649
|
* template can use a clean `v-for` without nested `v-if`.
|
|
108014
108650
|
*
|
|
108651
|
+
* The `widgetType` carried on the bubbled `@widget-event`
|
|
108652
|
+
* payload is the widget's `componentName` (when the
|
|
108653
|
+
* discriminator is `'component'`) or `widget.type` otherwise —
|
|
108654
|
+
* giving consumers a stable identifier for the dispatch.
|
|
108655
|
+
*
|
|
108015
108656
|
* @param {object} section A section entry with `widgets[]`.
|
|
108016
108657
|
* @param {number} sectionIndex Index in `sections[]`.
|
|
108017
108658
|
* @return {Array<{key: string, component: object, props: object, widgetType: string, widgetIndex: number}>}
|
|
@@ -108021,19 +108662,57 @@ var script$8 = {
|
|
|
108021
108662
|
const widgets = Array.isArray(section.widgets) ? section.widgets : [];
|
|
108022
108663
|
for (let widgetIndex = 0; widgetIndex < widgets.length; widgetIndex++) {
|
|
108023
108664
|
const widget = widgets[widgetIndex] || {};
|
|
108024
|
-
const component = this.resolveWidgetComponent(widget
|
|
108665
|
+
const component = this.resolveWidgetComponent(widget);
|
|
108025
108666
|
if (!component) continue
|
|
108667
|
+
const widgetType = widget.type === 'component' && typeof widget.componentName === 'string'
|
|
108668
|
+
? widget.componentName
|
|
108669
|
+
: widget.type;
|
|
108026
108670
|
entries.push({
|
|
108027
108671
|
key: `widget-${sectionIndex}-${widgetIndex}`,
|
|
108028
108672
|
component,
|
|
108029
108673
|
props: widget.props || {},
|
|
108030
|
-
widgetType
|
|
108674
|
+
widgetType,
|
|
108031
108675
|
widgetIndex,
|
|
108032
108676
|
});
|
|
108033
108677
|
}
|
|
108034
108678
|
return entries
|
|
108035
108679
|
},
|
|
108036
108680
|
|
|
108681
|
+
/**
|
|
108682
|
+
* Resolve the active-tab id on mount / when `tabs[]` changes.
|
|
108683
|
+
* Lookup order: explicit `initialTab` prop → first tab in
|
|
108684
|
+
* `tabs[]` → empty string. Unknown `initialTab` values fall
|
|
108685
|
+
* back to the first tab so the page never gets stuck.
|
|
108686
|
+
* (manifest-settings-orchestration REQ-MSO-5.)
|
|
108687
|
+
*
|
|
108688
|
+
* @return {string} The resolved tab id (empty in flat mode).
|
|
108689
|
+
*/
|
|
108690
|
+
resolveInitialTabId() {
|
|
108691
|
+
if (!this.hasTabs) return ''
|
|
108692
|
+
if (typeof this.initialTab === 'string' && this.initialTab.length > 0) {
|
|
108693
|
+
const exists = this.tabs.some(t => t && t.id === this.initialTab);
|
|
108694
|
+
if (exists) return this.initialTab
|
|
108695
|
+
}
|
|
108696
|
+
const first = this.tabs[0];
|
|
108697
|
+
return first && typeof first.id === 'string' ? first.id : ''
|
|
108698
|
+
},
|
|
108699
|
+
|
|
108700
|
+
/**
|
|
108701
|
+
* Handle a tab button click. Switches the active tab and
|
|
108702
|
+
* emits `@tab-change` so consumers can react (e.g. persist the
|
|
108703
|
+
* active tab in their preference store, update the URL hash).
|
|
108704
|
+
* (manifest-settings-orchestration REQ-MSO-5.)
|
|
108705
|
+
*
|
|
108706
|
+
* @param {object} tab The clicked tab definition.
|
|
108707
|
+
* @param {number} tabIndex The tab's index in `tabs[]`.
|
|
108708
|
+
*/
|
|
108709
|
+
onTabClick(tab, tabIndex) {
|
|
108710
|
+
if (!tab || typeof tab.id !== 'string') return
|
|
108711
|
+
if (this.activeTabId === tab.id) return
|
|
108712
|
+
this.activeTabId = tab.id;
|
|
108713
|
+
this.$emit('tab-change', { tabId: tab.id, tabIndex });
|
|
108714
|
+
},
|
|
108715
|
+
|
|
108037
108716
|
/**
|
|
108038
108717
|
* Re-emit a widget's `widget-event` (caught by the local
|
|
108039
108718
|
* CnSettingsWidgetMount helper) as a top-level `widget-event`
|
|
@@ -108109,11 +108788,51 @@ var __vue_render__$8 = function () {
|
|
|
108109
108788
|
)
|
|
108110
108789
|
: _vm._e(),
|
|
108111
108790
|
_vm._v(" "),
|
|
108112
|
-
_vm.
|
|
108791
|
+
_vm.hasTabs
|
|
108792
|
+
? _c(
|
|
108793
|
+
"div",
|
|
108794
|
+
{
|
|
108795
|
+
staticClass: "cn-settings-page__tabs",
|
|
108796
|
+
attrs: { role: "tablist" },
|
|
108797
|
+
},
|
|
108798
|
+
_vm._l(_vm.tabs, function (tab, tabIndex) {
|
|
108799
|
+
return _c(
|
|
108800
|
+
"button",
|
|
108801
|
+
{
|
|
108802
|
+
key: "tab-" + tab.id,
|
|
108803
|
+
staticClass: "cn-settings-page__tab",
|
|
108804
|
+
class: {
|
|
108805
|
+
"cn-settings-page__tab--active": _vm.activeTabId === tab.id,
|
|
108806
|
+
},
|
|
108807
|
+
attrs: {
|
|
108808
|
+
role: "tab",
|
|
108809
|
+
type: "button",
|
|
108810
|
+
"aria-selected":
|
|
108811
|
+
_vm.activeTabId === tab.id ? "true" : "false",
|
|
108812
|
+
"aria-controls": "cn-settings-tab-panel-" + tab.id,
|
|
108813
|
+
},
|
|
108814
|
+
on: {
|
|
108815
|
+
click: function ($event) {
|
|
108816
|
+
return _vm.onTabClick(tab, tabIndex)
|
|
108817
|
+
},
|
|
108818
|
+
},
|
|
108819
|
+
},
|
|
108820
|
+
[
|
|
108821
|
+
_vm._v(
|
|
108822
|
+
"\n\t\t\t" + _vm._s(_vm.resolveLabel(tab.label)) + "\n\t\t"
|
|
108823
|
+
),
|
|
108824
|
+
]
|
|
108825
|
+
)
|
|
108826
|
+
}),
|
|
108827
|
+
0
|
|
108828
|
+
)
|
|
108829
|
+
: _vm._e(),
|
|
108830
|
+
_vm._v(" "),
|
|
108831
|
+
_vm._l(_vm.activeSections, function (section, sectionIndex) {
|
|
108113
108832
|
return _c(
|
|
108114
108833
|
"CnSettingsCard",
|
|
108115
108834
|
{
|
|
108116
|
-
key: "section-" + sectionIndex,
|
|
108835
|
+
key: "section-" + (_vm.activeTabId || "flat") + "-" + sectionIndex,
|
|
108117
108836
|
attrs: {
|
|
108118
108837
|
title: _vm.resolveLabel(section.title),
|
|
108119
108838
|
icon: section.icon || "",
|
|
@@ -108378,7 +109097,7 @@ __vue_render__$8._withStripped = true;
|
|
|
108378
109097
|
/* style */
|
|
108379
109098
|
const __vue_inject_styles__$8 = undefined;
|
|
108380
109099
|
/* scoped */
|
|
108381
|
-
const __vue_scope_id__$8 = "data-v-
|
|
109100
|
+
const __vue_scope_id__$8 = "data-v-4286dbd4";
|
|
108382
109101
|
/* module identifier */
|
|
108383
109102
|
const __vue_module_identifier__$8 = undefined;
|
|
108384
109103
|
/* functional template */
|
|
@@ -112257,6 +112976,7 @@ exports.SEARCH_TYPE = SEARCH_TYPE;
|
|
|
112257
112976
|
exports.auditTrailsPlugin = auditTrailsPlugin;
|
|
112258
112977
|
exports.buildHeaders = buildHeaders;
|
|
112259
112978
|
exports.buildQueryString = buildQueryString;
|
|
112979
|
+
exports.clearResolveCache = clearResolveCache;
|
|
112260
112980
|
exports.columnsFromSchema = columnsFromSchema;
|
|
112261
112981
|
exports.createCrudStore = createCrudStore;
|
|
112262
112982
|
exports.createObjectStore = createObjectStore;
|
|
@@ -112284,6 +113004,7 @@ exports.registerMappingPlugin = registerMappingPlugin;
|
|
|
112284
113004
|
exports.registerTranslations = registerTranslations;
|
|
112285
113005
|
exports.relationsPlugin = relationsPlugin;
|
|
112286
113006
|
exports.resetVisibilityCache = resetVisibilityCache;
|
|
113007
|
+
exports.resolveManifestSentinels = resolveManifestSentinels;
|
|
112287
113008
|
exports.searchPlugin = searchPlugin;
|
|
112288
113009
|
exports.selectionPlugin = selectionPlugin;
|
|
112289
113010
|
exports.useAppManifest = useAppManifest;
|