@ditojs/admin 2.99.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3532 -2763
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +82 -46
  5. package/src/DitoComponent.js +1 -1
  6. package/src/DitoContext.js +51 -0
  7. package/src/DitoTypeComponent.js +6 -1
  8. package/src/components/DitoAffixes.vue +6 -3
  9. package/src/components/DitoContainer.vue +19 -10
  10. package/src/components/DitoCreateButton.vue +11 -2
  11. package/src/components/DitoDialog.vue +21 -2
  12. package/src/components/DitoForm.vue +156 -83
  13. package/src/components/DitoLabel.vue +3 -2
  14. package/src/components/DitoPane.vue +55 -11
  15. package/src/components/DitoPanel.vue +33 -5
  16. package/src/components/DitoPanels.vue +2 -1
  17. package/src/components/DitoRoot.vue +13 -6
  18. package/src/components/DitoSchema.vue +278 -145
  19. package/src/components/DitoSchemaInlined.vue +1 -1
  20. package/src/components/DitoTabs.vue +16 -18
  21. package/src/components/DitoTrail.vue +5 -4
  22. package/src/components/DitoTreeItem.vue +17 -6
  23. package/src/components/DitoView.vue +25 -2
  24. package/src/mixins/DitoMixin.js +42 -7
  25. package/src/mixins/ItemMixin.js +3 -6
  26. package/src/mixins/NumberMixin.js +47 -31
  27. package/src/mixins/OptionsMixin.js +122 -71
  28. package/src/mixins/ResourceMixin.js +5 -2
  29. package/src/mixins/RouteMixin.js +39 -1
  30. package/src/mixins/SortableMixin.js +0 -13
  31. package/src/mixins/SourceMixin.js +142 -95
  32. package/src/mixins/TextMixin.js +1 -1
  33. package/src/mixins/TypeMixin.js +37 -22
  34. package/src/mixins/ValidationMixin.js +9 -27
  35. package/src/mixins/ValidatorMixin.js +5 -7
  36. package/src/mixins/ValueMixin.js +5 -2
  37. package/src/types/DitoTypeButton.vue +1 -1
  38. package/src/types/DitoTypeCheckbox.vue +1 -1
  39. package/src/types/DitoTypeCheckboxes.vue +1 -1
  40. package/src/types/DitoTypeCode.vue +8 -1
  41. package/src/types/DitoTypeColor.vue +20 -34
  42. package/src/types/DitoTypeComponent.vue +1 -1
  43. package/src/types/DitoTypeComputed.vue +4 -27
  44. package/src/types/DitoTypeDate.vue +16 -5
  45. package/src/types/DitoTypeLabel.vue +1 -4
  46. package/src/types/DitoTypeList.vue +9 -5
  47. package/src/types/DitoTypeMarkup.vue +25 -13
  48. package/src/types/DitoTypeMultiselect.vue +26 -6
  49. package/src/types/DitoTypeNumber.vue +1 -1
  50. package/src/types/DitoTypeObject.vue +10 -7
  51. package/src/types/DitoTypePanel.vue +19 -6
  52. package/src/types/DitoTypeProgress.vue +1 -1
  53. package/src/types/DitoTypeRadio.vue +1 -1
  54. package/src/types/DitoTypeSection.vue +32 -4
  55. package/src/types/DitoTypeSelect.vue +1 -1
  56. package/src/types/DitoTypeSlider.vue +1 -1
  57. package/src/types/DitoTypeSwitch.vue +1 -1
  58. package/src/types/DitoTypeText.vue +13 -15
  59. package/src/types/DitoTypeTextarea.vue +1 -1
  60. package/src/types/DitoTypeTreeList.vue +12 -7
  61. package/src/types/DitoTypeUpload.vue +1 -2
  62. package/src/utils/DataSchemaResolver.js +141 -0
  63. package/src/utils/DataSchemaResolver.test.js +132 -0
  64. package/src/utils/FormModel.js +556 -0
  65. package/src/utils/FormModel.test.js +808 -0
  66. package/src/utils/data.js +20 -0
  67. package/src/utils/data.test.js +28 -0
  68. package/src/utils/filter.js +3 -2
  69. package/src/utils/options.js +3 -0
  70. package/src/utils/schema/data.js +617 -0
  71. package/src/utils/schema/data.test.js +471 -0
  72. package/src/utils/schema/lookup.js +266 -0
  73. package/src/utils/schema/lookup.test.js +61 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/structure.test.js +57 -0
  77. package/src/utils/schema/types.js +34 -0
  78. package/src/utils/schema/validation.js +142 -0
  79. package/src/utils/schema/validation.test.js +395 -0
  80. package/src/utils/units.js +70 -3
  81. package/src/utils/units.test.js +71 -0
  82. package/types/index.d.ts +149 -59
  83. package/src/mixins/DataMixin.js +0 -131
  84. package/src/utils/schema.js +0 -1121
