@conduction/nextcloud-vue 1.0.0-beta.20 → 1.0.0-beta.21
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 +430 -75
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +59 -7
- package/dist/nextcloud-vue.esm.js +430 -75
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +305 -26
- package/src/schemas/app-manifest.schema.json +40 -2
- package/src/utils/validateManifest.js +146 -51
|
@@ -56105,7 +56105,70 @@ function validateTypeConfig(page, index, errors) {
|
|
|
56105
56105
|
break
|
|
56106
56106
|
}
|
|
56107
56107
|
case 'settings': {
|
|
56108
|
-
|
|
56108
|
+
// `manifest-settings-orchestration` REQ-MSO-1: a settings page
|
|
56109
|
+
// MUST declare EXACTLY ONE of `sections` | `tabs`. When both
|
|
56110
|
+
// are set, emit the orchestration mutex error. When neither is
|
|
56111
|
+
// set, fall through to the legacy `sections required` error
|
|
56112
|
+
// (back-compat — REQ-MSO-7 / REQ-MSO-1 last scenario).
|
|
56113
|
+
const hasSections = cfg && Array.isArray(cfg.sections);
|
|
56114
|
+
const hasTabs = cfg && Array.isArray(cfg.tabs);
|
|
56115
|
+
|
|
56116
|
+
if (hasSections && hasTabs) {
|
|
56117
|
+
errors.push(`${pathSlash}: ${pathBracket}: must declare exactly one of sections | tabs`);
|
|
56118
|
+
break
|
|
56119
|
+
}
|
|
56120
|
+
|
|
56121
|
+
if (hasTabs) {
|
|
56122
|
+
// `manifest-settings-orchestration` REQ-MSO-2..4: validate
|
|
56123
|
+
// the `tabs[]` orchestration shape.
|
|
56124
|
+
if (cfg.tabs.length === 0) {
|
|
56125
|
+
errors.push(`${pathSlash}/tabs: ${pathBracket}.tabs: must contain at least 1 tab`);
|
|
56126
|
+
break
|
|
56127
|
+
}
|
|
56128
|
+
const seenTabIds = Object.create(null);
|
|
56129
|
+
cfg.tabs.forEach((tab, tIndex) => {
|
|
56130
|
+
if (!isPlainObject$1(tab)) {
|
|
56131
|
+
errors.push(`${pathSlash}/tabs/${tIndex}: must be an object`);
|
|
56132
|
+
return
|
|
56133
|
+
}
|
|
56134
|
+
if (typeof tab.id !== 'string' || tab.id.length === 0) {
|
|
56135
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/id: required, must be a non-empty string`);
|
|
56136
|
+
}
|
|
56137
|
+
if (typeof tab.label !== 'string' || tab.label.length === 0) {
|
|
56138
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/label: required, must be a non-empty string`);
|
|
56139
|
+
}
|
|
56140
|
+
// REQ-MSO-3: tab IDs must be unique within a page.
|
|
56141
|
+
if (typeof tab.id === 'string' && tab.id.length > 0) {
|
|
56142
|
+
if (seenTabIds[tab.id]) {
|
|
56143
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/id: ${pathBracket}.tabs[${tIndex}].id: duplicate id "${tab.id}" — tab IDs must be unique within a page`);
|
|
56144
|
+
}
|
|
56145
|
+
seenTabIds[tab.id] = true;
|
|
56146
|
+
}
|
|
56147
|
+
// `tab.sections` MUST be a non-empty array.
|
|
56148
|
+
if (!Array.isArray(tab.sections)) {
|
|
56149
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/sections: ${pathBracket}.tabs[${tIndex}].sections: required, must be an array`);
|
|
56150
|
+
return
|
|
56151
|
+
}
|
|
56152
|
+
if (tab.sections.length === 0) {
|
|
56153
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/sections: ${pathBracket}.tabs[${tIndex}].sections: must contain at least 1 section`);
|
|
56154
|
+
return
|
|
56155
|
+
}
|
|
56156
|
+
// REQ-MSO-4: each tab's sections follow the same rules
|
|
56157
|
+
// as the flat case — share the per-section validator.
|
|
56158
|
+
tab.sections.forEach((section, sIndex) => {
|
|
56159
|
+
validateSettingsSection(
|
|
56160
|
+
section,
|
|
56161
|
+
`${pathSlash}/tabs/${tIndex}/sections/${sIndex}`,
|
|
56162
|
+
`${pathBracket}.tabs[${tIndex}].sections[${sIndex}]`,
|
|
56163
|
+
errors,
|
|
56164
|
+
);
|
|
56165
|
+
});
|
|
56166
|
+
});
|
|
56167
|
+
break
|
|
56168
|
+
}
|
|
56169
|
+
|
|
56170
|
+
// Flat `sections[]` (existing path — REQ-MSRS-* + back-compat).
|
|
56171
|
+
if (!hasSections) {
|
|
56109
56172
|
errors.push(`${pathSlash}/sections: ${pathBracket}.sections: required, must be an array`);
|
|
56110
56173
|
break
|
|
56111
56174
|
}
|
|
@@ -56114,56 +56177,12 @@ function validateTypeConfig(page, index, errors) {
|
|
|
56114
56177
|
break
|
|
56115
56178
|
}
|
|
56116
56179
|
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
|
-
}
|
|
56180
|
+
validateSettingsSection(
|
|
56181
|
+
section,
|
|
56182
|
+
`${pathSlash}/sections/${sIndex}`,
|
|
56183
|
+
`${pathBracket}.sections[${sIndex}]`,
|
|
56184
|
+
errors,
|
|
56185
|
+
);
|
|
56167
56186
|
});
|
|
56168
56187
|
break
|
|
56169
56188
|
}
|
|
@@ -56591,6 +56610,82 @@ function validateLayoutArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
56591
56610
|
});
|
|
56592
56611
|
}
|
|
56593
56612
|
|
|
56613
|
+
/**
|
|
56614
|
+
* Validate a single `sections[]` entry for `type:"settings"` pages.
|
|
56615
|
+
* Shared between the flat `pages[].config.sections[]` path AND the
|
|
56616
|
+
* tab-nested `pages[].config.tabs[].sections[]` path
|
|
56617
|
+
* (`manifest-settings-orchestration` REQ-MSO-4).
|
|
56618
|
+
*
|
|
56619
|
+
* Enforces the rich-sections REQ-MSRS-1 mutex (`fields | component |
|
|
56620
|
+
* widgets` exactly-one-of) plus per-widget shape rules. The new
|
|
56621
|
+
* `widget.type === "component"` discriminator (REQ-MSO-6) requires
|
|
56622
|
+
* `componentName: <non-empty string>`.
|
|
56623
|
+
*
|
|
56624
|
+
* @param {*} section The section under validation
|
|
56625
|
+
* @param {string} pathSlash JSON-pointer-style path prefix for errors
|
|
56626
|
+
* @param {string} pathBracket Human-readable bracket-path for errors
|
|
56627
|
+
* @param {string[]} errors Accumulator
|
|
56628
|
+
*/
|
|
56629
|
+
function validateSettingsSection(section, pathSlash, pathBracket, errors) {
|
|
56630
|
+
if (!isPlainObject$1(section)) {
|
|
56631
|
+
errors.push(`${pathSlash}: must be an object`);
|
|
56632
|
+
return
|
|
56633
|
+
}
|
|
56634
|
+
if (typeof section.title !== 'string') {
|
|
56635
|
+
errors.push(`${pathSlash}/title: required, must be a string`);
|
|
56636
|
+
}
|
|
56637
|
+
|
|
56638
|
+
// `manifest-settings-rich-sections` REQ-MSRS-1: exactly one of
|
|
56639
|
+
// fields | component | widgets.
|
|
56640
|
+
const hasFields = Array.isArray(section.fields);
|
|
56641
|
+
const hasComponent = typeof section.component === 'string' && section.component.length > 0;
|
|
56642
|
+
const hasWidgets = Array.isArray(section.widgets) && section.widgets.length > 0;
|
|
56643
|
+
const bodyCount = (hasFields ? 1 : 0) + (hasComponent ? 1 : 0) + (hasWidgets ? 1 : 0);
|
|
56644
|
+
|
|
56645
|
+
if (bodyCount !== 1) {
|
|
56646
|
+
errors.push(`${pathSlash}: ${pathBracket}: must declare exactly one of fields | component | widgets`);
|
|
56647
|
+
}
|
|
56648
|
+
|
|
56649
|
+
// `widgets` set but not an array (string / object / etc.)
|
|
56650
|
+
if (section.widgets !== undefined && !Array.isArray(section.widgets)) {
|
|
56651
|
+
errors.push(`${pathSlash}/widgets: must be an array when set`);
|
|
56652
|
+
}
|
|
56653
|
+
|
|
56654
|
+
// `component` set but not a string.
|
|
56655
|
+
if (section.component !== undefined && typeof section.component !== 'string') {
|
|
56656
|
+
errors.push(`${pathSlash}/component: must be a string when set`);
|
|
56657
|
+
}
|
|
56658
|
+
|
|
56659
|
+
// Per-widget shape rules.
|
|
56660
|
+
if (hasWidgets) {
|
|
56661
|
+
section.widgets.forEach((widget, wIndex) => {
|
|
56662
|
+
if (!isPlainObject$1(widget)) {
|
|
56663
|
+
errors.push(`${pathSlash}/widgets/${wIndex}: must be an object`);
|
|
56664
|
+
return
|
|
56665
|
+
}
|
|
56666
|
+
if (typeof widget.type !== 'string' || widget.type.length === 0) {
|
|
56667
|
+
errors.push(`${pathSlash}/widgets/${wIndex}/type: must be a non-empty string`);
|
|
56668
|
+
return
|
|
56669
|
+
}
|
|
56670
|
+
// `manifest-settings-orchestration` REQ-MSO-6: when the
|
|
56671
|
+
// discriminator is "component", `componentName` MUST be a
|
|
56672
|
+
// non-empty string. Other widget types ignore
|
|
56673
|
+
// `componentName`.
|
|
56674
|
+
if (widget.type === 'component') {
|
|
56675
|
+
if (typeof widget.componentName !== 'string' || widget.componentName.length === 0) {
|
|
56676
|
+
errors.push(`${pathSlash}/widgets/${wIndex}/componentName: required when type is "component", must be a non-empty string`);
|
|
56677
|
+
}
|
|
56678
|
+
}
|
|
56679
|
+
});
|
|
56680
|
+
}
|
|
56681
|
+
|
|
56682
|
+
// `manifest-config-refs` REQ-MCR — when fields[] body is used,
|
|
56683
|
+
// each entry must match the formField $def shape.
|
|
56684
|
+
if (hasFields) {
|
|
56685
|
+
validateFieldsArray(section.fields, `${pathSlash}/fields`, errors);
|
|
56686
|
+
}
|
|
56687
|
+
}
|
|
56688
|
+
|
|
56594
56689
|
/**
|
|
56595
56690
|
* Validate `config.sections[].fields[]` for settings page type
|
|
56596
56691
|
* (`manifest-config-refs` REQ-MCR). Each field MUST be an object with
|
|
@@ -107628,8 +107723,46 @@ var CnSettingsWidgetMount = {
|
|
|
107628
107723
|
//
|
|
107629
107724
|
//
|
|
107630
107725
|
//
|
|
107726
|
+
//
|
|
107727
|
+
//
|
|
107728
|
+
//
|
|
107729
|
+
//
|
|
107730
|
+
//
|
|
107731
|
+
//
|
|
107732
|
+
//
|
|
107733
|
+
//
|
|
107734
|
+
//
|
|
107735
|
+
//
|
|
107736
|
+
//
|
|
107737
|
+
//
|
|
107738
|
+
//
|
|
107739
|
+
//
|
|
107740
|
+
//
|
|
107741
|
+
//
|
|
107742
|
+
//
|
|
107743
|
+
//
|
|
107744
|
+
//
|
|
107745
|
+
//
|
|
107746
|
+
//
|
|
107747
|
+
//
|
|
107748
|
+
//
|
|
107749
|
+
//
|
|
107750
|
+
//
|
|
107751
|
+
//
|
|
107752
|
+
//
|
|
107753
|
+
//
|
|
107754
|
+
//
|
|
107631
107755
|
|
|
107632
107756
|
|
|
107757
|
+
/**
|
|
107758
|
+
* Sentinel value used in the built-in widget registry to mark the
|
|
107759
|
+
* `'component'` discriminator (manifest-settings-orchestration
|
|
107760
|
+
* REQ-MSO-6). The discriminator does NOT resolve to a fixed
|
|
107761
|
+
* component — instead, the resolver detects this sentinel and looks
|
|
107762
|
+
* up `widget.componentName` in the customComponents registry.
|
|
107763
|
+
*/
|
|
107764
|
+
const COMPONENT_DISCRIMINATOR = Symbol('cn-settings-component-widget');
|
|
107765
|
+
|
|
107633
107766
|
/**
|
|
107634
107767
|
* Built-in widget registry. Used by `CnSettingsPage` to resolve
|
|
107635
107768
|
* `widgets[].type` to a component BEFORE consulting the
|
|
@@ -107638,13 +107771,19 @@ var CnSettingsWidgetMount = {
|
|
|
107638
107771
|
* The order matters — built-ins win on collision so consumers can't
|
|
107639
107772
|
* accidentally shadow `version-info` with their own component. If a
|
|
107640
107773
|
* consumer needs to truly replace one of these, they can render their
|
|
107641
|
-
* own component via `section.component`
|
|
107774
|
+
* own component via `section.component` or
|
|
107775
|
+
* `{ type: "component", componentName: <name> }` instead of `widgets[]`.
|
|
107642
107776
|
*
|
|
107643
|
-
* Spec:
|
|
107777
|
+
* Spec:
|
|
107778
|
+
* - REQ-MSRS-2 (manifest-settings-rich-sections) — fixed-component
|
|
107779
|
+
* built-ins (`version-info`, `register-mapping`).
|
|
107780
|
+
* - REQ-MSO-6 (manifest-settings-orchestration) — `'component'`
|
|
107781
|
+
* discriminator (sentinel value, resolved via componentName).
|
|
107644
107782
|
*/
|
|
107645
107783
|
const BUILTIN_SETTINGS_WIDGETS = Object.freeze({
|
|
107646
107784
|
'version-info': __vue_component__$17,
|
|
107647
107785
|
'register-mapping': __vue_component__$I,
|
|
107786
|
+
component: COMPONENT_DISCRIMINATOR,
|
|
107648
107787
|
});
|
|
107649
107788
|
|
|
107650
107789
|
/**
|
|
@@ -107752,19 +107891,50 @@ var script$8 = {
|
|
|
107752
107891
|
default: '',
|
|
107753
107892
|
},
|
|
107754
107893
|
/**
|
|
107755
|
-
* Section definitions
|
|
107894
|
+
* Section definitions (flat shape — back-compat). Each section
|
|
107895
|
+
* MUST declare EXACTLY ONE of:
|
|
107756
107896
|
* - `fields: Array<Field>` (back-compat flat-field body)
|
|
107757
107897
|
* - `component: <registry-name>` + optional `props`
|
|
107758
|
-
* - `widgets: Array<{ type, props? }>`
|
|
107898
|
+
* - `widgets: Array<{ type, props?, componentName? }>`
|
|
107759
107899
|
*
|
|
107760
107900
|
* Common keys: `{ title, description?, icon?, collapsible?, docUrl? }`.
|
|
107761
107901
|
*
|
|
107902
|
+
* Mutually exclusive with `tabs[]` (XOR — see
|
|
107903
|
+
* manifest-settings-orchestration REQ-MSO-1).
|
|
107904
|
+
*
|
|
107762
107905
|
* @type {Array<object>}
|
|
107763
107906
|
*/
|
|
107764
107907
|
sections: {
|
|
107765
107908
|
type: Array,
|
|
107766
107909
|
default: () => [],
|
|
107767
107910
|
},
|
|
107911
|
+
/**
|
|
107912
|
+
* Tab definitions (orchestration shape — manifest-settings-
|
|
107913
|
+
* orchestration REQ-MSO-2). When set, CnSettingsPage renders
|
|
107914
|
+
* a tab strip above the section area; the active tab's
|
|
107915
|
+
* `sections[]` flow into the same renderer used by the flat
|
|
107916
|
+
* shape. Mutually exclusive with `sections[]`.
|
|
107917
|
+
*
|
|
107918
|
+
* Each tab MUST be `{ id: string, label: string,
|
|
107919
|
+
* icon?: string, sections: array<Section> }`.
|
|
107920
|
+
*
|
|
107921
|
+
* @type {Array<object>}
|
|
107922
|
+
*/
|
|
107923
|
+
tabs: {
|
|
107924
|
+
type: Array,
|
|
107925
|
+
default: () => [],
|
|
107926
|
+
},
|
|
107927
|
+
/**
|
|
107928
|
+
* Optional ID of the tab to activate on mount. When empty AND
|
|
107929
|
+
* `tabs[]` is non-empty, the first tab is active by default.
|
|
107930
|
+
* Unknown IDs fall back to the first tab.
|
|
107931
|
+
*
|
|
107932
|
+
* @type {string}
|
|
107933
|
+
*/
|
|
107934
|
+
initialTab: {
|
|
107935
|
+
type: String,
|
|
107936
|
+
default: '',
|
|
107937
|
+
},
|
|
107768
107938
|
/**
|
|
107769
107939
|
* Initial values keyed by `field.key`. Defaults to an empty
|
|
107770
107940
|
* object; in practice the consumer passes the current
|
|
@@ -107826,14 +107996,30 @@ var script$8 = {
|
|
|
107826
107996
|
},
|
|
107827
107997
|
},
|
|
107828
107998
|
|
|
107829
|
-
emits: ['save', 'error', 'input', 'widget-event'],
|
|
107999
|
+
emits: ['save', 'error', 'input', 'widget-event', 'tab-change'],
|
|
107830
108000
|
|
|
107831
108001
|
data() {
|
|
108002
|
+
// Resolve the initial active-tab id synchronously so the very
|
|
108003
|
+
// first render has the correct tab active (otherwise tests
|
|
108004
|
+
// that mount + assert without an `await tick` see the empty
|
|
108005
|
+
// default). Mirrors the logic in `resolveInitialTabId` (the
|
|
108006
|
+
// watcher path); keep them aligned.
|
|
108007
|
+
let activeTabId = '';
|
|
108008
|
+
const tabs = Array.isArray(this.tabs) ? this.tabs : [];
|
|
108009
|
+
if (tabs.length > 0) {
|
|
108010
|
+
if (typeof this.initialTab === 'string' && this.initialTab.length > 0
|
|
108011
|
+
&& tabs.some(t => t && t.id === this.initialTab)) {
|
|
108012
|
+
activeTabId = this.initialTab;
|
|
108013
|
+
} else if (tabs[0] && typeof tabs[0].id === 'string') {
|
|
108014
|
+
activeTabId = tabs[0].id;
|
|
108015
|
+
}
|
|
108016
|
+
}
|
|
107832
108017
|
return {
|
|
107833
108018
|
formData: this.cloneInitial(),
|
|
107834
108019
|
originalData: this.cloneInitial(),
|
|
107835
108020
|
saving: false,
|
|
107836
108021
|
lastError: null,
|
|
108022
|
+
activeTabId,
|
|
107837
108023
|
}
|
|
107838
108024
|
},
|
|
107839
108025
|
|
|
@@ -107852,6 +108038,35 @@ var script$8 = {
|
|
|
107852
108038
|
effectiveCustomComponents() {
|
|
107853
108039
|
return this.customComponents ?? this.cnCustomComponents ?? {}
|
|
107854
108040
|
},
|
|
108041
|
+
/**
|
|
108042
|
+
* Whether the page is in tabs orchestration mode. True when
|
|
108043
|
+
* `tabs[]` is non-empty — drives the tab-strip render gate
|
|
108044
|
+
* (manifest-settings-orchestration REQ-MSO-5).
|
|
108045
|
+
*
|
|
108046
|
+
* @return {boolean}
|
|
108047
|
+
*/
|
|
108048
|
+
hasTabs() {
|
|
108049
|
+
return Array.isArray(this.tabs) && this.tabs.length > 0
|
|
108050
|
+
},
|
|
108051
|
+
/**
|
|
108052
|
+
* The sections to render right now. In flat mode, this is the
|
|
108053
|
+
* `sections` prop directly. In tabs mode, this is the
|
|
108054
|
+
* `sections[]` array of the currently active tab. Centralising
|
|
108055
|
+
* this in one computed keeps the template's `v-for` simple
|
|
108056
|
+
* and decouples it from the body kind dispatcher (which
|
|
108057
|
+
* applies per-section, not per-mode).
|
|
108058
|
+
*
|
|
108059
|
+
* @return {Array<object>}
|
|
108060
|
+
*/
|
|
108061
|
+
activeSections() {
|
|
108062
|
+
if (!this.hasTabs) return this.sections || []
|
|
108063
|
+
const active = this.tabs.find(t => t && t.id === this.activeTabId);
|
|
108064
|
+
if (active && Array.isArray(active.sections)) return active.sections
|
|
108065
|
+
// Defensive fallback — should not happen because
|
|
108066
|
+
// `resolveInitialTabId` always lands on a known tab.
|
|
108067
|
+
const first = this.tabs[0];
|
|
108068
|
+
return first && Array.isArray(first.sections) ? first.sections : []
|
|
108069
|
+
},
|
|
107855
108070
|
},
|
|
107856
108071
|
|
|
107857
108072
|
watch: {
|
|
@@ -107862,6 +108077,22 @@ var script$8 = {
|
|
|
107862
108077
|
this.originalData = this.cloneInitial();
|
|
107863
108078
|
},
|
|
107864
108079
|
},
|
|
108080
|
+
// When `tabs[]` changes (e.g. consumer swaps manifests at
|
|
108081
|
+
// runtime), re-resolve the active tab so the page doesn't get
|
|
108082
|
+
// stuck on a removed id.
|
|
108083
|
+
tabs: {
|
|
108084
|
+
handler() {
|
|
108085
|
+
this.activeTabId = this.resolveInitialTabId();
|
|
108086
|
+
},
|
|
108087
|
+
},
|
|
108088
|
+
// When `initialTab` changes (consumer-controlled tab
|
|
108089
|
+
// activation), follow it.
|
|
108090
|
+
initialTab(next) {
|
|
108091
|
+
if (typeof next === 'string' && next.length > 0) {
|
|
108092
|
+
const exists = this.tabs.some(t => t && t.id === next);
|
|
108093
|
+
if (exists) this.activeTabId = next;
|
|
108094
|
+
}
|
|
108095
|
+
},
|
|
107865
108096
|
},
|
|
107866
108097
|
|
|
107867
108098
|
methods: {
|
|
@@ -107907,11 +108138,20 @@ var script$8 = {
|
|
|
107907
108138
|
},
|
|
107908
108139
|
cloneInitial() {
|
|
107909
108140
|
const merged = { ...(this.initialValues || {}) };
|
|
107910
|
-
//
|
|
107911
|
-
//
|
|
107912
|
-
//
|
|
107913
|
-
|
|
107914
|
-
|
|
108141
|
+
// Collect every section across both modes (flat
|
|
108142
|
+
// `sections[]` AND `tabs[].sections[]`) so default values
|
|
108143
|
+
// are applied regardless of orchestration shape.
|
|
108144
|
+
// Only flat-field sections contribute defaults; component
|
|
108145
|
+
// and widgets sections own their own state.
|
|
108146
|
+
const allSections = [];
|
|
108147
|
+
for (const section of this.sections || []) allSections.push(section);
|
|
108148
|
+
for (const tab of this.tabs || []) {
|
|
108149
|
+
if (tab && Array.isArray(tab.sections)) {
|
|
108150
|
+
for (const section of tab.sections) allSections.push(section);
|
|
108151
|
+
}
|
|
108152
|
+
}
|
|
108153
|
+
for (const section of allSections) {
|
|
108154
|
+
if (!section || !Array.isArray(section.fields)) continue
|
|
107915
108155
|
for (const field of section.fields) {
|
|
107916
108156
|
if (field.default !== undefined && merged[field.key] === undefined) {
|
|
107917
108157
|
merged[field.key] = field.default;
|
|
@@ -107977,23 +108217,55 @@ var script$8 = {
|
|
|
107977
108217
|
},
|
|
107978
108218
|
|
|
107979
108219
|
/**
|
|
107980
|
-
* Resolve a `widgets[]
|
|
107981
|
-
* order:
|
|
108220
|
+
* Resolve a single `widgets[]` entry to a concrete Vue
|
|
108221
|
+
* component. Lookup order:
|
|
107982
108222
|
*
|
|
107983
108223
|
* 1. Built-in widget map (`version-info`, `register-mapping`).
|
|
107984
|
-
* 2. `
|
|
108224
|
+
* 2. `'component'` discriminator (REQ-MSO-6) — resolves
|
|
108225
|
+
* `widget.componentName` against `effectiveCustomComponents`.
|
|
108226
|
+
* 3. Legacy fallback — looks up `widget.type` against
|
|
108227
|
+
* `effectiveCustomComponents`. Kept for back-compat with
|
|
108228
|
+
* manifest-settings-rich-sections consumers; flagged as
|
|
108229
|
+
* deprecated in JSDoc — manifest authors should migrate to
|
|
108230
|
+
* the explicit `{ type: "component", componentName }` shape.
|
|
107985
108231
|
*
|
|
107986
|
-
* Returns `null` (and warns) when
|
|
107987
|
-
* win on collision so consumers can't accidentally shadow them
|
|
107988
|
-
* (REQ-MSRS-2).
|
|
108232
|
+
* Returns `null` (and warns) when nothing resolves. Built-ins
|
|
108233
|
+
* win on collision so consumers can't accidentally shadow them.
|
|
107989
108234
|
*
|
|
107990
|
-
* @param {
|
|
108235
|
+
* @param {object} widget A `widgets[]` entry, e.g. `{ type, props?, componentName? }`.
|
|
107991
108236
|
* @return {object|null} Vue component or null.
|
|
107992
108237
|
*/
|
|
107993
|
-
resolveWidgetComponent(
|
|
108238
|
+
resolveWidgetComponent(widget) {
|
|
108239
|
+
const type = widget && typeof widget.type === 'string' ? widget.type : '';
|
|
108240
|
+
if (!type) return null
|
|
107994
108241
|
if (Object.prototype.hasOwnProperty.call(BUILTIN_SETTINGS_WIDGETS, type)) {
|
|
107995
|
-
|
|
108242
|
+
const builtin = BUILTIN_SETTINGS_WIDGETS[type];
|
|
108243
|
+
if (builtin === COMPONENT_DISCRIMINATOR) {
|
|
108244
|
+
// REQ-MSO-6: discriminator — look up `componentName`.
|
|
108245
|
+
const name = widget.componentName;
|
|
108246
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
108247
|
+
// eslint-disable-next-line no-console
|
|
108248
|
+
console.warn(
|
|
108249
|
+
'[CnSettingsPage] Widget {type:"component"} requires a non-empty `componentName`. Widget will be skipped.',
|
|
108250
|
+
);
|
|
108251
|
+
return null
|
|
108252
|
+
}
|
|
108253
|
+
const resolved = this.effectiveCustomComponents[name];
|
|
108254
|
+
if (!resolved) {
|
|
108255
|
+
// eslint-disable-next-line no-console
|
|
108256
|
+
console.warn(
|
|
108257
|
+
`[CnSettingsPage] Widget component "${name}" not found in customComponents registry. Widget will be skipped.`,
|
|
108258
|
+
);
|
|
108259
|
+
return null
|
|
108260
|
+
}
|
|
108261
|
+
return resolved
|
|
108262
|
+
}
|
|
108263
|
+
return builtin
|
|
107996
108264
|
}
|
|
108265
|
+
// Legacy fallback (manifest-settings-rich-sections REQ-MSRS-2).
|
|
108266
|
+
// Deprecated — manifest authors should migrate to
|
|
108267
|
+
// `{ type: "component", componentName: <X> }`. Kept here so
|
|
108268
|
+
// existing consumers continue working unchanged.
|
|
107997
108269
|
const resolved = this.effectiveCustomComponents[type];
|
|
107998
108270
|
if (!resolved) {
|
|
107999
108271
|
// eslint-disable-next-line no-console
|
|
@@ -108012,6 +108284,11 @@ var script$8 = {
|
|
|
108012
108284
|
* has already logged a warn. The filter happens here so the
|
|
108013
108285
|
* template can use a clean `v-for` without nested `v-if`.
|
|
108014
108286
|
*
|
|
108287
|
+
* The `widgetType` carried on the bubbled `@widget-event`
|
|
108288
|
+
* payload is the widget's `componentName` (when the
|
|
108289
|
+
* discriminator is `'component'`) or `widget.type` otherwise —
|
|
108290
|
+
* giving consumers a stable identifier for the dispatch.
|
|
108291
|
+
*
|
|
108015
108292
|
* @param {object} section A section entry with `widgets[]`.
|
|
108016
108293
|
* @param {number} sectionIndex Index in `sections[]`.
|
|
108017
108294
|
* @return {Array<{key: string, component: object, props: object, widgetType: string, widgetIndex: number}>}
|
|
@@ -108021,19 +108298,57 @@ var script$8 = {
|
|
|
108021
108298
|
const widgets = Array.isArray(section.widgets) ? section.widgets : [];
|
|
108022
108299
|
for (let widgetIndex = 0; widgetIndex < widgets.length; widgetIndex++) {
|
|
108023
108300
|
const widget = widgets[widgetIndex] || {};
|
|
108024
|
-
const component = this.resolveWidgetComponent(widget
|
|
108301
|
+
const component = this.resolveWidgetComponent(widget);
|
|
108025
108302
|
if (!component) continue
|
|
108303
|
+
const widgetType = widget.type === 'component' && typeof widget.componentName === 'string'
|
|
108304
|
+
? widget.componentName
|
|
108305
|
+
: widget.type;
|
|
108026
108306
|
entries.push({
|
|
108027
108307
|
key: `widget-${sectionIndex}-${widgetIndex}`,
|
|
108028
108308
|
component,
|
|
108029
108309
|
props: widget.props || {},
|
|
108030
|
-
widgetType
|
|
108310
|
+
widgetType,
|
|
108031
108311
|
widgetIndex,
|
|
108032
108312
|
});
|
|
108033
108313
|
}
|
|
108034
108314
|
return entries
|
|
108035
108315
|
},
|
|
108036
108316
|
|
|
108317
|
+
/**
|
|
108318
|
+
* Resolve the active-tab id on mount / when `tabs[]` changes.
|
|
108319
|
+
* Lookup order: explicit `initialTab` prop → first tab in
|
|
108320
|
+
* `tabs[]` → empty string. Unknown `initialTab` values fall
|
|
108321
|
+
* back to the first tab so the page never gets stuck.
|
|
108322
|
+
* (manifest-settings-orchestration REQ-MSO-5.)
|
|
108323
|
+
*
|
|
108324
|
+
* @return {string} The resolved tab id (empty in flat mode).
|
|
108325
|
+
*/
|
|
108326
|
+
resolveInitialTabId() {
|
|
108327
|
+
if (!this.hasTabs) return ''
|
|
108328
|
+
if (typeof this.initialTab === 'string' && this.initialTab.length > 0) {
|
|
108329
|
+
const exists = this.tabs.some(t => t && t.id === this.initialTab);
|
|
108330
|
+
if (exists) return this.initialTab
|
|
108331
|
+
}
|
|
108332
|
+
const first = this.tabs[0];
|
|
108333
|
+
return first && typeof first.id === 'string' ? first.id : ''
|
|
108334
|
+
},
|
|
108335
|
+
|
|
108336
|
+
/**
|
|
108337
|
+
* Handle a tab button click. Switches the active tab and
|
|
108338
|
+
* emits `@tab-change` so consumers can react (e.g. persist the
|
|
108339
|
+
* active tab in their preference store, update the URL hash).
|
|
108340
|
+
* (manifest-settings-orchestration REQ-MSO-5.)
|
|
108341
|
+
*
|
|
108342
|
+
* @param {object} tab The clicked tab definition.
|
|
108343
|
+
* @param {number} tabIndex The tab's index in `tabs[]`.
|
|
108344
|
+
*/
|
|
108345
|
+
onTabClick(tab, tabIndex) {
|
|
108346
|
+
if (!tab || typeof tab.id !== 'string') return
|
|
108347
|
+
if (this.activeTabId === tab.id) return
|
|
108348
|
+
this.activeTabId = tab.id;
|
|
108349
|
+
this.$emit('tab-change', { tabId: tab.id, tabIndex });
|
|
108350
|
+
},
|
|
108351
|
+
|
|
108037
108352
|
/**
|
|
108038
108353
|
* Re-emit a widget's `widget-event` (caught by the local
|
|
108039
108354
|
* CnSettingsWidgetMount helper) as a top-level `widget-event`
|
|
@@ -108109,11 +108424,51 @@ var __vue_render__$8 = function () {
|
|
|
108109
108424
|
)
|
|
108110
108425
|
: _vm._e(),
|
|
108111
108426
|
_vm._v(" "),
|
|
108112
|
-
_vm.
|
|
108427
|
+
_vm.hasTabs
|
|
108428
|
+
? _c(
|
|
108429
|
+
"div",
|
|
108430
|
+
{
|
|
108431
|
+
staticClass: "cn-settings-page__tabs",
|
|
108432
|
+
attrs: { role: "tablist" },
|
|
108433
|
+
},
|
|
108434
|
+
_vm._l(_vm.tabs, function (tab, tabIndex) {
|
|
108435
|
+
return _c(
|
|
108436
|
+
"button",
|
|
108437
|
+
{
|
|
108438
|
+
key: "tab-" + tab.id,
|
|
108439
|
+
staticClass: "cn-settings-page__tab",
|
|
108440
|
+
class: {
|
|
108441
|
+
"cn-settings-page__tab--active": _vm.activeTabId === tab.id,
|
|
108442
|
+
},
|
|
108443
|
+
attrs: {
|
|
108444
|
+
role: "tab",
|
|
108445
|
+
type: "button",
|
|
108446
|
+
"aria-selected":
|
|
108447
|
+
_vm.activeTabId === tab.id ? "true" : "false",
|
|
108448
|
+
"aria-controls": "cn-settings-tab-panel-" + tab.id,
|
|
108449
|
+
},
|
|
108450
|
+
on: {
|
|
108451
|
+
click: function ($event) {
|
|
108452
|
+
return _vm.onTabClick(tab, tabIndex)
|
|
108453
|
+
},
|
|
108454
|
+
},
|
|
108455
|
+
},
|
|
108456
|
+
[
|
|
108457
|
+
_vm._v(
|
|
108458
|
+
"\n\t\t\t" + _vm._s(_vm.resolveLabel(tab.label)) + "\n\t\t"
|
|
108459
|
+
),
|
|
108460
|
+
]
|
|
108461
|
+
)
|
|
108462
|
+
}),
|
|
108463
|
+
0
|
|
108464
|
+
)
|
|
108465
|
+
: _vm._e(),
|
|
108466
|
+
_vm._v(" "),
|
|
108467
|
+
_vm._l(_vm.activeSections, function (section, sectionIndex) {
|
|
108113
108468
|
return _c(
|
|
108114
108469
|
"CnSettingsCard",
|
|
108115
108470
|
{
|
|
108116
|
-
key: "section-" + sectionIndex,
|
|
108471
|
+
key: "section-" + (_vm.activeTabId || "flat") + "-" + sectionIndex,
|
|
108117
108472
|
attrs: {
|
|
108118
108473
|
title: _vm.resolveLabel(section.title),
|
|
108119
108474
|
icon: section.icon || "",
|
|
@@ -108378,7 +108733,7 @@ __vue_render__$8._withStripped = true;
|
|
|
108378
108733
|
/* style */
|
|
108379
108734
|
const __vue_inject_styles__$8 = undefined;
|
|
108380
108735
|
/* scoped */
|
|
108381
|
-
const __vue_scope_id__$8 = "data-v-
|
|
108736
|
+
const __vue_scope_id__$8 = "data-v-4286dbd4";
|
|
108382
108737
|
/* module identifier */
|
|
108383
108738
|
const __vue_module_identifier__$8 = undefined;
|
|
108384
108739
|
/* functional template */
|