@ditojs/admin 2.100.0 → 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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3421 -2662
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +81 -45
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +142 -95
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- 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
|
+
}
|