@ditojs/admin 3.1.0 → 3.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "type": "module",
5
5
  "description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/main/packages/admin",
@@ -41,8 +41,8 @@
41
41
  "not ie_mob > 0"
42
42
  ],
43
43
  "dependencies": {
44
- "@ditojs/ui": "^3.1.0",
45
- "@ditojs/utils": "^3.1.0"
44
+ "@ditojs/ui": "^3.1.2",
45
+ "@ditojs/utils": "^3.1.2"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@kyvg/vue3-notification": "^3.4.2",
@@ -87,7 +87,7 @@
87
87
  "vue-upload-component": "^3.2.2"
88
88
  },
89
89
  "devDependencies": {
90
- "@ditojs/build": "^3.1.0",
90
+ "@ditojs/build": "^3.1.2",
91
91
  "@kyvg/vue3-notification": "^3.4.2",
92
92
  "@tiptap/core": "^3.31.4",
93
93
  "@tiptap/extension-blockquote": "^3.31.4",
@@ -131,5 +131,5 @@
131
131
  "vue-router": "^5.3.1",
132
132
  "vue-upload-component": "^3.2.2"
133
133
  },
134
- "gitHead": "97314265c5cbac6a248d7350f43fea8ea92b995f"
134
+ "gitHead": "a87ccff01f65386d9363366fb19bb993d38e8fbc"
135
135
  }
@@ -54,8 +54,10 @@ export default {
54
54
  // `isTransient()` to only return `!this.providesData`.
55
55
  const form = this.formComponent
56
56
  return (
57
- !this.providesData &&
58
- !this.linksToView ||
57
+ (
58
+ !this.providesData &&
59
+ !this.linksToView
60
+ ) ||
59
61
  form && (
60
62
  form.isTransient ||
61
63
  form.isCreating
@@ -87,9 +87,10 @@ export default DitoTypeComponent.register(
87
87
  type: Boolean,
88
88
  default: false,
89
89
  schema
90
- }) ||
91
- schema.children &&
92
- hasEditableForms(schema.children)
90
+ }) || (
91
+ schema.children &&
92
+ hasEditableForms(schema.children)
93
+ )
93
94
  )
94
95
  )
95
96
  }
@@ -9,7 +9,7 @@ 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
15
  getSchemaValue,
