@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.
@@ -178,7 +178,70 @@ function validateTypeConfig(page, index, errors) {
178
178
  break
179
179
  }
180
180
  case 'settings': {
181
- if (!cfg || !Array.isArray(cfg.sections)) {
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
- if (!isPlainObject(section)) {
191
- errors.push(`${pathSlash}/sections/${sIndex}: must be an object`)
192
- return
193
- }
194
- if (typeof section.title !== 'string') {
195
- errors.push(`${pathSlash}/sections/${sIndex}/title: required, must be a string`)
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