@ditojs/admin 3.0.0 → 3.1.1
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.es.js +2043 -1956
- package/dist/dito-admin.umd.js +5 -6
- package/package.json +8 -6
- package/src/DitoAdmin.js +1 -65
- package/src/DitoTypeComponent.js +17 -0
- package/src/components/DitoButtons.vue +14 -1
- package/src/components/DitoDialog.vue +1 -1
- package/src/components/DitoForm.vue +3 -8
- package/src/components/DitoPanel.vue +3 -2
- package/src/components/DitoRoot.vue +1 -8
- package/src/components/DitoRouterView.vue +27 -0
- package/src/components/DitoView.vue +2 -7
- package/src/components/index.js +1 -0
- package/src/mixins/DitoMixin.js +28 -17
- package/src/mixins/OptionsMixin.js +14 -4
- package/src/mixins/ResourceMixin.js +4 -2
- package/src/mixins/RouteMixin.js +1 -1
- package/src/mixins/SourceMixin.js +13 -1
- package/src/types/DitoTypeMultiselect.vue +23 -12
- package/src/types/DitoTypePanel.vue +2 -1
- package/src/types/DitoTypeTreeList.vue +5 -11
- package/src/utils/DataSchemaResolver.js +13 -0
- package/src/utils/FormModel.js +91 -32
- package/src/utils/request.js +70 -0
- package/src/utils/schema/data.js +102 -37
- package/src/utils/schema/lookup.js +9 -25
- package/src/utils/schema/validation.js +2 -4
- package/types/index.d.ts +25 -0
- package/src/utils/DataSchemaResolver.test.js +0 -132
- package/src/utils/FormModel.test.js +0 -808
- package/src/utils/data.test.js +0 -28
- package/src/utils/schema/data.test.js +0 -471
- package/src/utils/schema/lookup.test.js +0 -61
- package/src/utils/schema/structure.test.js +0 -57
- package/src/utils/schema/validation.test.js +0 -395
- package/src/utils/units.test.js +0 -71
package/src/utils/FormModel.js
CHANGED
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
} from '@ditojs/utils'
|
|
10
10
|
import DitoContext from '../DitoContext.js'
|
|
11
11
|
import { DataSchemaResolver } from './DataSchemaResolver.js'
|
|
12
|
-
import { isNested
|
|
12
|
+
import { isNested } from './schema/structure.js'
|
|
13
13
|
import { isEmptySchema } from './schema/lookup.js'
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
getSchemaValue,
|
|
16
|
+
shouldSetDefaultValue,
|
|
17
|
+
getDefaultValue,
|
|
16
18
|
initializeData,
|
|
17
19
|
computeValue,
|
|
18
20
|
hasValueFromDataSchema,
|
|
@@ -23,9 +25,10 @@ import {
|
|
|
23
25
|
// FormModel holds the state of the data edited by a form, view or dialog that
|
|
24
26
|
// is derived from schema and data, independently of what is rendered:
|
|
25
27
|
//
|
|
26
|
-
// - Missing values are set to their defaults when the data is set up,
|
|
27
|
-
//
|
|
28
|
-
//
|
|
28
|
+
// - Missing values are set to their defaults when the data is set up, and when
|
|
29
|
+
// they go missing later, e.g. in items that code adds, except the values of
|
|
30
|
+
// components with `compute()`, which fall back to their defaults when
|
|
31
|
+
// `compute()` doesn't return a value, unlike in new data, see
|
|
29
32
|
// `initializeData()`.
|
|
30
33
|
// - The items of lists with `orderKey` are numbered when the data is set up,
|
|
31
34
|
// which doesn't make the data dirty.
|
|
@@ -94,12 +97,19 @@ export class FormModel {
|
|
|
94
97
|
// The entries are read through a computed property, so that the watchers
|
|
95
98
|
// of the entries can check synchronously whether their entry is still
|
|
96
99
|
// current, see `createComputedValueScope()`.
|
|
97
|
-
this.
|
|
100
|
+
this.dataEntries = computed(() => this.getDataEntries())
|
|
98
101
|
watch(
|
|
99
|
-
() => this.
|
|
102
|
+
() => this.dataEntries.value.computedValueEntries,
|
|
100
103
|
entries => this.updateComputedValueRecords(entries),
|
|
101
104
|
{ immediate: true }
|
|
102
105
|
)
|
|
106
|
+
// Values that go missing after the data was set up, e.g. in items that
|
|
107
|
+
// code adds, get their defaults too:
|
|
108
|
+
watch(
|
|
109
|
+
() => this.dataEntries.value.entriesWithMissingValues,
|
|
110
|
+
entries => this.setDefaultValues(entries),
|
|
111
|
+
{ immediate: true }
|
|
112
|
+
)
|
|
103
113
|
if (getSourceSchema) {
|
|
104
114
|
// Data that is set up, e.g. loaded, saved or applied, isn't dirty:
|
|
105
115
|
watch(
|
|
@@ -285,7 +295,7 @@ export class FormModel {
|
|
|
285
295
|
initializeData(schema, data, this.component, {
|
|
286
296
|
dataPath: this.dataPath,
|
|
287
297
|
rootData: this.rootData,
|
|
288
|
-
|
|
298
|
+
shouldSkipSourcesWithResource: true,
|
|
289
299
|
// The model writes the values of components with `compute()`,
|
|
290
300
|
// including their defaults:
|
|
291
301
|
shouldSetDefaultsOfComponentsWithCompute: false
|
|
@@ -293,32 +303,78 @@ export class FormModel {
|
|
|
293
303
|
}
|
|
294
304
|
}
|
|
295
305
|
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
306
|
+
// Walks the data and returns the entries of `processSchemaData()` of the
|
|
307
|
+
// components whose `if` doesn't evaluate to `false`: the ones with computed
|
|
308
|
+
// values, by component path, and the ones whose values are missing. Called
|
|
309
|
+
// by a computed property, so that it runs again when the data structure
|
|
310
|
+
// changes, e.g. when list items are added or removed, values go missing, or
|
|
311
|
+
// `if` conditions change.
|
|
312
|
+
getDataEntries() {
|
|
302
313
|
const schema = this.getSchema()
|
|
303
314
|
const data = this.getData()
|
|
304
|
-
const
|
|
315
|
+
const computedValueEntries = new Map()
|
|
316
|
+
const entriesWithMissingValues = []
|
|
305
317
|
if (data && !isEmptySchema(schema)) {
|
|
306
318
|
processSchemaData(schema, data, {
|
|
307
319
|
dataPath: this.dataPath,
|
|
308
320
|
componentPath: this.componentPath,
|
|
309
|
-
shouldProcess: entry => (
|
|
310
|
-
|
|
311
|
-
shouldRenderSchema(entry.schema, this.createEntryContext(entry))
|
|
312
|
-
),
|
|
321
|
+
shouldProcess: entry => this.isEntryShown(entry),
|
|
322
|
+
shouldSkipSourcesWithResource: true,
|
|
313
323
|
before: entry => {
|
|
314
324
|
if (hasComputedValueSource(entry.schema)) {
|
|
315
|
-
|
|
325
|
+
computedValueEntries.set(entry.componentPath, entry)
|
|
326
|
+
}
|
|
327
|
+
if (this.shouldSetDefaultValue(entry)) {
|
|
328
|
+
entriesWithMissingValues.push(entry)
|
|
316
329
|
}
|
|
317
330
|
},
|
|
318
331
|
options: { component: this.component, rootData: this.rootData }
|
|
319
332
|
})
|
|
320
333
|
}
|
|
321
|
-
return
|
|
334
|
+
return { computedValueEntries, entriesWithMissingValues }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Returns whether the `if` of the entry's component doesn't evaluate to
|
|
338
|
+
// `false`. Unlike `shouldRenderSchema()`, it doesn't evaluate the components
|
|
339
|
+
// of sections and tabs, as the walk of the data visits them anyway, see
|
|
340
|
+
// `getDataEntries()`, which leads to the same entries.
|
|
341
|
+
isEntryShown(entry) {
|
|
342
|
+
return (
|
|
343
|
+
entry.schema.if === undefined ||
|
|
344
|
+
getSchemaValue('if', {
|
|
345
|
+
type: Boolean,
|
|
346
|
+
schema: entry.schema,
|
|
347
|
+
context: this.createEntryContext(entry),
|
|
348
|
+
default: true
|
|
349
|
+
})
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Returns whether the value of the entry is missing and needs its default.
|
|
354
|
+
// The model writes the values of components with `compute()`, including
|
|
355
|
+
// their defaults, see `computeValue()`.
|
|
356
|
+
shouldSetDefaultValue(entry) {
|
|
357
|
+
const { schema, data, name } = entry
|
|
358
|
+
return shouldSetDefaultValue(
|
|
359
|
+
schema,
|
|
360
|
+
data,
|
|
361
|
+
name,
|
|
362
|
+
() => this.createEntryContext(entry),
|
|
363
|
+
{ shouldSetDefaultsOfComponentsWithCompute: false }
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Sets the defaults of the values of the entries, unless they were set in
|
|
368
|
+
// the meantime.
|
|
369
|
+
setDefaultValues(entries) {
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
if (this.shouldSetDefaultValue(entry)) {
|
|
372
|
+
const { schema, data, name } = entry
|
|
373
|
+
data[name] = getDefaultValue(schema, () =>
|
|
374
|
+
this.createEntryContext(entry)
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
322
378
|
}
|
|
323
379
|
|
|
324
380
|
// Keeps the records of the components that are still present with the same
|
|
@@ -395,9 +451,10 @@ export class FormModel {
|
|
|
395
451
|
}
|
|
396
452
|
|
|
397
453
|
// Returns whether the entry is still one of the current entries, with the
|
|
398
|
-
// same schema and data, see `
|
|
454
|
+
// same schema and data, see `getDataEntries()`.
|
|
399
455
|
isEntryCurrent({ componentPath, schema, data }) {
|
|
400
|
-
const currentEntry =
|
|
456
|
+
const currentEntry =
|
|
457
|
+
this.dataEntries.value.computedValueEntries.get(componentPath)
|
|
401
458
|
return currentEntry?.schema === schema && currentEntry.data === data
|
|
402
459
|
}
|
|
403
460
|
|
|
@@ -466,7 +523,7 @@ export class FormModel {
|
|
|
466
523
|
const tokens = isNested(schema)
|
|
467
524
|
? parseDataPath(dataPath)
|
|
468
525
|
: [...parseDataPath(dataPath), name]
|
|
469
|
-
return tokens.slice(parseDataPath(this.dataPath).length)
|
|
526
|
+
return tokens.slice(parseDataPath(this.dataPath).length).join('/')
|
|
470
527
|
}
|
|
471
528
|
|
|
472
529
|
// Returns the context for `if`, data schemas and options, with the component
|
|
@@ -497,8 +554,9 @@ function takeOverValuesAtDataPaths(
|
|
|
497
554
|
}
|
|
498
555
|
const result = clone(processedData)
|
|
499
556
|
for (const dataPath of dataPaths) {
|
|
500
|
-
const
|
|
501
|
-
const
|
|
557
|
+
const tokens = parseDataPath(dataPath)
|
|
558
|
+
const parentDataPath = tokens.slice(0, -1)
|
|
559
|
+
const key = tokens.at(-1)
|
|
502
560
|
const parent = getValueAtDataPath(result, parentDataPath, () => null)
|
|
503
561
|
if (parent && typeof parent === 'object') {
|
|
504
562
|
const value = getValueAtDataPath(
|
|
@@ -526,12 +584,13 @@ function takeOverChangedValues(target, before, after) {
|
|
|
526
584
|
return target
|
|
527
585
|
}
|
|
528
586
|
const isSameShape = (
|
|
529
|
-
isPlainObject(before) && isPlainObject(after) && isPlainObject(target) ||
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
587
|
+
isPlainObject(before) && isPlainObject(after) && isPlainObject(target) || (
|
|
588
|
+
isArray(before) &&
|
|
589
|
+
isArray(after) &&
|
|
590
|
+
isArray(target) &&
|
|
591
|
+
before.length === after.length &&
|
|
592
|
+
after.length === target.length
|
|
593
|
+
)
|
|
535
594
|
)
|
|
536
595
|
if (!isSameShape) {
|
|
537
596
|
return clone(after)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isAbsoluteUrl } from '@ditojs/utils'
|
|
2
|
+
import { formatQuery } from './route.js'
|
|
3
|
+
|
|
4
|
+
export class RequestError extends Error {
|
|
5
|
+
constructor(response) {
|
|
6
|
+
super(
|
|
7
|
+
`Request failed with status code: ${response.status} (${
|
|
8
|
+
response.statusText
|
|
9
|
+
})`
|
|
10
|
+
)
|
|
11
|
+
this.response = response
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function request(api, {
|
|
16
|
+
url,
|
|
17
|
+
method = 'get',
|
|
18
|
+
query = null,
|
|
19
|
+
headers = null,
|
|
20
|
+
data = null,
|
|
21
|
+
signal = null,
|
|
22
|
+
timeout = api.timeout ?? null
|
|
23
|
+
}) {
|
|
24
|
+
const isApiUrl = api.isApiUrl(url)
|
|
25
|
+
// Abort requests that take longer than the timeout, if there is one, so that
|
|
26
|
+
// nothing waits for them forever, e.g. submitting for pending loads:
|
|
27
|
+
const signals = [signal, timeout && AbortSignal.timeout(timeout)]
|
|
28
|
+
signal = AbortSignal.any(signals.filter(Boolean))
|
|
29
|
+
|
|
30
|
+
const response = await fetch(api.getApiUrl({ url, query }), {
|
|
31
|
+
method: method.toUpperCase(),
|
|
32
|
+
...(data && { body: JSON.stringify(data) }),
|
|
33
|
+
headers: {
|
|
34
|
+
...(isApiUrl && api.headers),
|
|
35
|
+
...headers
|
|
36
|
+
},
|
|
37
|
+
credentials:
|
|
38
|
+
isApiUrl && api.cors?.credentials
|
|
39
|
+
? 'include'
|
|
40
|
+
: 'same-origin',
|
|
41
|
+
signal
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (response.headers.get('Content-Type')?.includes('application/json')) {
|
|
45
|
+
response.data = await response.json()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new RequestError(response)
|
|
50
|
+
}
|
|
51
|
+
return response
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isApiUrl(api, url) {
|
|
55
|
+
return !isAbsoluteUrl(url) || url.startsWith(api.url)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getApiUrl(api, { url, query }) {
|
|
59
|
+
if (!url.startsWith(api.url) && !isAbsoluteUrl(url)) {
|
|
60
|
+
url = combineUrls(api.url, url)
|
|
61
|
+
}
|
|
62
|
+
// Support optional query parameters, to be are added to the URL.
|
|
63
|
+
const search = formatQuery(query)
|
|
64
|
+
return search ? `${url}${url.includes('?') ? '&' : '?'}${search}` : url
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function combineUrls(baseUrl, relativeUrl) {
|
|
68
|
+
// Use same approach as axios `combineURLs()` to join baseUrl & relativeUrl:
|
|
69
|
+
return `${baseUrl.replace(/\/+$/, '')}/${relativeUrl.replace(/^\/+/, '')}`
|
|
70
|
+
}
|
package/src/utils/schema/data.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
someNestedSchemaComponent,
|
|
17
17
|
hasNestedSchemaComponents,
|
|
18
18
|
isPanelWithOwnData,
|
|
19
|
+
isSourceWithResource,
|
|
19
20
|
isNested
|
|
20
21
|
} from './structure.js'
|
|
21
22
|
import {
|
|
@@ -126,10 +127,13 @@ export function shouldIgnoreMissingValue(schema, context) {
|
|
|
126
127
|
// is missing in `data`: if there's no value, or if the value is `null` and
|
|
127
128
|
// the type treats that as missing, e.g. nested sections.
|
|
128
129
|
export function isMissingValue(schema, data, name, context) {
|
|
130
|
+
// Only read the value if needed, so that callers that are tracked, e.g. the
|
|
131
|
+
// walk of the form model's data, don't depend on all values.
|
|
129
132
|
return (
|
|
130
|
-
!(name in data) ||
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
!(name in data) || (
|
|
134
|
+
!!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context)) &&
|
|
135
|
+
data[name] === null
|
|
136
|
+
)
|
|
133
137
|
)
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -150,6 +154,22 @@ export function getMultipleValue(schema) {
|
|
|
150
154
|
return schema.multiple ?? !!getTypeOptions(schema)?.defaultMultiple
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Returns whether the value of the component described by `schema` and `name`
|
|
159
|
+
* is missing in `data` and needs its default, see `initializeData()`.
|
|
160
|
+
* `shouldSetDefaultsOfComponentsWithCompute` decides for components with
|
|
161
|
+
* `compute()`, which may derive their missing values instead.
|
|
162
|
+
*/
|
|
163
|
+
export function shouldSetDefaultValue(schema, data, name, context, {
|
|
164
|
+
shouldSetDefaultsOfComponentsWithCompute = true
|
|
165
|
+
} = {}) {
|
|
166
|
+
return (
|
|
167
|
+
(shouldSetDefaultsOfComponentsWithCompute || !schema.compute) &&
|
|
168
|
+
isMissingValue(schema, data, name, context) &&
|
|
169
|
+
!shouldIgnoreMissingValue(schema, context)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
153
173
|
/**
|
|
154
174
|
* Initializes `data` for the components of `schema`: Sets missing values to
|
|
155
175
|
* their defaults, so they can be correctly watched for changes, and numbers
|
|
@@ -160,6 +180,7 @@ export function initializeData(schema, data = {}, component, {
|
|
|
160
180
|
dataPath = null,
|
|
161
181
|
rootData = data,
|
|
162
182
|
shouldProcess,
|
|
183
|
+
shouldSkipSourcesWithResource = false,
|
|
163
184
|
// Whether to also set the defaults of components with `compute()`. New data
|
|
164
185
|
// starts with all defaults, which `compute()` can rely on. In data that was
|
|
165
186
|
// loaded, `compute()` may derive missing values instead, and they fall back
|
|
@@ -177,11 +198,14 @@ export function initializeData(schema, data = {}, component, {
|
|
|
177
198
|
dataPath,
|
|
178
199
|
rootData
|
|
179
200
|
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
const shouldSetDefault = shouldSetDefaultValue(
|
|
202
|
+
schema,
|
|
203
|
+
data,
|
|
204
|
+
name,
|
|
205
|
+
context,
|
|
206
|
+
{ shouldSetDefaultsOfComponentsWithCompute }
|
|
207
|
+
)
|
|
208
|
+
if (shouldSetDefault) {
|
|
185
209
|
data[name] = getDefaultValue(schema, context)
|
|
186
210
|
}
|
|
187
211
|
if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
|
|
@@ -193,6 +217,7 @@ export function initializeData(schema, data = {}, component, {
|
|
|
193
217
|
dataPath,
|
|
194
218
|
before,
|
|
195
219
|
shouldProcess,
|
|
220
|
+
shouldSkipSourcesWithResource,
|
|
196
221
|
options
|
|
197
222
|
})
|
|
198
223
|
}
|
|
@@ -203,8 +228,9 @@ export function initializeData(schema, data = {}, component, {
|
|
|
203
228
|
* value in `data`, or its default if it's missing. Never writes into `data`:
|
|
204
229
|
* Computed values, including the defaults of components with `compute()`, are
|
|
205
230
|
* written by `FormModel`, and other defaults when the data is set up, see
|
|
206
|
-
* `initializeData()
|
|
207
|
-
* options for `context.options`, called
|
|
231
|
+
* `initializeData()`, or when they go missing later, see `FormModel`. If
|
|
232
|
+
* provided, `getOptions()` returns the options for `context.options`, called
|
|
233
|
+
* only when they're read.
|
|
208
234
|
*/
|
|
209
235
|
export function computeValue(schema, data, name, dataPath, {
|
|
210
236
|
component = null,
|
|
@@ -406,17 +432,37 @@ export function processData(schema, sourceSchema, data, dataPath, {
|
|
|
406
432
|
* returns `false` are skipped along with their content. The entries passed to
|
|
407
433
|
* them contain the `componentPath` of the component, tab or panel, continuing
|
|
408
434
|
* `componentPath` like `DitoMixin.componentPath` does.
|
|
435
|
+
*
|
|
436
|
+
* With `shouldSkipSourcesWithResource`, sources with their own resource are
|
|
437
|
+
* skipped along with their items, which they load and save through their
|
|
438
|
+
* resource, but not their panels, which display the data that contains them,
|
|
439
|
+
* see `DitoContainer.panelEntries`.
|
|
440
|
+
*
|
|
441
|
+
* The primitive values of lists with `wrapPrimitives` are walked wrapped in
|
|
442
|
+
* objects under the `wrapPrimitives` key, like the admin edits them, see
|
|
443
|
+
* `SourceMixin`. `wrappedPrimitiveName` is that key while walking such an
|
|
444
|
+
* object, and its component keeps the data path of the item, where the admin
|
|
445
|
+
* maps errors of the value to. Values written into the objects don't reach
|
|
446
|
+
* the data, e.g. defaults, as the primitive values themselves are the data.
|
|
409
447
|
*/
|
|
410
448
|
export function processSchemaData(schema, data, {
|
|
411
449
|
dataPath = null,
|
|
412
450
|
componentPath = '',
|
|
413
451
|
processedData = null,
|
|
452
|
+
wrappedPrimitiveName = null,
|
|
414
453
|
before = null,
|
|
415
454
|
after = null,
|
|
416
455
|
shouldProcess = () => true,
|
|
456
|
+
shouldSkipSourcesWithResource = false,
|
|
417
457
|
options
|
|
418
458
|
}) {
|
|
419
|
-
const walkOptions = {
|
|
459
|
+
const walkOptions = {
|
|
460
|
+
before,
|
|
461
|
+
after,
|
|
462
|
+
shouldProcess,
|
|
463
|
+
shouldSkipSourcesWithResource,
|
|
464
|
+
options
|
|
465
|
+
}
|
|
420
466
|
const getDataPath = (dataPath, token) =>
|
|
421
467
|
dataPath != null
|
|
422
468
|
? appendDataPath(dataPath, token)
|
|
@@ -426,9 +472,11 @@ export function processSchemaData(schema, data, {
|
|
|
426
472
|
if (components) {
|
|
427
473
|
for (const [name, componentSchema] of Object.entries(components)) {
|
|
428
474
|
const isNestedComponent = isNested(componentSchema)
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
475
|
+
const isWrappedPrimitive = name === wrappedPrimitiveName
|
|
476
|
+
const componentDataPath =
|
|
477
|
+
isNestedComponent && !isWrappedPrimitive
|
|
478
|
+
? getDataPath(dataPath, name)
|
|
479
|
+
: dataPath
|
|
432
480
|
const entry = {
|
|
433
481
|
schema: componentSchema,
|
|
434
482
|
data,
|
|
@@ -440,6 +488,13 @@ export function processSchemaData(schema, data, {
|
|
|
440
488
|
if (!shouldProcess(entry)) {
|
|
441
489
|
continue
|
|
442
490
|
}
|
|
491
|
+
if (
|
|
492
|
+
shouldSkipSourcesWithResource &&
|
|
493
|
+
isSourceWithResource(componentSchema)
|
|
494
|
+
) {
|
|
495
|
+
processTabOrPanelSchemas(componentSchema.panels, entry.componentPath)
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
443
498
|
if (isPanelWithOwnData(componentSchema)) {
|
|
444
499
|
// The components of panels with their own data don't edit `data`.
|
|
445
500
|
continue
|
|
@@ -453,12 +508,12 @@ export function processSchemaData(schema, data, {
|
|
|
453
508
|
processedData
|
|
454
509
|
})
|
|
455
510
|
} else {
|
|
511
|
+
const { wrapPrimitives } = componentSchema
|
|
456
512
|
const processItem = (item, index = null) => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return item
|
|
513
|
+
// Data that isn't processed holds the primitive values themselves,
|
|
514
|
+
// processed data holds them wrapped already, see `processData()`.
|
|
515
|
+
if (wrapPrimitives && !isObject(item)) {
|
|
516
|
+
item = { [wrapPrimitives]: item }
|
|
462
517
|
}
|
|
463
518
|
const itemDataPath =
|
|
464
519
|
index !== null
|
|
@@ -489,7 +544,8 @@ export function processSchemaData(schema, data, {
|
|
|
489
544
|
...walkOptions,
|
|
490
545
|
dataPath: itemDataPath,
|
|
491
546
|
componentPath: itemComponentPath,
|
|
492
|
-
processedData: processedItem
|
|
547
|
+
processedData: processedItem,
|
|
548
|
+
wrappedPrimitiveName: wrapPrimitives ?? null
|
|
493
549
|
})
|
|
494
550
|
} else {
|
|
495
551
|
// Items without a matching form, e.g. of an unknown type, are
|
|
@@ -500,29 +556,38 @@ export function processSchemaData(schema, data, {
|
|
|
500
556
|
|
|
501
557
|
before?.(entry)
|
|
502
558
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
559
|
+
// Only read the values of sources with forms, so that callers that
|
|
560
|
+
// are tracked, e.g. the walk of the form model's data, don't depend
|
|
561
|
+
// on all values.
|
|
562
|
+
if (hasFormSchema(componentSchema)) {
|
|
563
|
+
let value = processedData ? processedData[name] : data[name]
|
|
564
|
+
if (value != null) {
|
|
565
|
+
// Recursively process data on nested form items.
|
|
566
|
+
if (isArray(value)) {
|
|
567
|
+
// Optimization: No need to collect values if not cloning!
|
|
568
|
+
value = processedData
|
|
569
|
+
? value.map(processItem)
|
|
570
|
+
: value.forEach(processItem)
|
|
571
|
+
} else {
|
|
572
|
+
value = processItem(value)
|
|
573
|
+
}
|
|
574
|
+
if (processedData) {
|
|
575
|
+
processedData[name] = value
|
|
576
|
+
}
|
|
516
577
|
}
|
|
517
578
|
}
|
|
518
579
|
|
|
519
580
|
after?.(entry)
|
|
581
|
+
// The panels of nested components display the data that contains
|
|
582
|
+
// the components, like the ones of unnested components, see
|
|
583
|
+
// `DitoContainer.panelEntries`.
|
|
584
|
+
processTabOrPanelSchemas(componentSchema.panels, entry.componentPath)
|
|
520
585
|
}
|
|
521
586
|
}
|
|
522
587
|
}
|
|
523
588
|
}
|
|
524
589
|
|
|
525
|
-
const processTabOrPanelSchemas = schemas => {
|
|
590
|
+
const processTabOrPanelSchemas = (schemas, parentComponentPath) => {
|
|
526
591
|
// Tabs and panels add their names to component paths, not to data paths.
|
|
527
592
|
for (const [name, tabOrPanelSchema] of Object.entries(schemas || {})) {
|
|
528
593
|
const entry = {
|
|
@@ -530,7 +595,7 @@ export function processSchemaData(schema, data, {
|
|
|
530
595
|
data,
|
|
531
596
|
name: null,
|
|
532
597
|
dataPath,
|
|
533
|
-
componentPath: appendDataPath(
|
|
598
|
+
componentPath: appendDataPath(parentComponentPath, name),
|
|
534
599
|
processedData
|
|
535
600
|
}
|
|
536
601
|
// The components of panels with their own data don't edit `data`.
|
|
@@ -541,8 +606,8 @@ export function processSchemaData(schema, data, {
|
|
|
541
606
|
}
|
|
542
607
|
|
|
543
608
|
processComponents(schema.components, componentPath)
|
|
544
|
-
processTabOrPanelSchemas(schema.tabs)
|
|
545
|
-
processTabOrPanelSchemas(schema.panels)
|
|
609
|
+
processTabOrPanelSchemas(schema.tabs, componentPath)
|
|
610
|
+
processTabOrPanelSchemas(schema.panels, componentPath)
|
|
546
611
|
|
|
547
612
|
return processedData || data
|
|
548
613
|
}
|
|
@@ -176,27 +176,10 @@ export function getButtonSchemas(buttons) {
|
|
|
176
176
|
)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
179
|
+
// Returns the entry of the panel described by `schema`, which `DitoPanels`
|
|
180
|
+
// displays at `dataPath` and `componentPath`.
|
|
181
|
+
function getPanelEntry(schema, dataPath, componentPath, tabComponent) {
|
|
182
|
+
return schema ? { schema, dataPath, componentPath, tabComponent } : null
|
|
200
183
|
}
|
|
201
184
|
|
|
202
185
|
export function getPanelEntries(
|
|
@@ -224,8 +207,9 @@ export function getPanelEntries(
|
|
|
224
207
|
|
|
225
208
|
// Returns the entries of the panel that the type of the component described by
|
|
226
209
|
// `schema` provides, and of the panels in `schema.panels`. The type's panel is
|
|
227
|
-
// addressed relative to `dataPath` and its
|
|
228
|
-
// `dataPathComponentPath`, while the panels in `schema.panels`
|
|
210
|
+
// addressed by its name, e.g. `$filters`, relative to `dataPath` and its
|
|
211
|
+
// component path, `dataPathComponentPath`, while the panels in `schema.panels`
|
|
212
|
+
// are addressed by their keys, see `getPanelEntries()`, continuing
|
|
229
213
|
// `schemaComponentPath`, the component path of `schema` itself, which also
|
|
230
214
|
// contains the names of unnested components.
|
|
231
215
|
export function getAllPanelEntries(
|
|
@@ -247,8 +231,8 @@ export function getAllPanelEntries(
|
|
|
247
231
|
? [
|
|
248
232
|
getPanelEntry(
|
|
249
233
|
panelSchema,
|
|
250
|
-
dataPath,
|
|
251
|
-
dataPathComponentPath,
|
|
234
|
+
appendDataPath(dataPath, panelSchema.name),
|
|
235
|
+
appendDataPath(dataPathComponentPath, panelSchema.name),
|
|
252
236
|
tabComponent
|
|
253
237
|
)
|
|
254
238
|
]
|
|
@@ -2,7 +2,6 @@ import DitoContext from '../../DitoContext.js'
|
|
|
2
2
|
import * as validators from '../../validators/index.js'
|
|
3
3
|
import { isFunction, isRegExp, asArray, normalizeDataPath } from '@ditojs/utils'
|
|
4
4
|
import { getTypeOptions } from './types.js'
|
|
5
|
-
import { isSourceWithResource } from './structure.js'
|
|
6
5
|
import {
|
|
7
6
|
getSchemaValue,
|
|
8
7
|
shouldRenderSchema,
|
|
@@ -86,10 +85,8 @@ export function getDataValidationErrors(schema, data, {
|
|
|
86
85
|
rootData
|
|
87
86
|
})
|
|
88
87
|
|
|
89
|
-
const shouldProcess = entry =>
|
|
90
|
-
!isSourceWithResource(entry.schema) &&
|
|
88
|
+
const shouldProcess = entry =>
|
|
91
89
|
shouldRenderSchema(entry.schema, createContext(entry))
|
|
92
|
-
)
|
|
93
90
|
|
|
94
91
|
const before = entry => {
|
|
95
92
|
const { schema, data, name, dataPath } = entry
|
|
@@ -112,6 +109,7 @@ export function getDataValidationErrors(schema, data, {
|
|
|
112
109
|
processSchemaData(schema, data, {
|
|
113
110
|
dataPath,
|
|
114
111
|
shouldProcess,
|
|
112
|
+
shouldSkipSourcesWithResource: true,
|
|
115
113
|
before,
|
|
116
114
|
options: { component, rootData }
|
|
117
115
|
})
|
package/types/index.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export type RequestMethod = <T>(options: {
|
|
|
52
52
|
headers?: Record<string, string> | null
|
|
53
53
|
/** Abort signal to cancel the request. */
|
|
54
54
|
signal?: AbortSignal | null
|
|
55
|
+
/**
|
|
56
|
+
* Milliseconds after which the request is aborted.
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue {@link ApiConfig.timeout}
|
|
59
|
+
*/
|
|
60
|
+
timeout?: number | null
|
|
55
61
|
}) => Promise<RequestMethodResponse<T>>
|
|
56
62
|
|
|
57
63
|
/**
|
|
@@ -170,6 +176,14 @@ export interface ApiConfig {
|
|
|
170
176
|
*/
|
|
171
177
|
durationFactor: number
|
|
172
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Milliseconds after which requests are aborted, so that
|
|
181
|
+
* nothing waits for them forever, e.g. submitting forms for
|
|
182
|
+
* pending loads of options. Requests can set their own.
|
|
183
|
+
*
|
|
184
|
+
* @defaultValue `null`, no timeout
|
|
185
|
+
*/
|
|
186
|
+
timeout?: number | null
|
|
173
187
|
/**
|
|
174
188
|
* CORS settings applied to API requests (where
|
|
175
189
|
* {@link ApiConfig.isApiUrl} returns `true`).
|
|
@@ -330,6 +344,11 @@ export interface BaseSchema<$Item>
|
|
|
330
344
|
* when the field isn't rendered, unless its `if`
|
|
331
345
|
* evaluates to `false`. If the callback returns
|
|
332
346
|
* `undefined`, the current value is preserved.
|
|
347
|
+
*
|
|
348
|
+
* As it doesn't depend on the field's component, the
|
|
349
|
+
* context's component properties, e.g. `component`
|
|
350
|
+
* and `schemaComponent`, refer to the form, view or
|
|
351
|
+
* dialog that holds the data, not to the field.
|
|
333
352
|
*/
|
|
334
353
|
compute?: ItemAccessor<$Item>
|
|
335
354
|
/**
|
|
@@ -797,6 +816,12 @@ export type SchemaOptions<$Item, $Option = any> =
|
|
|
797
816
|
* receives the `DitoContext` and returns an inner
|
|
798
817
|
* function that is called to fetch the actual data,
|
|
799
818
|
* enabling reactive dependency tracking.
|
|
819
|
+
*
|
|
820
|
+
* The options are loaded independently of the
|
|
821
|
+
* field's component, also for computes, so the
|
|
822
|
+
* context's component properties, e.g. `component`
|
|
823
|
+
* and `schemaComponent`, refer to the form, view or
|
|
824
|
+
* dialog that holds the data, not to the field.
|
|
800
825
|
*/
|
|
801
826
|
data?: OrItemAccessor<
|
|
802
827
|
$Item,
|