@@ -295,7 +295,7 @@ export class FormModel {
295
295
  initializeData(schema, data, this.component, {
296
296
  dataPath: this.dataPath,
297
297
  rootData: this.rootData,
298
- shouldProcess: entry => !isSourceWithResource(entry.schema),
298
+ shouldSkipSourcesWithResource: true,
299
299
  // The model writes the values of components with `compute()`,
300
300
  // including their defaults:
301
301
  shouldSetDefaultsOfComponentsWithCompute: false
@@ -318,9 +318,8 @@ export class FormModel {
318
318
  processSchemaData(schema, data, {
319
319
  dataPath: this.dataPath,
320
320
  componentPath: this.componentPath,
321
- shouldProcess: entry => (
322
- !isSourceWithResource(entry.schema) && this.isEntryShown(entry)
323
- ),
321
+ shouldProcess: entry => this.isEntryShown(entry),
322
+ shouldSkipSourcesWithResource: true,
324
323
  before: entry => {
325
324
  if (hasComputedValueSource(entry.schema)) {
326
325
  computedValueEntries.set(entry.componentPath, entry)
@@ -585,12 +584,17 @@ function takeOverChangedValues(target, before, after) {
585
584
  return target
586
585
  }
587
586
  const isSameShape = (
588
- isPlainObject(before) && isPlainObject(after) && isPlainObject(target) ||
589
- isArray(before) &&
590
- isArray(after) &&
591
- isArray(target) &&
592
- before.length === after.length &&
593
- after.length === target.length
587
+ (
588
+ isPlainObject(before) &&
589
+ isPlainObject(after) &&
590
+ isPlainObject(target)
591
+ ) || (
592
+ isArray(before) &&
593
+ isArray(after) &&
594
+ isArray(target) &&
595
+ before.length === after.length &&
596
+ after.length === target.length
597
+ )
594
598
  )
595
599
  if (!isSameShape) {
596
600
  return clone(after)
@@ -16,12 +16,14 @@ import {
16
16
  someNestedSchemaComponent,
17
17
  hasNestedSchemaComponents,
18
18
  isPanelWithOwnData,
19
+ isSourceWithResource,
19
20
  isNested
20
21
  } from './structure.js'
21
22
  import {
22
23
  hasFormSchema,
23
24
  hasMultipleFormSchemas,
24
25
  getFormSchemas,
26
+ getItemFormSchema,
25
27
  getItemFormSchemaFromForms
26
28
  } from './lookup.js'
27
29
 
@@ -129,9 +131,10 @@ export function isMissingValue(schema, data, name, context) {
129
131
  // Only read the value if needed, so that callers that are tracked, e.g. the
130
132
  // walk of the form model's data, don't depend on all values.
131
133
  return (
132
- !(name in data) ||
133
- !!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context)) &&
134
- data[name] === null
134
+ !(name in data) || (
135
+ !!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context)) &&
136
+ data[name] === null
137
+ )
135
138
  )
136
139
  }
137
140
 
@@ -178,6 +181,7 @@ export function initializeData(schema, data = {}, component, {
178
181
  dataPath = null,
179
182
  rootData = data,
180
183
  shouldProcess,
184
+ shouldSkipSourcesWithResource = false,
181
185
  // Whether to also set the defaults of components with `compute()`. New data
182
186
  // starts with all defaults, which `compute()` can rely on. In data that was
183
187
  // loaded, `compute()` may derive missing values instead, and they fall back
@@ -214,6 +218,7 @@ export function initializeData(schema, data = {}, component, {
214
218
  dataPath,
215
219
  before,
216
220
  shouldProcess,
221
+ shouldSkipSourcesWithResource,
217
222
  options
218
223
  })
219
224
  }
@@ -429,6 +434,11 @@ export function processData(schema, sourceSchema, data, dataPath, {
429
434
  * them contain the `componentPath` of the component, tab or panel, continuing
430
435
  * `componentPath` like `DitoMixin.componentPath` does.
431
436
  *
437
+ * With `shouldSkipSourcesWithResource`, sources with their own resource are
438
+ * skipped along with their items, which they load and save through their
439
+ * resource, but not their panels, which display the data that contains them,
440
+ * see `DitoContainer.panelEntries`.
441
+ *
432
442
  * The primitive values of lists with `wrapPrimitives` are walked wrapped in
433
443
  * objects under the `wrapPrimitives` key, like the admin edits them, see
434
444
  * `SourceMixin`. `wrappedPrimitiveName` is that key while walking such an
@@ -444,9 +454,16 @@ export function processSchemaData(schema, data, {
444
454
  before = null,
445
455
  after = null,
446
456
  shouldProcess = () => true,
457
+ shouldSkipSourcesWithResource = false,
447
458
  options
448
459
  }) {
449
- const walkOptions = { before, after, shouldProcess, options }
460
+ const walkOptions = {
461
+ before,
462
+ after,
463
+ shouldProcess,
464
+ shouldSkipSourcesWithResource,
465
+ options
466
+ }
450
467
  const getDataPath = (dataPath, token) =>
451
468
  dataPath != null
452
469
  ? appendDataPath(dataPath, token)
@@ -472,6 +489,13 @@ export function processSchemaData(schema, data, {
472
489
  if (!shouldProcess(entry)) {
473
490
  continue
474
491
  }
492
+ if (
493
+ shouldSkipSourcesWithResource &&
494
+ isSourceWithResource(componentSchema)
495
+ ) {
496
+ processTabOrPanelSchemas(componentSchema.panels, entry.componentPath)
497
+ continue
498
+ }
475
499
  if (isPanelWithOwnData(componentSchema)) {
476
500
  // The components of panels with their own data don't edit `data`.
477
501
  continue
@@ -582,7 +606,20 @@ export function processSchemaData(schema, data, {
582
606
  }
583
607
  }
584
608
 
585
- processComponents(schema.components, componentPath)
609
+ const components = isFunction(schema.components)
610
+ ? // Unnested components with `components()` callbacks, e.g. sections:
611
+ getItemFormSchema(
612
+ schema,
613
+ data,
614
+ new DitoContext(options.component, {
615
+ schema,
616
+ data,
617
+ dataPath,
618
+ rootData: options.rootData
619
+ })
620
+ ).components
621
+ : schema.components
622
+ processComponents(components, componentPath)
586
623
  processTabOrPanelSchemas(schema.tabs, componentPath)
587
624
  processTabOrPanelSchemas(schema.panels, componentPath)
588
625
 
@@ -6,26 +6,32 @@ import {
6
6
  isFunction,
7
7
  camelize
8
8
  } from '@ditojs/utils'
9
- import { reactive } from 'vue'
9
+ import { reactive, computed } from 'vue'
10
10
  import { getTypeOptions } from './types.js'
11
11
  import {
12
12
  findNestedSchemaComponent,
13
13
  isSchema,
14
14
  isSingleComponentView
15
15
  } from './structure.js'
16
- // TODO: `getFormSchemas()` processes forms with `components()` callbacks on
17
- // every call, which also causes the only circular import between these
18
- // modules. Move this out of the lookup, see the form model.
16
+ // TODO: Setting up the forms of `components()` callbacks causes the only
17
+ // circular import between these modules.
19
18
  import { setupForm } from './setup.js'
20
19
 
21
20
  const emptySchema = {}
22
21
 
22
+ // The forms with the components that `components()` callbacks create, by
23
+ // callback and by the data that they create them for, see
24
+ // `getFormWithCreatedComponents()`.
25
+ const formsByComponentsCallback = new WeakMap()
26
+
23
27
  export function hasFormSchema(schema) {
24
28
  // Support both single form and multiple forms notation, as well as inlined
25
- // components.
29
+ // components, also created by `components()` callbacks.
26
30
  return (
27
- isSchema(schema) &&
28
- isObject(schema.form || schema.forms || schema.components)
31
+ isSchema(schema) && (
32
+ isObject(schema.form || schema.forms || schema.components) ||
33
+ isFunction(schema.components)
34
+ )
29
35
  )
30
36
  }
31
37
 
@@ -96,20 +102,51 @@ export function getFormSchemas(schema, context, modifyForm) {
96
102
  Object.entries(forms).map(([type, form]) => {
97
103
  // Support `schema.components` callbacks to create components on the fly.
98
104
  if (context && isFunction(form.components)) {
99
- // Make the form schema reactive since `setupForm()` is async, so that
100
- // the setting of defaults will be picked up by downstream code.
101
- form = reactive({
102
- ...form,
103
- components: form.components(context)
104
- })
105
- // Process the form again, now that we have the components.
106
- setupForm(context.api, form).catch(console.error)
105
+ form = getFormWithCreatedComponents(form, context)
107
106
  }
108
107
  return [type, modifyForm?.(form) ?? form]
109
108
  })
110
109
  )
111
110
  }
112
111
 
112
+ // Returns the form with the components that its `components()` callback
113
+ // creates for the data of `context`. The forms are cached per data, and the
114
+ // callback only creates new components when the data that it reads changes,
115
+ // so that rendering and the walks of the form model, see `FormModel`, share
116
+ // the same component schemas. `components()` callbacks therefore derive the
117
+ // components from the data of their context, not from its components.
118
+ function getFormWithCreatedComponents(form, context) {
119
+ const { item } = context
120
+ if (!isObject(item)) {
121
+ return createFormWithComponents(form, context)
122
+ }
123
+ let formsByItem = formsByComponentsCallback.get(form.components)
124
+ if (!formsByItem) {
125
+ formsByItem = new WeakMap()
126
+ formsByComponentsCallback.set(form.components, formsByItem)
127
+ }
128
+ let formWithCreatedComponents = formsByItem.get(item)
129
+ if (!formWithCreatedComponents) {
130
+ formWithCreatedComponents = computed(() =>
131
+ createFormWithComponents(form, context)
132
+ )
133
+ formsByItem.set(item, formWithCreatedComponents)
134
+ }
135
+ return formWithCreatedComponents.value
136
+ }
137
+
138
+ function createFormWithComponents(form, context) {
139
+ // Make the form schema reactive since `setupForm()` is async, so that the
140
+ // setting of defaults will be picked up by downstream code.
141
+ const formWithComponents = reactive({
142
+ ...form,
143
+ components: form.components(context)
144
+ })
145
+ // Process the form again, now that it has its components.
146
+ setupForm(context.api, formWithComponents).catch(console.error)
147
+ return formWithComponents
148
+ }
149
+
113
150
  export function getItemFormSchemaFromForms(forms, item) {
114
151
  return forms[item?.type] || forms.default || null
115
152
  }
@@ -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
@@ -1548,9 +1548,18 @@ export interface RadioSchema<$Item = any>
1548
1548
  layout?: 'horizontal' | 'vertical'
1549
1549
  }
1550
1550
 
1551
- type SectionContent<$Data> = {
1552
- /** The section's field components. */
1553
- components?: Components<$Data>
1551
+ type SectionContent<$Data, $Item = $Data> = {
1552
+ /**
1553
+ * The section's field components, or a callback that
1554
+ * creates them from the data, e.g. fields that depend on
1555
+ * a selected template. The callback runs again when the
1556
+ * data that it reads changes, and receives the context of
1557
+ * the section, so that `item` is the item that contains
1558
+ * it, also for nested sections.
1559
+ */
1560
+ components?:
1561
+ | Components<$Data>
1562
+ | ItemAccessor<$Item, {}, Components<$Data>>
1554
1563
  /**
1555
1564
  * A form schema for the section's content. Use this
1556
1565
  * instead of `components` to get form-level options
@@ -1690,7 +1699,7 @@ export type SectionSchema<$Item = any, $Nested = $Item> = BaseSchema<$Item> &
1690
1699
  * @defaultValue `false`
1691
1700
  */
1692
1701
  nested: true
1693
- } & SectionContent<$Nested>)
1702
+ } & SectionContent<$Nested, $Item>)
1694
1703
  )
1695
1704
 
1696
1705
  export interface CheckboxSchema<$Item = any> extends BaseSchema<$Item> {