@ditojs/admin 3.1.1 → 3.1.3

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.1",
3
+ "version": "3.1.3",
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.1",
45
- "@ditojs/utils": "^3.1.1"
44
+ "@ditojs/ui": "^3.1.3",
45
+ "@ditojs/utils": "^3.1.3"
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.1",
90
+ "@ditojs/build": "^3.1.3",
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": "acce85bb38d817aab18f9d5d03b3aa9dff00c3f9"
134
+ "gitHead": "3dba854ad248a56da1614122b960ad8a7eda70dd"
135
135
  }
@@ -4,7 +4,7 @@
4
4
  v-if="creatableForm"
5
5
  :type="isInlined ? 'button' : 'submit'"
6
6
  :disabled="disabled"
7
- v-bind="getButtonAttributes(verb, formLabel)"
7
+ v-bind="getButtonAttributes(verb, formLabel, text)"
8
8
  @click="createItem(creatableForm)"
9
9
  ) {{ text }}
10
10
  template(
@@ -13,7 +13,10 @@
13
13
  button.dito-button(
14
14
  type="button"
15
15
  :disabled="disabled"
16
- v-bind="{ ...pulldownTriggerAttributes, ...getButtonAttributes(verb) }"
16
+ v-bind=`{
17
+ ...pulldownTriggerAttributes,
18
+ ...getButtonAttributes(verb, null, text)
19
+ }`
17
20
  @mousedown.stop="onPulldownMouseDown()"
18
21
  ) {{ text }}
19
22
  ul.dito-pulldown(
@@ -295,13 +295,18 @@ export default {
295
295
  : labelize(name) || ''
296
296
  },
297
297
 
298
- getButtonAttributes(verb, subject = null) {
299
- // Name buttons by what they act on if known, e.g. 'Add Section'.
300
- const label = subject ? `${labelize(verb)} ${subject}` : labelize(verb)
298
+ getButtonAttributes(verb, subject = null, text = null) {
299
+ // Buttons that display text are named by it, others by what they act on
300
+ // if known, e.g. 'Add Section'.
301
+ const label = text
302
+ ? null
303
+ : `${labelize(verb)}${subject ? ` ${subject}` : ''}`
301
304
  return {
302
- 'class': `dito-button--${verb}`,
303
- 'title': label,
304
- 'aria-label': label
305
+ class: `dito-button--${verb}`,
306
+ ...(label && {
307
+ 'title': label,
308
+ 'aria-label': label
309
+ })
305
310
  }
306
311
  },
307
312
 
@@ -82,11 +82,6 @@ export default {
82
82
  return this.getOptionForValue(this.selectedValue)
83
83
  },
84
84
 
85
- // The form model of the data, owned by the dialog or route component.
86
- formModel() {
87
- return (this.dialogComponent ?? this.routeComponent).formModel
88
- },
89
-
90
85
  // The resolver of the options in the form model of the data, which loads
91
86
  // them and shares them with the computes of the form. It is looked up with
92
87
  // the entry of this component, in the shape of the entries of
@@ -424,7 +424,7 @@ export default {
424
424
  // @override ResourceMixin.clearData()
425
425
  clearData() {
426
426
  this.total = 0
427
- this.value = null
427
+ this.setLoadedValue(null)
428
428
  },
429
429
 
430
430
  // @override ResourceMixin.setData()
@@ -437,7 +437,7 @@ export default {
437
437
  if (this.isListSource && isArray(data)) {
438
438
  this.setLoadedListItems(data)
439
439
  } else if (!data || this.isObjectSource && isObject(data)) {
440
- this.value = data
440
+ this.setLoadedValue(data)
441
441
  } else if (this.unwrapListData(data)) {
442
442
  // The format didn't match, see if we received a `{ results, total }`
443
443
  // object, in which case `this.value` was already set by
@@ -483,7 +483,21 @@ export default {
483
483
  // current order. Lists in the data of forms, views and dialogs are
484
484
  // numbered by `FormModel`, see `initializeData()`.
485
485
  setLoadedListItems(items) {
486
- this.value = updateOrder(this.sourceSchema, items, this.paginationRange)
486
+ this.setLoadedValue(
487
+ updateOrder(this.sourceSchema, items, this.paginationRange)
488
+ )
489
+ },
490
+
491
+ // Writes the value loaded through the source's resource. Loading isn't an
492
+ // edit of the data, so the value is written as a clean change, which
493
+ // doesn't make the form dirty, e.g. for sources whose value is stored,
494
+ // like the order of their items, see `FormModel.applyCleanChanges()`.
495
+ setLoadedValue(value) {
496
+ this.formModel
497
+ .applyCleanChanges(() => {
498
+ this.value = value
499
+ })
500
+ .catch(console.error)
487
501
  },
488
502
 
489
503
  createItem(schema, type, index = null) {
@@ -37,6 +37,11 @@ export default {
37
37
  return this.schema.name
38
38
  },
39
39
 
40
+ // The form model of the data, owned by the dialog or route component.
41
+ formModel() {
42
+ return (this.dialogComponent ?? this.routeComponent).formModel
43
+ },
44
+
40
45
  type() {
41
46
  return this.schema.type
42
47
  },
@@ -251,9 +251,11 @@ export default DitoTypeComponent.register('markup', {
251
251
  const onFocus = () => this.onFocus()
252
252
 
253
253
  const onBlur = () => {
254
- // Write the value first, so that validating on blur validates it.
254
+ // Write the value first, so that validating on blur validates it, and
255
+ // emit the change once the editor isn't focused anymore.
255
256
  updateValue()
256
257
  this.onBlur()
258
+ emitChange()
257
259
  }
258
260
 
259
261
  const onUpdate = () => {
@@ -275,9 +277,16 @@ export default DitoTypeComponent.register('markup', {
275
277
  ignoreWatch = true
276
278
  this.value = value
277
279
  }
278
- if (!this.focused && changed) {
279
- this.onChange()
280
+ if (!this.focused) {
281
+ emitChange()
282
+ }
283
+ }
284
+
285
+ // Emits the change of the value, once the editing is done.
286
+ const emitChange = () => {
287
+ if (changed) {
280
288
  changed = false
289
+ this.onChange()
281
290
  }
282
291
  }
283
292
 
@@ -584,7 +584,11 @@ function takeOverChangedValues(target, before, after) {
584
584
  return target
585
585
  }
586
586
  const isSameShape = (
587
- isPlainObject(before) && isPlainObject(after) && isPlainObject(target) || (
587
+ (
588
+ isPlainObject(before) &&
589
+ isPlainObject(after) &&
590
+ isPlainObject(target)
591
+ ) || (
588
592
  isArray(before) &&
589
593
  isArray(after) &&
590
594
  isArray(target) &&
@@ -23,6 +23,7 @@ import {
23
23
  hasFormSchema,
24
24
  hasMultipleFormSchemas,
25
25
  getFormSchemas,
26
+ getItemFormSchema,
26
27
  getItemFormSchemaFromForms
27
28
  } from './lookup.js'
28
29
 
@@ -605,7 +606,20 @@ export function processSchemaData(schema, data, {
605
606
  }
606
607
  }
607
608
 
608
- 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)
609
623
  processTabOrPanelSchemas(schema.tabs, componentPath)
610
624
  processTabOrPanelSchemas(schema.panels, componentPath)
611
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
  }
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> {