@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
|
@@ -178,7 +178,70 @@ function validateTypeConfig(page, index, errors) {
|
|
|
178
178
|
break
|
|
179
179
|
}
|
|
180
180
|
case 'settings': {
|
|
181
|
-
|
|
181
|
+
// `manifest-settings-orchestration` REQ-MSO-1: a settings page
|
|
182
|
+
// MUST declare EXACTLY ONE of `sections` | `tabs`. When both
|
|
183
|
+
// are set, emit the orchestration mutex error. When neither is
|
|
184
|
+
// set, fall through to the legacy `sections required` error
|
|
185
|
+
// (back-compat — REQ-MSO-7 / REQ-MSO-1 last scenario).
|
|
186
|
+
const hasSections = cfg && Array.isArray(cfg.sections)
|
|
187
|
+
const hasTabs = cfg && Array.isArray(cfg.tabs)
|
|
188
|
+
|
|
189
|
+
if (hasSections && hasTabs) {
|
|
190
|
+
errors.push(`${pathSlash}: ${pathBracket}: must declare exactly one of sections | tabs`)
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (hasTabs) {
|
|
195
|
+
// `manifest-settings-orchestration` REQ-MSO-2..4: validate
|
|
196
|
+
// the `tabs[]` orchestration shape.
|
|
197
|
+
if (cfg.tabs.length === 0) {
|
|
198
|
+
errors.push(`${pathSlash}/tabs: ${pathBracket}.tabs: must contain at least 1 tab`)
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
const seenTabIds = Object.create(null)
|
|
202
|
+
cfg.tabs.forEach((tab, tIndex) => {
|
|
203
|
+
if (!isPlainObject(tab)) {
|
|
204
|
+
errors.push(`${pathSlash}/tabs/${tIndex}: must be an object`)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (typeof tab.id !== 'string' || tab.id.length === 0) {
|
|
208
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/id: required, must be a non-empty string`)
|
|
209
|
+
}
|
|
210
|
+
if (typeof tab.label !== 'string' || tab.label.length === 0) {
|
|
211
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/label: required, must be a non-empty string`)
|
|
212
|
+
}
|
|
213
|
+
// REQ-MSO-3: tab IDs must be unique within a page.
|
|
214
|
+
if (typeof tab.id === 'string' && tab.id.length > 0) {
|
|
215
|
+
if (seenTabIds[tab.id]) {
|
|
216
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/id: ${pathBracket}.tabs[${tIndex}].id: duplicate id "${tab.id}" — tab IDs must be unique within a page`)
|
|
217
|
+
}
|
|
218
|
+
seenTabIds[tab.id] = true
|
|
219
|
+
}
|
|
220
|
+
// `tab.sections` MUST be a non-empty array.
|
|
221
|
+
if (!Array.isArray(tab.sections)) {
|
|
222
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/sections: ${pathBracket}.tabs[${tIndex}].sections: required, must be an array`)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (tab.sections.length === 0) {
|
|
226
|
+
errors.push(`${pathSlash}/tabs/${tIndex}/sections: ${pathBracket}.tabs[${tIndex}].sections: must contain at least 1 section`)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
// REQ-MSO-4: each tab's sections follow the same rules
|
|
230
|
+
// as the flat case — share the per-section validator.
|
|
231
|
+
tab.sections.forEach((section, sIndex) => {
|
|
232
|
+
validateSettingsSection(
|
|
233
|
+
section,
|
|
234
|
+
`${pathSlash}/tabs/${tIndex}/sections/${sIndex}`,
|
|
235
|
+
`${pathBracket}.tabs[${tIndex}].sections[${sIndex}]`,
|
|
236
|
+
errors,
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Flat `sections[]` (existing path — REQ-MSRS-* + back-compat).
|
|
244
|
+
if (!hasSections) {
|
|
182
245
|
errors.push(`${pathSlash}/sections: ${pathBracket}.sections: required, must be an array`)
|
|
183
246
|
break
|
|
184
247
|
}
|
|
@@ -187,56 +250,12 @@ function validateTypeConfig(page, index, errors) {
|
|
|
187
250
|
break
|
|
188
251
|
}
|
|
189
252
|
cfg.sections.forEach((section, sIndex) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// `manifest-settings-rich-sections` REQ-MSRS-1: each
|
|
199
|
-
// section MUST declare exactly one of fields | component
|
|
200
|
-
// | widgets. Mixed bodies confuse the renderer + duplicate
|
|
201
|
-
// the section chrome; empty bodies render nothing so they
|
|
202
|
-
// are a manifest-author bug.
|
|
203
|
-
const hasFields = Array.isArray(section.fields)
|
|
204
|
-
const hasComponent = typeof section.component === 'string' && section.component.length > 0
|
|
205
|
-
const hasWidgets = Array.isArray(section.widgets) && section.widgets.length > 0
|
|
206
|
-
const bodyCount = (hasFields ? 1 : 0) + (hasComponent ? 1 : 0) + (hasWidgets ? 1 : 0)
|
|
207
|
-
|
|
208
|
-
if (bodyCount !== 1) {
|
|
209
|
-
errors.push(`${pathSlash}/sections/${sIndex}: ${pathBracket}.sections[${sIndex}]: must declare exactly one of fields | component | widgets`)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// `widgets` set but not an array (string / object / etc.)
|
|
213
|
-
if (section.widgets !== undefined && !Array.isArray(section.widgets)) {
|
|
214
|
-
errors.push(`${pathSlash}/sections/${sIndex}/widgets: must be an array when set`)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// `component` set but not a string.
|
|
218
|
-
if (section.component !== undefined && typeof section.component !== 'string') {
|
|
219
|
-
errors.push(`${pathSlash}/sections/${sIndex}/component: must be a string when set`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Per-widget shape rules.
|
|
223
|
-
if (hasWidgets) {
|
|
224
|
-
section.widgets.forEach((widget, wIndex) => {
|
|
225
|
-
if (!isPlainObject(widget)) {
|
|
226
|
-
errors.push(`${pathSlash}/sections/${sIndex}/widgets/${wIndex}: must be an object`)
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
if (typeof widget.type !== 'string' || widget.type.length === 0) {
|
|
230
|
-
errors.push(`${pathSlash}/sections/${sIndex}/widgets/${wIndex}/type: must be a non-empty string`)
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// `manifest-config-refs` REQ-MCR — when fields[] body is
|
|
236
|
-
// used, each entry must match the formField $def shape.
|
|
237
|
-
if (hasFields) {
|
|
238
|
-
validateFieldsArray(section.fields, `${pathSlash}/sections/${sIndex}/fields`, errors)
|
|
239
|
-
}
|
|
253
|
+
validateSettingsSection(
|
|
254
|
+
section,
|
|
255
|
+
`${pathSlash}/sections/${sIndex}`,
|
|
256
|
+
`${pathBracket}.sections[${sIndex}]`,
|
|
257
|
+
errors,
|
|
258
|
+
)
|
|
240
259
|
})
|
|
241
260
|
break
|
|
242
261
|
}
|
|
@@ -669,6 +688,82 @@ function validateLayoutArray(cfg, pathSlash, pathBracket, errors) {
|
|
|
669
688
|
})
|
|
670
689
|
}
|
|
671
690
|
|
|
691
|
+
/**
|
|
692
|
+
* Validate a single `sections[]` entry for `type:"settings"` pages.
|
|
693
|
+
* Shared between the flat `pages[].config.sections[]` path AND the
|
|
694
|
+
* tab-nested `pages[].config.tabs[].sections[]` path
|
|
695
|
+
* (`manifest-settings-orchestration` REQ-MSO-4).
|
|
696
|
+
*
|
|
697
|
+
* Enforces the rich-sections REQ-MSRS-1 mutex (`fields | component |
|
|
698
|
+
* widgets` exactly-one-of) plus per-widget shape rules. The new
|
|
699
|
+
* `widget.type === "component"` discriminator (REQ-MSO-6) requires
|
|
700
|
+
* `componentName: <non-empty string>`.
|
|
701
|
+
*
|
|
702
|
+
* @param {*} section The section under validation
|
|
703
|
+
* @param {string} pathSlash JSON-pointer-style path prefix for errors
|
|
704
|
+
* @param {string} pathBracket Human-readable bracket-path for errors
|
|
705
|
+
* @param {string[]} errors Accumulator
|
|
706
|
+
*/
|
|
707
|
+
function validateSettingsSection(section, pathSlash, pathBracket, errors) {
|
|
708
|
+
if (!isPlainObject(section)) {
|
|
709
|
+
errors.push(`${pathSlash}: must be an object`)
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
if (typeof section.title !== 'string') {
|
|
713
|
+
errors.push(`${pathSlash}/title: required, must be a string`)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// `manifest-settings-rich-sections` REQ-MSRS-1: exactly one of
|
|
717
|
+
// fields | component | widgets.
|
|
718
|
+
const hasFields = Array.isArray(section.fields)
|
|
719
|
+
const hasComponent = typeof section.component === 'string' && section.component.length > 0
|
|
720
|
+
const hasWidgets = Array.isArray(section.widgets) && section.widgets.length > 0
|
|
721
|
+
const bodyCount = (hasFields ? 1 : 0) + (hasComponent ? 1 : 0) + (hasWidgets ? 1 : 0)
|
|
722
|
+
|
|
723
|
+
if (bodyCount !== 1) {
|
|
724
|
+
errors.push(`${pathSlash}: ${pathBracket}: must declare exactly one of fields | component | widgets`)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// `widgets` set but not an array (string / object / etc.)
|
|
728
|
+
if (section.widgets !== undefined && !Array.isArray(section.widgets)) {
|
|
729
|
+
errors.push(`${pathSlash}/widgets: must be an array when set`)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// `component` set but not a string.
|
|
733
|
+
if (section.component !== undefined && typeof section.component !== 'string') {
|
|
734
|
+
errors.push(`${pathSlash}/component: must be a string when set`)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Per-widget shape rules.
|
|
738
|
+
if (hasWidgets) {
|
|
739
|
+
section.widgets.forEach((widget, wIndex) => {
|
|
740
|
+
if (!isPlainObject(widget)) {
|
|
741
|
+
errors.push(`${pathSlash}/widgets/${wIndex}: must be an object`)
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
if (typeof widget.type !== 'string' || widget.type.length === 0) {
|
|
745
|
+
errors.push(`${pathSlash}/widgets/${wIndex}/type: must be a non-empty string`)
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
// `manifest-settings-orchestration` REQ-MSO-6: when the
|
|
749
|
+
// discriminator is "component", `componentName` MUST be a
|
|
750
|
+
// non-empty string. Other widget types ignore
|
|
751
|
+
// `componentName`.
|
|
752
|
+
if (widget.type === 'component') {
|
|
753
|
+
if (typeof widget.componentName !== 'string' || widget.componentName.length === 0) {
|
|
754
|
+
errors.push(`${pathSlash}/widgets/${wIndex}/componentName: required when type is "component", must be a non-empty string`)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// `manifest-config-refs` REQ-MCR — when fields[] body is used,
|
|
761
|
+
// each entry must match the formField $def shape.
|
|
762
|
+
if (hasFields) {
|
|
763
|
+
validateFieldsArray(section.fields, `${pathSlash}/fields`, errors)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
672
767
|
/**
|
|
673
768
|
* Validate `config.sections[].fields[]` for settings page type
|
|
674
769
|
* (`manifest-config-refs` REQ-MCR). Each field MUST be an object with
|