@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.
Files changed (36) hide show
  1. package/dist/dito-admin.es.js +2043 -1956
  2. package/dist/dito-admin.umd.js +5 -6
  3. package/package.json +8 -6
  4. package/src/DitoAdmin.js +1 -65
  5. package/src/DitoTypeComponent.js +17 -0
  6. package/src/components/DitoButtons.vue +14 -1
  7. package/src/components/DitoDialog.vue +1 -1
  8. package/src/components/DitoForm.vue +3 -8
  9. package/src/components/DitoPanel.vue +3 -2
  10. package/src/components/DitoRoot.vue +1 -8
  11. package/src/components/DitoRouterView.vue +27 -0
  12. package/src/components/DitoView.vue +2 -7
  13. package/src/components/index.js +1 -0
  14. package/src/mixins/DitoMixin.js +28 -17
  15. package/src/mixins/OptionsMixin.js +14 -4
  16. package/src/mixins/ResourceMixin.js +4 -2
  17. package/src/mixins/RouteMixin.js +1 -1
  18. package/src/mixins/SourceMixin.js +13 -1
  19. package/src/types/DitoTypeMultiselect.vue +23 -12
  20. package/src/types/DitoTypePanel.vue +2 -1
  21. package/src/types/DitoTypeTreeList.vue +5 -11
  22. package/src/utils/DataSchemaResolver.js +13 -0
  23. package/src/utils/FormModel.js +91 -32
  24. package/src/utils/request.js +70 -0
  25. package/src/utils/schema/data.js +102 -37
  26. package/src/utils/schema/lookup.js +9 -25
  27. package/src/utils/schema/validation.js +2 -4
  28. package/types/index.d.ts +25 -0
  29. package/src/utils/DataSchemaResolver.test.js +0 -132
  30. package/src/utils/FormModel.test.js +0 -808
  31. package/src/utils/data.test.js +0 -28
  32. package/src/utils/schema/data.test.js +0 -471
  33. package/src/utils/schema/lookup.test.js +0 -61
  34. package/src/utils/schema/structure.test.js +0 -57
  35. package/src/utils/schema/validation.test.js +0 -395
  36. package/src/utils/units.test.js +0 -71
@@ -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, isSourceWithResource } from './schema/structure.js'
12
+ import { isNested } from './schema/structure.js'
13
13
  import { isEmptySchema } from './schema/lookup.js'
14
14
  import {
15
- shouldRenderSchema,
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, except
27
- // the values of components with `compute()`, which fall back to their
28
- // defaults when `compute()` doesn't return a value, unlike in new data, see
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.computedValueEntries = computed(() => this.getComputedValueEntries())
100
+ this.dataEntries = computed(() => this.getDataEntries())
98
101
  watch(
99
- () => this.computedValueEntries.value,
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
- shouldProcess: entry => !isSourceWithResource(entry.schema),
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
- // Returns the entries of `processSchemaData()` for all components with
297
- // computed values whose `if` doesn't evaluate to `false`, by component path.
298
- // Called by a computed property, so that it runs again when the data
299
- // structure changes, e.g. when list items are added or removed, or when `if`
300
- // conditions change.
301
- getComputedValueEntries() {
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 entries = new Map()
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
- !isSourceWithResource(entry.schema) &&
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
- entries.set(entry.componentPath, entry)
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 entries
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 `getComputedValueEntries()`.
454
+ // same schema and data, see `getDataEntries()`.
399
455
  isEntryCurrent({ componentPath, schema, data }) {
400
- const currentEntry = this.computedValueEntries.value.get(componentPath)
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 parentDataPath = dataPath.slice(0, -1)
501
- const key = dataPath.at(-1)
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
- isArray(before) &&
531
- isArray(after) &&
532
- isArray(target) &&
533
- before.length === after.length &&
534
- after.length === target.length
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
+ }
@@ -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
- data[name] === null &&
132
- !!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context))
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
- if (
181
- (shouldSetDefaultsOfComponentsWithCompute || !schema.compute) &&
182
- isMissingValue(schema, data, name, context) &&
183
- !shouldIgnoreMissingValue(schema, context)
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()`. If provided, `getOptions()` returns the
207
- * options for `context.options`, called only when they're read.
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 = { before, after, shouldProcess, options }
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 componentDataPath = isNestedComponent
430
- ? getDataPath(dataPath, name)
431
- : dataPath
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
- 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
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
- 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
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(componentPath, name),
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
- 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
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 component path,
228
- // `dataPathComponentPath`, while the panels in `schema.panels` continue
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,