@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
package/src/utils/data.js
CHANGED
|
@@ -11,6 +11,26 @@ export function appendDataPath(dataPath, token) {
|
|
|
11
11
|
: token
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Returns the part of `dataPath` inside `parentDataPath`, e.g. `0/title` for
|
|
16
|
+
* `chapters/0/title` in `chapters`, an empty string if both are the same, or
|
|
17
|
+
* `null` if `dataPath` isn't inside `parentDataPath`.
|
|
18
|
+
*/
|
|
19
|
+
export function getRelativeDataPath(dataPath, parentDataPath) {
|
|
20
|
+
if (dataPath == null || parentDataPath == null) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
const path = normalizeDataPath(dataPath)
|
|
24
|
+
const parentPath = normalizeDataPath(parentDataPath)
|
|
25
|
+
return path === parentPath
|
|
26
|
+
? ''
|
|
27
|
+
: !parentPath
|
|
28
|
+
? path
|
|
29
|
+
: path.startsWith(`${parentPath}/`)
|
|
30
|
+
? path.slice(parentPath.length + 1)
|
|
31
|
+
: null
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
export function parseParentDataPath(dataPath) {
|
|
15
35
|
const path = parseDataPath(dataPath)
|
|
16
36
|
path?.pop()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getRelativeDataPath } from './data.js'
|
|
2
|
+
|
|
3
|
+
describe('getRelativeDataPath()', () => {
|
|
4
|
+
it('returns the part of the data path inside the parent data path', () => {
|
|
5
|
+
expect(getRelativeDataPath('chapters/0/title', 'chapters')).toBe(
|
|
6
|
+
'0/title'
|
|
7
|
+
)
|
|
8
|
+
expect(getRelativeDataPath('chapters/0', '')).toBe('chapters/0')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('returns an empty string for the same data paths', () => {
|
|
12
|
+
expect(getRelativeDataPath('chapters', 'chapters')).toBe('')
|
|
13
|
+
expect(getRelativeDataPath('', '')).toBe('')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns `null` for data paths outside the parent data path', () => {
|
|
17
|
+
expect(getRelativeDataPath('chapters', 'chapters/0')).toBe(null)
|
|
18
|
+
expect(getRelativeDataPath('chaptersList/0', 'chapters')).toBe(null)
|
|
19
|
+
expect(getRelativeDataPath('chapters', undefined)).toBe(null)
|
|
20
|
+
expect(getRelativeDataPath(undefined, 'chapters')).toBe(null)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('normalizes the data paths', () => {
|
|
24
|
+
expect(getRelativeDataPath('chapters[0].title', 'chapters/')).toBe(
|
|
25
|
+
'0/title'
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
})
|
package/src/utils/filter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isArray, asArray, labelize } from '@ditojs/utils'
|
|
2
|
-
import { getNamedSchemas
|
|
2
|
+
import { getNamedSchemas } from './schema/lookup.js'
|
|
3
|
+
import { applyNestedSchemaDefaults } from './schema/setup.js'
|
|
3
4
|
|
|
4
5
|
export const filterComponents = {
|
|
5
6
|
'text'(filter) {
|
|
@@ -119,7 +120,7 @@ export function createFiltersPanel(api, filters, dataPath, query) {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
applyNestedSchemaDefaults(api, panel)
|
|
123
124
|
return panel
|
|
124
125
|
}
|
|
125
126
|
|
package/src/utils/options.js
CHANGED
|
@@ -36,8 +36,11 @@ const ditoOptionKeys = [
|
|
|
36
36
|
'generateLabel',
|
|
37
37
|
'excludeValue',
|
|
38
38
|
'ignoreMissingValue',
|
|
39
|
+
'treatNullAsMissing',
|
|
40
|
+
'valueFromDataSchema',
|
|
39
41
|
'omitSpacing',
|
|
40
42
|
'processValue',
|
|
43
|
+
'getTypeValidations',
|
|
41
44
|
'processSchema',
|
|
42
45
|
'getPanelSchema',
|
|
43
46
|
'getFormSchemasForProcessing',
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import DitoContext from '../../DitoContext.js'
|
|
2
|
+
import { getUid } from '../uid.js'
|
|
3
|
+
import { SchemaGraph } from '../SchemaGraph.js'
|
|
4
|
+
import { appendDataPath, getRelativeDataPath } from '../data.js'
|
|
5
|
+
import { isMatchingType, convertType } from '../type.js'
|
|
6
|
+
import {
|
|
7
|
+
isObject,
|
|
8
|
+
isArray,
|
|
9
|
+
isFunction,
|
|
10
|
+
asArray,
|
|
11
|
+
clone,
|
|
12
|
+
getValueAtDataPath
|
|
13
|
+
} from '@ditojs/utils'
|
|
14
|
+
import { getTypeOptions, getSourceType } from './types.js'
|
|
15
|
+
import {
|
|
16
|
+
someNestedSchemaComponent,
|
|
17
|
+
hasNestedSchemaComponents,
|
|
18
|
+
isPanelWithOwnData,
|
|
19
|
+
isNested
|
|
20
|
+
} from './structure.js'
|
|
21
|
+
import {
|
|
22
|
+
hasFormSchema,
|
|
23
|
+
hasMultipleFormSchemas,
|
|
24
|
+
getFormSchemas,
|
|
25
|
+
getItemFormSchemaFromForms
|
|
26
|
+
} from './lookup.js'
|
|
27
|
+
|
|
28
|
+
export function getSchemaValue(
|
|
29
|
+
keyOrDataPath,
|
|
30
|
+
{ type, schema, callback = true, default: def, context } = {}
|
|
31
|
+
) {
|
|
32
|
+
const types = type && asArray(type)
|
|
33
|
+
// For performance reasons, data-paths in `keyOrDataPath` can only be
|
|
34
|
+
// provided in in array format here:
|
|
35
|
+
let value = schema
|
|
36
|
+
? isArray(keyOrDataPath)
|
|
37
|
+
? getValueAtDataPath(schema, keyOrDataPath, () => undefined)
|
|
38
|
+
: schema[keyOrDataPath]
|
|
39
|
+
: undefined
|
|
40
|
+
|
|
41
|
+
if (value === undefined && def !== undefined) {
|
|
42
|
+
if (callback && isFunction(def) && !isMatchingType(types, def)) {
|
|
43
|
+
// Support `default()` functions for any type except `Function`:
|
|
44
|
+
def = def(context)
|
|
45
|
+
}
|
|
46
|
+
return def
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isMatchingType(types, value)) {
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
// Any schema value handled through `getSchemaValue()` can provide
|
|
53
|
+
// a function that's resolved when the value is evaluated:
|
|
54
|
+
if (callback && isFunction(value)) {
|
|
55
|
+
value = value(context)
|
|
56
|
+
}
|
|
57
|
+
// Now finally see if we can convert to the expect types.
|
|
58
|
+
if (types && value != null && !isMatchingType(types, value)) {
|
|
59
|
+
for (const type of types) {
|
|
60
|
+
const converted = convertType(type, value)
|
|
61
|
+
if (converted !== value) {
|
|
62
|
+
return converted
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function shouldRenderSchema(schema, context) {
|
|
70
|
+
return (
|
|
71
|
+
getSchemaValue('if', {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
schema,
|
|
74
|
+
context,
|
|
75
|
+
default: true
|
|
76
|
+
}) && (
|
|
77
|
+
!hasNestedSchemaComponents(schema) ||
|
|
78
|
+
// The components of sources, e.g. lists with inlined components, are
|
|
79
|
+
// rendered for each item, with the item's own context:
|
|
80
|
+
!!getSourceType(schema) ||
|
|
81
|
+
// Evaluate the components with their own contexts, like when rendered:
|
|
82
|
+
someNestedSchemaComponent(schema, (component, name) =>
|
|
83
|
+
shouldRenderSchema(
|
|
84
|
+
component,
|
|
85
|
+
context.createChildContext(component, name)
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getContext(context) {
|
|
93
|
+
return isFunction(context) ? context() : context
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getDefaultValue(schema, context) {
|
|
97
|
+
// Support default values both on schema and on component level.
|
|
98
|
+
// NOTE: At the time of creation, components may not be instantiated, (e.g. if
|
|
99
|
+
// entries are created through nested forms, the parent form isn't mounted) so
|
|
100
|
+
// we can't use `dataPath` to get to components, and the `defaultValue` from
|
|
101
|
+
// there. That's why `defaultValue` is defined statically in the components:
|
|
102
|
+
const defaultValue =
|
|
103
|
+
schema.default !== undefined
|
|
104
|
+
? schema.default
|
|
105
|
+
: getTypeOptions(schema)?.defaultValue
|
|
106
|
+
return isFunction(defaultValue)
|
|
107
|
+
? defaultValue(getContext(context))
|
|
108
|
+
: clone(defaultValue)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function shouldExcludeValue(schema, context) {
|
|
112
|
+
const excludeValue =
|
|
113
|
+
schema.exclude !== undefined
|
|
114
|
+
? schema.exclude
|
|
115
|
+
: getTypeOptions(schema)?.excludeValue
|
|
116
|
+
return isFunction(excludeValue)
|
|
117
|
+
? excludeValue(getContext(context))
|
|
118
|
+
: !!excludeValue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function shouldIgnoreMissingValue(schema, context) {
|
|
122
|
+
return !!getTypeOptions(schema)?.ignoreMissingValue?.(getContext(context))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Returns whether the value of the component described by `schema` and `name`
|
|
126
|
+
// is missing in `data`: if there's no value, or if the value is `null` and
|
|
127
|
+
// the type treats that as missing, e.g. nested sections.
|
|
128
|
+
export function isMissingValue(schema, data, name, context) {
|
|
129
|
+
return (
|
|
130
|
+
!(name in data) ||
|
|
131
|
+
data[name] === null &&
|
|
132
|
+
!!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context))
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns whether the value of the component described by `schema` is
|
|
138
|
+
* resolved from its data schema, `schema.data` or `schema.dataPath`, as for
|
|
139
|
+
* the types with the `valueFromDataSchema` option, e.g. `computed`. See
|
|
140
|
+
* `FormModel`.
|
|
141
|
+
*/
|
|
142
|
+
export function hasValueFromDataSchema(schema) {
|
|
143
|
+
return (
|
|
144
|
+
!!getTypeOptions(schema)?.valueFromDataSchema &&
|
|
145
|
+
!!(schema.data || schema.dataPath)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getMultipleValue(schema) {
|
|
150
|
+
return schema.multiple ?? !!getTypeOptions(schema)?.defaultMultiple
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Initializes `data` for the components of `schema`: Sets missing values to
|
|
155
|
+
* their defaults, so they can be correctly watched for changes, and numbers
|
|
156
|
+
* the items of lists by their order key, see `updateOrder()`, so that their
|
|
157
|
+
* order is stored even if it never changes.
|
|
158
|
+
*/
|
|
159
|
+
export function initializeData(schema, data = {}, component, {
|
|
160
|
+
dataPath = null,
|
|
161
|
+
rootData = data,
|
|
162
|
+
shouldProcess,
|
|
163
|
+
// Whether to also set the defaults of components with `compute()`. New data
|
|
164
|
+
// starts with all defaults, which `compute()` can rely on. In data that was
|
|
165
|
+
// loaded, `compute()` may derive missing values instead, and they fall back
|
|
166
|
+
// to their defaults only if it doesn't return a value, see `computeValue()`.
|
|
167
|
+
shouldSetDefaultsOfComponentsWithCompute = true
|
|
168
|
+
} = {}) {
|
|
169
|
+
const options = { component, rootData }
|
|
170
|
+
|
|
171
|
+
const before = ({ schema, data, name, dataPath }) => {
|
|
172
|
+
const context = () =>
|
|
173
|
+
new DitoContext(component, {
|
|
174
|
+
schema,
|
|
175
|
+
name,
|
|
176
|
+
data,
|
|
177
|
+
dataPath,
|
|
178
|
+
rootData
|
|
179
|
+
})
|
|
180
|
+
if (
|
|
181
|
+
(shouldSetDefaultsOfComponentsWithCompute || !schema.compute) &&
|
|
182
|
+
isMissingValue(schema, data, name, context) &&
|
|
183
|
+
!shouldIgnoreMissingValue(schema, context)
|
|
184
|
+
) {
|
|
185
|
+
data[name] = getDefaultValue(schema, context)
|
|
186
|
+
}
|
|
187
|
+
if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
|
|
188
|
+
updateOrder(schema, data[name])
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return processSchemaData(schema, data, {
|
|
193
|
+
dataPath,
|
|
194
|
+
before,
|
|
195
|
+
shouldProcess,
|
|
196
|
+
options
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns the value of the component described by `schema` and `name` in
|
|
202
|
+
* `data`: the result of `schema.compute()` if it returns a value, else the
|
|
203
|
+
* value in `data`, or its default if it's missing. Never writes into `data`:
|
|
204
|
+
* Computed values, including the defaults of components with `compute()`, are
|
|
205
|
+
* written by `FormModel`, and other defaults when the data is set up, see
|
|
206
|
+
* `initializeData()`. If provided, `getOptions()` returns the
|
|
207
|
+
* options for `context.options`, called only when they're read.
|
|
208
|
+
*/
|
|
209
|
+
export function computeValue(schema, data, name, dataPath, {
|
|
210
|
+
component = null,
|
|
211
|
+
rootData = component?.rootData,
|
|
212
|
+
getOptions = null
|
|
213
|
+
} = {}) {
|
|
214
|
+
const context = () =>
|
|
215
|
+
// Pass a function, so that the `options` getter isn't evaluated when the
|
|
216
|
+
// context is created.
|
|
217
|
+
new DitoContext(component, () => {
|
|
218
|
+
const properties = {
|
|
219
|
+
schema,
|
|
220
|
+
// Override value to prevent endless recursion through calling the
|
|
221
|
+
// getter for `this.value` in `DitoContext`. Read it only when it's
|
|
222
|
+
// used, so that `compute()` only depends on its own value if it reads
|
|
223
|
+
// it, and writing the computed value doesn't call it again.
|
|
224
|
+
get value() {
|
|
225
|
+
return data[name]
|
|
226
|
+
},
|
|
227
|
+
name,
|
|
228
|
+
data,
|
|
229
|
+
dataPath,
|
|
230
|
+
rootData
|
|
231
|
+
}
|
|
232
|
+
if (getOptions) {
|
|
233
|
+
Object.defineProperty(properties, 'options', {
|
|
234
|
+
get: getOptions,
|
|
235
|
+
enumerable: true
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
return properties
|
|
239
|
+
})
|
|
240
|
+
const { compute } = schema
|
|
241
|
+
// Like the form model, only compute values of components that are shown
|
|
242
|
+
// through their `if`, as `compute()` may rely on the same conditions, e.g.
|
|
243
|
+
// `if: ({ item }) => item.preview` with `compute: ({ item }) =>
|
|
244
|
+
// item.preview.name`.
|
|
245
|
+
if (
|
|
246
|
+
compute &&
|
|
247
|
+
getSchemaValue('if', {
|
|
248
|
+
type: Boolean,
|
|
249
|
+
schema,
|
|
250
|
+
context: getContext(context),
|
|
251
|
+
default: true
|
|
252
|
+
})
|
|
253
|
+
) {
|
|
254
|
+
const value = compute(getContext(context))
|
|
255
|
+
if (value !== undefined) {
|
|
256
|
+
return value
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return getValueOrDefault(schema, data, name, context)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Returns the value in `data`, or its default if it's missing. `context` can be
|
|
263
|
+
// a function that creates the context, called only when it's needed.
|
|
264
|
+
function getValueOrDefault(schema, data, name, context) {
|
|
265
|
+
const shouldUseDefault = (
|
|
266
|
+
isMissingValue(schema, data, name, context) &&
|
|
267
|
+
!shouldIgnoreMissingValue(schema, context)
|
|
268
|
+
)
|
|
269
|
+
return shouldUseDefault ? getDefaultValue(schema, context) : data[name]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cloneItem(sourceSchema, item, options) {
|
|
273
|
+
if (options.schemaOnly) {
|
|
274
|
+
const copy = {}
|
|
275
|
+
const { idKey = 'id', orderKey } = sourceSchema
|
|
276
|
+
const id = item[idKey]
|
|
277
|
+
if (id !== undefined) {
|
|
278
|
+
copy[idKey] = id
|
|
279
|
+
}
|
|
280
|
+
// Copy over type in case there are multiple forms to choose from.
|
|
281
|
+
if (hasMultipleFormSchemas(sourceSchema)) {
|
|
282
|
+
copy.type = item.type
|
|
283
|
+
}
|
|
284
|
+
if (orderKey) {
|
|
285
|
+
copy[orderKey] = item[orderKey]
|
|
286
|
+
}
|
|
287
|
+
return copy
|
|
288
|
+
} else {
|
|
289
|
+
return { ...item }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function processData(schema, sourceSchema, data, dataPath, {
|
|
294
|
+
component,
|
|
295
|
+
rootData,
|
|
296
|
+
schemaOnly, // whether to only include data covered by the schema, or all data
|
|
297
|
+
target,
|
|
298
|
+
// Whether to call the schema callbacks `compute()` and `process()`. Data that
|
|
299
|
+
// is compared while it's edited, e.g. by `FormModel.isDirty`, isn't validated
|
|
300
|
+
// yet, which the callbacks may rely on, and `FormModel` writes the computed
|
|
301
|
+
// values into the data. Without them, the values in the data are used, or
|
|
302
|
+
// their defaults if they're missing, the types still process them through
|
|
303
|
+
// `processValue()`, and the excluded values of components with `process()`
|
|
304
|
+
// are kept, as `process()` may store them elsewhere through `processedItem`.
|
|
305
|
+
shouldCallComputeAndProcess = true
|
|
306
|
+
} = {}) {
|
|
307
|
+
const options = { component, rootData, schemaOnly, target }
|
|
308
|
+
const processedData = cloneItem(sourceSchema, data, options)
|
|
309
|
+
const graph = new SchemaGraph()
|
|
310
|
+
|
|
311
|
+
const before = ({ schema, data, name, dataPath, processedData }) => {
|
|
312
|
+
let value = shouldCallComputeAndProcess
|
|
313
|
+
? computeValue(schema, data, name, dataPath, options)
|
|
314
|
+
: getValueOrDefault(
|
|
315
|
+
schema,
|
|
316
|
+
data,
|
|
317
|
+
name,
|
|
318
|
+
() =>
|
|
319
|
+
new DitoContext(component, {
|
|
320
|
+
schema,
|
|
321
|
+
name,
|
|
322
|
+
data,
|
|
323
|
+
dataPath,
|
|
324
|
+
rootData: options.rootData
|
|
325
|
+
})
|
|
326
|
+
)
|
|
327
|
+
// The schema expects the `wrapPrimitives` transformations to be present on
|
|
328
|
+
// the data that it is applied on, so warp before and unwrap after.
|
|
329
|
+
if (isArray(value)) {
|
|
330
|
+
const { wrapPrimitives } = schema
|
|
331
|
+
if (wrapPrimitives) {
|
|
332
|
+
value = value.map(entry => ({
|
|
333
|
+
[wrapPrimitives]: entry
|
|
334
|
+
}))
|
|
335
|
+
} else {
|
|
336
|
+
// Always shallow-clone array values:
|
|
337
|
+
value = [...value]
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
processedData[name] = value
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const after = ({ schema, data, name, dataPath, processedData }) => {
|
|
344
|
+
const { wrapPrimitives, process } = schema
|
|
345
|
+
let value = processedData[name]
|
|
346
|
+
|
|
347
|
+
// NOTE: We don't cache this context, since `value` is changing.
|
|
348
|
+
const context = () =>
|
|
349
|
+
new DitoContext(component, {
|
|
350
|
+
schema,
|
|
351
|
+
value,
|
|
352
|
+
name,
|
|
353
|
+
data,
|
|
354
|
+
dataPath,
|
|
355
|
+
rootData: options.rootData,
|
|
356
|
+
// Pass the already processed data to `process()`, so it can be modified
|
|
357
|
+
// through `processedItem` from there.
|
|
358
|
+
processedData
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// First unwrap the wrapped primitives again, to bring the data back into
|
|
362
|
+
// its native form. See `before()` for more details.
|
|
363
|
+
if (wrapPrimitives && isArray(value)) {
|
|
364
|
+
value = value.map(object => object[wrapPrimitives])
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Each component type can provide its own static `processValue()` method
|
|
368
|
+
// to convert the data for storage.
|
|
369
|
+
const processValue = getTypeOptions(schema)?.processValue
|
|
370
|
+
if (processValue) {
|
|
371
|
+
value = processValue(getContext(context), graph)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle the user's `process()` callback next, if one is provided, so that
|
|
375
|
+
// it can modify data in `processedData` even if it provides `exclude: true`
|
|
376
|
+
if (process && shouldCallComputeAndProcess) {
|
|
377
|
+
value = process(getContext(context))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Without calling `process()`, keep the excluded values that it may store
|
|
381
|
+
// elsewhere, see `shouldCallComputeAndProcess`:
|
|
382
|
+
const shouldKeepExcludedValue = !!process && !shouldCallComputeAndProcess
|
|
383
|
+
if (!shouldKeepExcludedValue && shouldExcludeValue(schema, context)) {
|
|
384
|
+
delete processedData[name]
|
|
385
|
+
} else {
|
|
386
|
+
processedData[name] = value
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
processSchemaData(schema, data, {
|
|
391
|
+
dataPath,
|
|
392
|
+
processedData,
|
|
393
|
+
before,
|
|
394
|
+
after,
|
|
395
|
+
options
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
return graph.process(sourceSchema, processedData, options)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Walks `data` along the components of `schema`, including nested forms of
|
|
403
|
+
* list and object items, calling `before()` and `after()` for each nested
|
|
404
|
+
* component, e.g. to process the data into `processedData`. `shouldProcess()`
|
|
405
|
+
* is called for all components, tabs and panels, and the ones for which it
|
|
406
|
+
* returns `false` are skipped along with their content. The entries passed to
|
|
407
|
+
* them contain the `componentPath` of the component, tab or panel, continuing
|
|
408
|
+
* `componentPath` like `DitoMixin.componentPath` does.
|
|
409
|
+
*/
|
|
410
|
+
export function processSchemaData(schema, data, {
|
|
411
|
+
dataPath = null,
|
|
412
|
+
componentPath = '',
|
|
413
|
+
processedData = null,
|
|
414
|
+
before = null,
|
|
415
|
+
after = null,
|
|
416
|
+
shouldProcess = () => true,
|
|
417
|
+
options
|
|
418
|
+
}) {
|
|
419
|
+
const walkOptions = { before, after, shouldProcess, options }
|
|
420
|
+
const getDataPath = (dataPath, token) =>
|
|
421
|
+
dataPath != null
|
|
422
|
+
? appendDataPath(dataPath, token)
|
|
423
|
+
: null
|
|
424
|
+
|
|
425
|
+
const processComponents = (components, parentComponentPath) => {
|
|
426
|
+
if (components) {
|
|
427
|
+
for (const [name, componentSchema] of Object.entries(components)) {
|
|
428
|
+
const isNestedComponent = isNested(componentSchema)
|
|
429
|
+
const componentDataPath = isNestedComponent
|
|
430
|
+
? getDataPath(dataPath, name)
|
|
431
|
+
: dataPath
|
|
432
|
+
const entry = {
|
|
433
|
+
schema: componentSchema,
|
|
434
|
+
data,
|
|
435
|
+
name,
|
|
436
|
+
dataPath: componentDataPath,
|
|
437
|
+
componentPath: appendDataPath(parentComponentPath, name),
|
|
438
|
+
processedData
|
|
439
|
+
}
|
|
440
|
+
if (!shouldProcess(entry)) {
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
if (isPanelWithOwnData(componentSchema)) {
|
|
444
|
+
// The components of panels with their own data don't edit `data`.
|
|
445
|
+
continue
|
|
446
|
+
}
|
|
447
|
+
if (!isNestedComponent) {
|
|
448
|
+
// Recursively process data on unnested components.
|
|
449
|
+
processSchemaData(componentSchema, data, {
|
|
450
|
+
...walkOptions,
|
|
451
|
+
dataPath,
|
|
452
|
+
componentPath: entry.componentPath,
|
|
453
|
+
processedData
|
|
454
|
+
})
|
|
455
|
+
} else {
|
|
456
|
+
const processItem = (item, index = null) => {
|
|
457
|
+
if (!isObject(item)) {
|
|
458
|
+
// Items that aren't objects can't hold component values, e.g.
|
|
459
|
+
// the unwrapped values of lists with `wrapPrimitives` when
|
|
460
|
+
// walking data that isn't processed.
|
|
461
|
+
return item
|
|
462
|
+
}
|
|
463
|
+
const itemDataPath =
|
|
464
|
+
index !== null
|
|
465
|
+
? getDataPath(componentDataPath, index)
|
|
466
|
+
: componentDataPath
|
|
467
|
+
const itemComponentPath =
|
|
468
|
+
index !== null
|
|
469
|
+
? appendDataPath(entry.componentPath, index)
|
|
470
|
+
: entry.componentPath
|
|
471
|
+
const context = new DitoContext(options.component, {
|
|
472
|
+
schema: componentSchema,
|
|
473
|
+
data,
|
|
474
|
+
dataPath: componentDataPath,
|
|
475
|
+
name,
|
|
476
|
+
rootData: options.rootData
|
|
477
|
+
})
|
|
478
|
+
const getForms = (
|
|
479
|
+
getTypeOptions(componentSchema)?.getFormSchemasForProcessing ||
|
|
480
|
+
getFormSchemas
|
|
481
|
+
)
|
|
482
|
+
const forms = getForms(componentSchema, context)
|
|
483
|
+
const form = getItemFormSchemaFromForms(forms, item)
|
|
484
|
+
if (form) {
|
|
485
|
+
const processedItem = processedData
|
|
486
|
+
? cloneItem(componentSchema, item, options)
|
|
487
|
+
: null
|
|
488
|
+
return processSchemaData(form, item, {
|
|
489
|
+
...walkOptions,
|
|
490
|
+
dataPath: itemDataPath,
|
|
491
|
+
componentPath: itemComponentPath,
|
|
492
|
+
processedData: processedItem
|
|
493
|
+
})
|
|
494
|
+
} else {
|
|
495
|
+
// Items without a matching form, e.g. of an unknown type, are
|
|
496
|
+
// passed through as shallow clones.
|
|
497
|
+
return { ...item }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
before?.(entry)
|
|
502
|
+
|
|
503
|
+
let value = processedData ? processedData[name] : data[name]
|
|
504
|
+
if (value != null && hasFormSchema(componentSchema)) {
|
|
505
|
+
// Recursively process data on nested form items.
|
|
506
|
+
if (isArray(value)) {
|
|
507
|
+
// Optimization: No need to collect values if we're not cloning!
|
|
508
|
+
value = processedData
|
|
509
|
+
? value.map(processItem)
|
|
510
|
+
: value.forEach(processItem)
|
|
511
|
+
} else {
|
|
512
|
+
value = processItem(value)
|
|
513
|
+
}
|
|
514
|
+
if (processedData) {
|
|
515
|
+
processedData[name] = value
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
after?.(entry)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const processTabOrPanelSchemas = schemas => {
|
|
526
|
+
// Tabs and panels add their names to component paths, not to data paths.
|
|
527
|
+
for (const [name, tabOrPanelSchema] of Object.entries(schemas || {})) {
|
|
528
|
+
const entry = {
|
|
529
|
+
schema: tabOrPanelSchema,
|
|
530
|
+
data,
|
|
531
|
+
name: null,
|
|
532
|
+
dataPath,
|
|
533
|
+
componentPath: appendDataPath(componentPath, name),
|
|
534
|
+
processedData
|
|
535
|
+
}
|
|
536
|
+
// The components of panels with their own data don't edit `data`.
|
|
537
|
+
if (shouldProcess(entry) && !isPanelWithOwnData(tabOrPanelSchema)) {
|
|
538
|
+
processComponents(tabOrPanelSchema.components, entry.componentPath)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
processComponents(schema.components, componentPath)
|
|
544
|
+
processTabOrPanelSchemas(schema.tabs)
|
|
545
|
+
processTabOrPanelSchemas(schema.panels)
|
|
546
|
+
|
|
547
|
+
return processedData || data
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Returns the component path of the value at `dataPath` in `data`, by walking
|
|
552
|
+
* `data` along the components of `schema` to the deepest component that
|
|
553
|
+
* contains the value, e.g. `main/chapters/1` for `chapters/1`, with the list
|
|
554
|
+
* `chapters` in the tab `main`. The part of `dataPath` inside that component,
|
|
555
|
+
* e.g. item indices, is appended to its component path. Of several components
|
|
556
|
+
* that display the same data, the first in schema order is used.
|
|
557
|
+
*/
|
|
558
|
+
export function getComponentPathByDataPath(schema, data, dataPath, {
|
|
559
|
+
componentPath = '',
|
|
560
|
+
component = null,
|
|
561
|
+
rootData = data
|
|
562
|
+
} = {}) {
|
|
563
|
+
let deepestEntry = { dataPath: '', componentPath }
|
|
564
|
+
if (data) {
|
|
565
|
+
processSchemaData(schema, data, {
|
|
566
|
+
dataPath: '',
|
|
567
|
+
componentPath,
|
|
568
|
+
// Only walk the components that contain the value at `dataPath`:
|
|
569
|
+
shouldProcess: entry => (
|
|
570
|
+
getRelativeDataPath(dataPath, entry.dataPath) !== null
|
|
571
|
+
),
|
|
572
|
+
before: entry => {
|
|
573
|
+
if (entry.dataPath.length > deepestEntry.dataPath.length) {
|
|
574
|
+
deepestEntry = entry
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
options: { component, rootData }
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
const relativeDataPath = getRelativeDataPath(dataPath, deepestEntry.dataPath)
|
|
581
|
+
return relativeDataPath
|
|
582
|
+
? appendDataPath(deepestEntry.componentPath, relativeDataPath)
|
|
583
|
+
: deepestEntry.componentPath
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function getItemId(sourceSchema, item) {
|
|
587
|
+
const id = item[sourceSchema.idKey || 'id']
|
|
588
|
+
return id != null ? String(id) : undefined
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Returns whether the items of the list described by `schema` are numbered by
|
|
592
|
+
// its `orderKey`, see `updateOrder()`. Lists of primitives have no items that
|
|
593
|
+
// could hold an order key.
|
|
594
|
+
export function hasItemsNumberedByOrderKey(schema) {
|
|
595
|
+
return !!schema.orderKey && !schema.wrapPrimitives
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function updateOrder(sourceSchema, list, paginationRange) {
|
|
599
|
+
const { orderKey } = sourceSchema
|
|
600
|
+
if (orderKey) {
|
|
601
|
+
// Reorder the changed entries by their order key, taking pagination
|
|
602
|
+
// offsets into account:
|
|
603
|
+
const offset = paginationRange?.[0] || 0
|
|
604
|
+
for (let i = 0; i < list.length; i++) {
|
|
605
|
+
list[i][orderKey] = i + offset
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return list
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function getItemUid(sourceSchema, item) {
|
|
612
|
+
// Try to use the item id as the uid, falling back on auto-generated ids, but
|
|
613
|
+
// either way, pass through `getUid()` so that the ids are associated with the
|
|
614
|
+
// item through a weak map, as the ids can be filtered out in `processData()`
|
|
615
|
+
// while the components that use the uids as key are still visible.
|
|
616
|
+
return getUid(item, item => getItemId(sourceSchema, item))
|
|
617
|
+
}
|