@@ -0,0 +1,266 @@
1
+ import { appendDataPath } from '../data.js'
2
+ import {
3
+ isObject,
4
+ isString,
5
+ isArray,
6
+ isFunction,
7
+ camelize
8
+ } from '@ditojs/utils'
9
+ import { reactive } from 'vue'
10
+ import { getTypeOptions } from './types.js'
11
+ import {
12
+ findNestedSchemaComponent,
13
+ isSchema,
14
+ isSingleComponentView
15
+ } from './structure.js'
16
+ // TODO: `getFormSchemas()` processes forms with `components()` callbacks on
17
+ // every call, which also causes the only circular import between these
18
+ // modules. Move this out of the lookup, see the form model.
19
+ import { setupForm } from './setup.js'
20
+
21
+ const emptySchema = {}
22
+
23
+ export function hasFormSchema(schema) {
24
+ // Support both single form and multiple forms notation, as well as inlined
25
+ // components.
26
+ return (
27
+ isSchema(schema) &&
28
+ isObject(schema.form || schema.forms || schema.components)
29
+ )
30
+ }
31
+
32
+ export function hasMultipleFormSchemas(schema) {
33
+ return (
34
+ isSchema(schema) &&
35
+ Object.keys(schema?.forms || {}).length > 1
36
+ )
37
+ }
38
+
39
+ export function getViewFormSchema(schema, context) {
40
+ const { view } = schema
41
+ const viewSchema = view && context.flattenedViews[view]
42
+ return viewSchema
43
+ ? // NOTE: Views can have tabs, in which case the view component is nested
44
+ // in one of the tabs, go find it.
45
+ findNestedSchemaComponent(viewSchema, hasFormSchema) || null
46
+ : null
47
+ }
48
+
49
+ export function getViewSchema(schema, context) {
50
+ return getViewFormSchema(schema, context)
51
+ ? context.flattenedViews[schema.view]
52
+ : null
53
+ }
54
+
55
+ export function hasViewSchema(schema, context) {
56
+ return !!getViewSchema(schema, context)
57
+ }
58
+
59
+ export function getViewPath(schema, context) {
60
+ const view = getViewSchema(schema, context)
61
+ if (view) {
62
+ return isSingleComponentView(view)
63
+ ? view.fullPath
64
+ : `${view.fullPath}/${view.path}`
65
+ }
66
+ return null
67
+ }
68
+
69
+ export function getViewEditPath(schema, id, context) {
70
+ const path = getViewPath(schema, context)
71
+ return path ? `${path}/${id}` : null
72
+ }
73
+
74
+ export function getFormSchemas(schema, context, modifyForm) {
75
+ const viewSchema = context && getViewFormSchema(schema, context)
76
+ if (viewSchema) {
77
+ schema = viewSchema
78
+ } else if (schema.view) {
79
+ throw new Error(`Unknown view: '${schema.view}'`)
80
+ }
81
+
82
+ let { form, forms } = schema
83
+ if (!form && !forms) {
84
+ const { name, compact, clipboard, tabs, components } = schema
85
+ if (components || tabs) {
86
+ // Convert inlined forms to stand-alone forms, supporting `name`,
87
+ // `compact`, `clipboard`, `tabs` and `components` settings.
88
+ form = { type: 'form', name, compact, clipboard, tabs, components }
89
+ } else {
90
+ // No `forms`, `form` or `components`, return and empty `forms` object.
91
+ return {}
92
+ }
93
+ }
94
+ forms ||= { default: form }
95
+ return Object.fromEntries(
96
+ Object.entries(forms).map(([type, form]) => {
97
+ // Support `schema.components` callbacks to create components on the fly.
98
+ if (context && isFunction(form.components)) {
99
+ // Make the form schema reactive since `setupForm()` is async, so that
100
+ // the setting of defaults will be picked up by downstream code.
101
+ form = reactive({
102
+ ...form,
103
+ components: form.components(context)
104
+ })
105
+ // Process the form again, now that we have the components.
106
+ setupForm(context.api, form).catch(console.error)
107
+ }
108
+ return [type, modifyForm?.(form) ?? form]
109
+ })
110
+ )
111
+ }
112
+
113
+ export function getItemFormSchemaFromForms(forms, item) {
114
+ return forms[item?.type] || forms.default || null
115
+ }
116
+
117
+ export function getItemFormSchema(schema, item, context) {
118
+ return (
119
+ getItemFormSchemaFromForms(getFormSchemas(schema, context), item) ||
120
+ // Always return a schema object so we don't need to check for it.
121
+ emptySchema
122
+ )
123
+ }
124
+
125
+ export function isEmptySchema(schema) {
126
+ return schema === emptySchema
127
+ }
128
+
129
+ export function getNamedSchemas(schemas, defaults) {
130
+ const toObject = (array, toSchema) => {
131
+ return array.length > 0
132
+ ? array.reduce((object, value) => {
133
+ const schema = toSchema(value)
134
+ if (schema) {
135
+ object[schema.name] =
136
+ schema && defaults
137
+ ? { ...defaults, ...schema }
138
+ : schema
139
+ }
140
+ return object
141
+ }, {})
142
+ : null
143
+ }
144
+
145
+ return isArray(schemas)
146
+ ? toObject(schemas, value =>
147
+ isObject(value)
148
+ ? value
149
+ : {
150
+ name: camelize(value, false)
151
+ }
152
+ )
153
+ : isObject(schemas)
154
+ ? toObject(
155
+ Object.entries(schemas),
156
+ ([name, value]) =>
157
+ isObject(value)
158
+ ? {
159
+ name,
160
+ ...value
161
+ }
162
+ : isString(value)
163
+ ? {
164
+ name,
165
+ label: value
166
+ }
167
+ : null
168
+ )
169
+ : null
170
+ }
171
+
172
+ export function getButtonSchemas(buttons) {
173
+ return getNamedSchemas(
174
+ buttons,
175
+ { type: 'button' } // Defaults
176
+ )
177
+ }
178
+
179
+ export function getPanelEntry(
180
+ schema,
181
+ dataPath = null,
182
+ componentPath = '',
183
+ tabComponent = null
184
+ ) {
185
+ return schema
186
+ ? {
187
+ schema,
188
+ // If the panel provides its own name, append it to the paths. This is
189
+ // used e.g. for $filters panels.
190
+ dataPath:
191
+ dataPath != null && schema.name
192
+ ? appendDataPath(dataPath, schema.name)
193
+ : dataPath,
194
+ componentPath: schema.name
195
+ ? appendDataPath(componentPath, schema.name)
196
+ : componentPath,
197
+ tabComponent
198
+ }
199
+ : null
200
+ }
201
+
202
+ export function getPanelEntries(
203
+ panelSchemas,
204
+ dataPath,
205
+ componentPath,
206
+ tabComponent = null,
207
+ panelEntries = []
208
+ ) {
209
+ if (panelSchemas) {
210
+ for (const [key, schema] of Object.entries(panelSchemas)) {
211
+ const entry = getPanelEntry(
212
+ schema,
213
+ appendDataPath(dataPath, key),
214
+ appendDataPath(componentPath, key),
215
+ tabComponent
216
+ )
217
+ if (entry) {
218
+ panelEntries.push(entry)
219
+ }
220
+ }
221
+ }
222
+ return panelEntries
223
+ }
224
+
225
+ // Returns the entries of the panel that the type of the component described by
226
+ // `schema` provides, and of the panels in `schema.panels`. The type's panel is
227
+ // addressed relative to `dataPath` and its component path,
228
+ // `dataPathComponentPath`, while the panels in `schema.panels` continue
229
+ // `schemaComponentPath`, the component path of `schema` itself, which also
230
+ // contains the names of unnested components.
231
+ export function getAllPanelEntries(
232
+ api,
233
+ schema,
234
+ dataPath,
235
+ dataPathComponentPath,
236
+ schemaComponentPath,
237
+ component = null,
238
+ tabComponent = null
239
+ ) {
240
+ const panelSchema = getTypeOptions(schema)?.getPanelSchema?.(
241
+ api,
242
+ schema,
243
+ dataPath,
244
+ component
245
+ )
246
+ const panelEntries = panelSchema
247
+ ? [
248
+ getPanelEntry(
249
+ panelSchema,
250
+ dataPath,
251
+ dataPathComponentPath,
252
+ tabComponent
253
+ )
254
+ ]
255
+ : []
256
+ // Allow each component to provide its own set of panels, in
257
+ // addition to the default one (e.g. getFiltersPanel(), $filters):
258
+ getPanelEntries(
259
+ schema?.panels,
260
+ dataPath,
261
+ schemaComponentPath,
262
+ tabComponent,
263
+ panelEntries
264
+ )
265
+ return panelEntries
266
+ }
@@ -0,0 +1,61 @@
1
+ import { registerTypeComponent } from './types.js'
2
+ import { getAllPanelEntries } from './lookup.js'
3
+
4
+ // Register minimal type options, as the actual type components can't be
5
+ // imported without a Vue SFC compiler:
6
+ registerTypeComponent('panel', {
7
+ defaultNested: false,
8
+ getPanelSchema: (api, schema) => schema
9
+ })
10
+ registerTypeComponent('list', {
11
+ defaultNested: true,
12
+ getPanelSchema: () => ({ type: 'panel', name: '$filters' })
13
+ })
14
+ registerTypeComponent('section', { defaultNested: false })
15
+
16
+ describe('getAllPanelEntries()', () => {
17
+ const getPaths = entries =>
18
+ entries.map(({ dataPath, componentPath }) => ({ dataPath, componentPath }))
19
+
20
+ it('adds the names of panel components once to their paths', () => {
21
+ // Unnested components add their name to their component path, but not to
22
+ // their data path, so the panel is addressed relative to the data path.
23
+ const links = { type: 'panel', name: 'links' }
24
+ expect(
25
+ getPaths(getAllPanelEntries(null, links, 'book', 'main', 'main/links'))
26
+ ).toEqual([{ dataPath: 'book/links', componentPath: 'main/links' }])
27
+ })
28
+
29
+ it('continues the paths of nested components for type panels', () => {
30
+ const books = { type: 'list', name: 'books' }
31
+ expect(
32
+ getPaths(
33
+ getAllPanelEntries(
34
+ null,
35
+ books,
36
+ 'shelf/books',
37
+ 'main/books',
38
+ 'main/books'
39
+ )
40
+ )
41
+ ).toEqual([
42
+ {
43
+ dataPath: 'shelf/books/$filters',
44
+ componentPath: 'main/books/$filters'
45
+ }
46
+ ])
47
+ })
48
+
49
+ it('continues the component path of components for their panels', () => {
50
+ const section = {
51
+ type: 'section',
52
+ name: 'section',
53
+ panels: { info: { type: 'panel' } }
54
+ }
55
+ expect(
56
+ getPaths(
57
+ getAllPanelEntries(null, section, 'book', 'main', 'main/section')
58
+ )
59
+ ).toEqual([{ dataPath: 'book/info', componentPath: 'main/section/info' }])
60
+ })
61
+ })
@@ -0,0 +1,354 @@
1
+ import DitoMixin from '../../mixins/DitoMixin.js'
2
+ import TypeMixin from '../../mixins/TypeMixin.js'
3
+ import {
4
+ isObject,
5
+ isArray,
6
+ isFunction,
7
+ isPromise,
8
+ isModule,
9
+ camelize,
10
+ assignDeeply,
11
+ mapConcurrently
12
+ } from '@ditojs/utils'
13
+ import { markRaw } from 'vue'
14
+ import { getTypeOptions } from './types.js'
15
+ import {
16
+ iterateSchemaComponents,
17
+ iterateNestedSchemaComponents,
18
+ isForm,
19
+ isView,
20
+ isTab,
21
+ isPanel,
22
+ isMenu,
23
+ getSchemaIdentifier,
24
+ getPanelSchemas
25
+ } from './structure.js'
26
+ import { getFormSchemas } from './lookup.js'
27
+
28
+ const resolvedSchemas = new WeakMap()
29
+
30
+ export async function resolveSchema(value, unwrapModule = false) {
31
+ if (resolvedSchemas.has(value)) {
32
+ return resolvedSchemas.get(value)
33
+ }
34
+ let schema = value
35
+ if (isFunction(schema)) {
36
+ schema = schema()
37
+ }
38
+ if (isPromise(schema)) {
39
+ schema = await schema
40
+ }
41
+ if (isModule(schema)) {
42
+ // Copy to convert from module to object:
43
+ schema = { ...schema }
44
+ // Unwrap default or named schema
45
+ if (!schema.name && (unwrapModule || schema.default)) {
46
+ // Filter out internal key added by vite / vue 3 plugin when changing
47
+ // code in a dynamically imported vue component, see:
48
+ // https://github.com/vitejs/vite-plugin-vue/blob/abdf5f4f32d02af641e5f60871bde14535569b1e/packages/plugin-vue/src/main.ts#L133
49
+ const keys = Object.keys(schema).filter(key => key !== '_rerender_only')
50
+ if (keys.length === 1) {
51
+ const name = keys[0]
52
+ schema = schema[name]
53
+ if (name !== 'default') {
54
+ schema = { ...schema, name }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ resolvedSchemas.set(value, schema)
60
+ return schema
61
+ }
62
+
63
+ export async function resolveSchemas(
64
+ unresolvedSchemas,
65
+ resolveItem = resolveSchema
66
+ ) {
67
+ let schemas = isFunction(unresolvedSchemas)
68
+ ? unresolvedSchemas()
69
+ : unresolvedSchemas
70
+ schemas = await resolveSchema(schemas, false)
71
+ if (isArray(schemas)) {
72
+ // Translate an array of dynamic import, each importing one named schema
73
+ // module to an object with named entries.
74
+ schemas = Object.fromEntries(
75
+ await mapConcurrently(
76
+ schemas,
77
+ async item => {
78
+ const schema = await resolveItem(item, true)
79
+ return [schema.name, schema]
80
+ }
81
+ )
82
+ )
83
+ } else if (isObject(schemas)) {
84
+ schemas = Object.fromEntries(
85
+ await mapConcurrently(
86
+ Object.entries(schemas),
87
+ async ([key, item]) => {
88
+ const schema = await resolveItem(item, true)
89
+ return [key, schema]
90
+ }
91
+ )
92
+ )
93
+ }
94
+ return schemas
95
+ }
96
+
97
+ export async function resolveViews(unresolvedViews) {
98
+ return resolveSchemas(unresolvedViews, async (schema, unwrapModule) => {
99
+ schema = await resolveSchema(schema, unwrapModule)
100
+ if (!schema.name && isMenu(schema)) {
101
+ // Generate a name for sub-menus from their label if it's missing.
102
+ // NOTE: This is never actually referenced from anywhere, but they need
103
+ // a name by which they're stored in the parent object.
104
+ schema = {
105
+ ...schema,
106
+ name: camelize(schema.label),
107
+ items: await resolveSchemas(schema.items)
108
+ }
109
+ }
110
+ return schema
111
+ })
112
+ }
113
+
114
+ export function flattenViews(views) {
115
+ return Object.fromEntries(
116
+ Object.entries(views).reduce(
117
+ (entries, [key, schema]) => {
118
+ if (isMenu(schema)) {
119
+ entries.push(...Object.entries(schema.items))
120
+ } else {
121
+ entries.push([key, schema])
122
+ }
123
+ return entries
124
+ },
125
+ []
126
+ )
127
+ )
128
+ }
129
+
130
+ export async function resolveSchemaComponent(schema) {
131
+ // Resolves async schema components and adds DitoMixin and TypeMixin to them.
132
+ let { component } = schema
133
+ if (component) {
134
+ component = await resolveSchema(component, true)
135
+ if (component) {
136
+ // Prevent warning: "Vue received a Component which was made a reactive
137
+ // object. This can lead to unnecessary performance overhead, and should
138
+ // be avoided by marking the component with `markRaw`":
139
+ schema.component = markRaw({
140
+ ...component,
141
+ mixins: [DitoMixin, TypeMixin, ...(component.mixins || [])]
142
+ })
143
+ }
144
+ }
145
+ }
146
+
147
+ export async function resolveSchemaComponents(schemas) {
148
+ // `schemas` are of the same possible forms as passed to `getNamedSchemas()`
149
+ await mapConcurrently(Object.values(schemas || {}), resolveSchemaComponent)
150
+ }
151
+
152
+ const processedSchemaDepths = new WeakMap()
153
+
154
+ export function setupSchemaComponents(
155
+ api,
156
+ schema,
157
+ routes = null,
158
+ level = 0,
159
+ maxDepth = 1
160
+ ) {
161
+ if (schema) {
162
+ const depth = processedSchemaDepths.get(schema) ?? 0
163
+ if (depth < maxDepth) {
164
+ processedSchemaDepths.set(schema, depth + 1)
165
+ const promises = []
166
+ const process = (component, name, relativeLevel) => {
167
+ promises.push(
168
+ setupSchemaComponent(
169
+ api,
170
+ component,
171
+ name,
172
+ routes,
173
+ level + relativeLevel
174
+ )
175
+ )
176
+ }
177
+
178
+ iterateNestedSchemaComponents(schema, process)
179
+ iterateSchemaComponents(getPanelSchemas(schema), process)
180
+
181
+ return Promise.all(promises)
182
+ }
183
+ }
184
+ }
185
+
186
+ export function setupSchemaComponent(
187
+ api,
188
+ schema,
189
+ name,
190
+ routes = null,
191
+ level = 0
192
+ ) {
193
+ applySchemaDefaults(api, schema)
194
+
195
+ return Promise.all([
196
+ // Also process nested panel schemas.
197
+ mapConcurrently(
198
+ getPanelSchemas(schema),
199
+ panel => setupSchemaComponents(api, panel, routes, level)
200
+ ),
201
+ // Delegate schema processing to the actual type components.
202
+ getTypeOptions(schema)?.processSchema?.(
203
+ api,
204
+ schema,
205
+ name,
206
+ routes,
207
+ level
208
+ )
209
+ ])
210
+ }
211
+
212
+ export async function setupView(component, api, schema, name, fullPath = '') {
213
+ applySchemaDefaults(api, schema)
214
+ setupRouteSchema(api, schema, name, fullPath)
215
+ let children = []
216
+ if (isView(schema)) {
217
+ await setupNestedSchemas(api, schema)
218
+ await setupSchemaComponents(api, schema, children)
219
+ } else if (isMenu(schema)) {
220
+ children = await Promise.all(
221
+ Object.entries(schema.items).map(async ([name, item]) =>
222
+ setupView(component, api, item, name, schema.fullPath)
223
+ )
224
+ )
225
+ } else {
226
+ throw new Error(`Invalid view schema: '${getSchemaIdentifier(schema)}'`)
227
+ }
228
+ return {
229
+ path: schema.fullPath,
230
+ children,
231
+ component,
232
+ meta: {
233
+ api,
234
+ schema
235
+ }
236
+ }
237
+ }
238
+
239
+ export function applySchemaDefaults(api, schema) {
240
+ let defaults = (
241
+ api.defaults[schema.type] ||
242
+ api.defaults[camelize(schema.type)]
243
+ )
244
+ if (defaults) {
245
+ if (isFunction(defaults)) {
246
+ defaults = defaults(schema)
247
+ }
248
+ if (isObject(defaults)) {
249
+ for (const [key, value] of Object.entries(defaults)) {
250
+ if (schema[key] === undefined) {
251
+ schema[key] = value
252
+ } else {
253
+ schema[key] = assignDeeply(schema[key], value)
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ export function applyNestedSchemaDefaults(api, schema) {
261
+ // Process defaults for nested schemas. Note that this is also done when
262
+ // calling `setupSchemaComponents()`, but that function is async, and we
263
+ // need a sync version that only handles the defaults for filters, see
264
+ // `getFiltersPanel()`.
265
+ iterateNestedSchemaComponents(schema, component => {
266
+ applySchemaDefaults(api, component)
267
+ const forms = getFormSchemas(component)
268
+ for (const form of Object.values(forms)) {
269
+ applyNestedSchemaDefaults(api, form)
270
+ }
271
+ })
272
+ }
273
+
274
+ export function setupRouteSchema(api, schema, name, fullPath = null) {
275
+ // Used for view and source schemas, see SourceMixin.
276
+ schema.name ??= name
277
+ schema.path ??= api.normalizePath(name)
278
+ if (fullPath !== null) {
279
+ schema.fullPath = `${fullPath}/${schema.path}`
280
+ }
281
+ }
282
+
283
+ export async function setupForms(api, schema, level) {
284
+ const routes = []
285
+ // First resolve the forms and store the results back on the schema.
286
+ const { form, forms, components, maxDepth = 1 } = schema
287
+ if (forms) {
288
+ schema.forms = await resolveSchemas(forms, form =>
289
+ setupForm(api, form, routes, level, maxDepth)
290
+ )
291
+ } else if (form) {
292
+ schema.form = await setupForm(api, form, routes, level, maxDepth)
293
+ } else if (isObject(components)) {
294
+ // NOTE: Processing forms in computed components is not supported, since it
295
+ // only can be computed in conjunction with actual data.
296
+ const form = {
297
+ type: 'form',
298
+ components
299
+ }
300
+ await setupForm(api, form, routes, level, maxDepth)
301
+ }
302
+ return routes
303
+ }
304
+
305
+ export async function setupForm(
306
+ api,
307
+ schema,
308
+ routes = null,
309
+ level = 0,
310
+ maxDepth = 1
311
+ ) {
312
+ schema = await resolveSchema(schema, true)
313
+ if (!isForm(schema)) {
314
+ throw new Error(`Invalid form schema: '${getSchemaIdentifier(schema)}'`)
315
+ }
316
+ applySchemaDefaults(api, schema)
317
+ await setupNestedSchemas(api, schema)
318
+ await setupSchemaComponents(api, schema, routes, level, maxDepth)
319
+ return schema
320
+ }
321
+
322
+ export async function setupTab(api, schema) {
323
+ schema = await resolveSchema(schema, true)
324
+ if (!isTab(schema)) {
325
+ throw new Error(`Invalid tab schema: '${getSchemaIdentifier(schema)}'`)
326
+ }
327
+ applySchemaDefaults(api, schema)
328
+ return schema
329
+ }
330
+
331
+ export async function setupPanel(api, schema) {
332
+ schema = await resolveSchema(schema, true)
333
+ if (!isPanel(schema)) {
334
+ throw new Error(`Invalid panel schema: '${getSchemaIdentifier(schema)}'`)
335
+ }
336
+ applySchemaDefaults(api, schema)
337
+ return schema
338
+ }
339
+
340
+ export async function setupNestedSchemas(api, schema) {
341
+ const { tabs, panels } = schema
342
+ if (tabs) {
343
+ schema.tabs = await resolveSchemas(
344
+ tabs,
345
+ tab => setupTab(api, tab)
346
+ )
347
+ }
348
+ if (panels) {
349
+ schema.panels = await resolveSchemas(
350
+ panels,
351
+ panel => setupPanel(api, panel)
352
+ )
353
+ }
354
+ }