@ditojs/admin 3.2.7 → 3.3.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.
Files changed (112) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +6011 -5826
  3. package/dist/dito-admin.umd.js +3 -7
  4. package/package.json +18 -17
  5. package/src/DitoAdmin.js +44 -24
  6. package/src/DitoContext.js +28 -2
  7. package/src/DitoTypeComponent.js +1 -0
  8. package/src/appState.js +6 -3
  9. package/src/components/DitoAccount.vue +35 -27
  10. package/src/components/DitoAffixes.vue +9 -2
  11. package/src/components/DitoClipboard.vue +59 -93
  12. package/src/components/DitoConfirmMessage.vue +18 -0
  13. package/src/components/DitoContainer.vue +56 -18
  14. package/src/components/DitoCreateButton.vue +36 -84
  15. package/src/components/DitoDialog.vue +20 -15
  16. package/src/components/DitoDragHandle.vue +44 -0
  17. package/src/components/DitoDraggable.vue +43 -16
  18. package/src/components/DitoEditButtons.vue +48 -24
  19. package/src/components/DitoErrors.vue +9 -56
  20. package/src/components/DitoForm.vue +82 -42
  21. package/src/components/DitoInputAffixes.vue +50 -0
  22. package/src/components/DitoLabel.vue +1 -15
  23. package/src/components/DitoMenu.vue +45 -52
  24. package/src/components/DitoNotifications.vue +38 -14
  25. package/src/components/DitoOptionList.vue +81 -0
  26. package/src/components/DitoOptionsEditButtons.vue +57 -0
  27. package/src/components/DitoPagination.vue +1 -8
  28. package/src/components/DitoPane.vue +51 -39
  29. package/src/components/DitoPanel.vue +12 -26
  30. package/src/components/DitoPanels.vue +11 -5
  31. package/src/components/DitoResizeHandle.vue +47 -0
  32. package/src/components/DitoRoot.vue +113 -320
  33. package/src/components/DitoSchema.vue +72 -63
  34. package/src/components/DitoSchemaInlined.vue +8 -9
  35. package/src/components/DitoScopes.vue +14 -18
  36. package/src/components/DitoSidebar.vue +4 -29
  37. package/src/components/DitoSourceItem.vue +114 -0
  38. package/src/components/DitoTableCell.vue +4 -0
  39. package/src/components/DitoTableHead.vue +15 -17
  40. package/src/components/DitoTabs.vue +25 -0
  41. package/src/components/DitoTrail.vue +25 -25
  42. package/src/components/DitoTreeItem.vue +102 -141
  43. package/src/components/DitoUploadFile.vue +5 -2
  44. package/src/components/DitoView.vue +33 -24
  45. package/src/index.js +4 -1
  46. package/src/mixins/DitoMixin.js +77 -110
  47. package/src/mixins/DomMixin.js +12 -14
  48. package/src/mixins/EmitterMixin.js +3 -2
  49. package/src/mixins/ItemMixin.js +38 -94
  50. package/src/mixins/OptionsMixin.js +38 -25
  51. package/src/mixins/ResourceMixin.js +182 -111
  52. package/src/mixins/RouteMixin.js +61 -44
  53. package/src/mixins/SchemaParentMixin.js +4 -1
  54. package/src/mixins/SourceMixin.js +118 -100
  55. package/src/mixins/TypeMixin.js +42 -40
  56. package/src/mixins/ValidationMixin.js +16 -4
  57. package/src/mixins/ValidatorMixin.js +1 -1
  58. package/src/styles/_settings.scss +4 -0
  59. package/src/styles/_tippy.scss +0 -11
  60. package/src/styles/style.scss +0 -1
  61. package/src/types/DitoTypeButton.vue +11 -14
  62. package/src/types/DitoTypeCheckboxes.vue +35 -17
  63. package/src/types/DitoTypeCode.vue +19 -41
  64. package/src/types/DitoTypeColor.vue +67 -19
  65. package/src/types/DitoTypeDate.vue +11 -14
  66. package/src/types/DitoTypeList.vue +50 -62
  67. package/src/types/DitoTypeMarkup.vue +41 -54
  68. package/src/types/DitoTypeMultiselect.vue +82 -52
  69. package/src/types/DitoTypeNumber.vue +12 -15
  70. package/src/types/DitoTypeObject.vue +4 -38
  71. package/src/types/DitoTypeRadio.vue +19 -6
  72. package/src/types/DitoTypeSection.vue +23 -9
  73. package/src/types/DitoTypeSelect.vue +39 -23
  74. package/src/types/DitoTypeSlider.vue +9 -1
  75. package/src/types/DitoTypeSwitch.vue +1 -1
  76. package/src/types/DitoTypeText.vue +11 -14
  77. package/src/types/DitoTypeTreeList.vue +46 -8
  78. package/src/types/DitoTypeUpload.vue +190 -67
  79. package/src/utils/DataModel.js +368 -146
  80. package/src/utils/FileDropTargets.js +97 -0
  81. package/src/utils/ItemStores.js +31 -0
  82. package/src/utils/ListQuery.js +115 -0
  83. package/src/utils/LoadingTracker.js +76 -0
  84. package/src/utils/SchemaGraph.js +3 -2
  85. package/src/utils/Session.js +151 -0
  86. package/src/utils/ViewRegistry.js +85 -0
  87. package/src/utils/accessor.js +12 -4
  88. package/src/utils/affixes.js +33 -0
  89. package/src/utils/changeOnceEdited.js +33 -0
  90. package/src/utils/clipboard.js +55 -0
  91. package/src/utils/data.js +10 -5
  92. package/src/utils/dialogs.js +59 -0
  93. package/src/utils/dragResize.js +61 -0
  94. package/src/utils/filter.js +36 -2
  95. package/src/utils/html.js +9 -0
  96. package/src/utils/list.js +10 -0
  97. package/src/utils/options.js +4 -0
  98. package/src/utils/registration.js +18 -0
  99. package/src/utils/request.js +108 -13
  100. package/src/utils/route.js +6 -17
  101. package/src/utils/schema/data.js +24 -2
  102. package/src/utils/schema/events.js +19 -0
  103. package/src/utils/schema/item.js +114 -0
  104. package/src/utils/schema/lookup.js +39 -14
  105. package/src/utils/schema/setup.js +5 -5
  106. package/src/utils/schema/validation.js +16 -2
  107. package/src/utils/uid.js +105 -8
  108. package/src/validators/_decimals.js +16 -5
  109. package/types/index.d.ts +168 -41
  110. package/src/mixins/LoadingMixin.js +0 -23
  111. package/src/mixins/PulldownMixin.js +0 -72
  112. package/src/styles/_pulldown.scss +0 -38
@@ -10,6 +10,7 @@ import { reactive, computed } from 'vue'
10
10
  import { getTypeOptions } from './types.js'
11
11
  import {
12
12
  findNestedSchemaComponent,
13
+ isMenu,
13
14
  isSchema,
14
15
  isSingleComponentView
15
16
  } from './structure.js'
@@ -72,6 +73,14 @@ export function getViewPath(schema, context) {
72
73
  return null
73
74
  }
74
75
 
76
+ // Returns the path that links to the view or menu `schema`, displayed at
77
+ // `path`: Menus link to their first item.
78
+ export function getViewLinkPath(schema, path = schema.fullPath) {
79
+ return isMenu(schema)
80
+ ? Object.values(schema.items)[0].fullPath
81
+ : path
82
+ }
83
+
75
84
  export function getViewEditPath(schema, id, context) {
76
85
  const path = getViewPath(schema, context)
77
86
  return path ? `${path}/${id}` : null
@@ -125,14 +134,19 @@ function getFormWithCreatedComponents(form, context) {
125
134
  formsByItem = new WeakMap()
126
135
  formsByComponentsCallback.set(form.components, formsByItem)
127
136
  }
128
- let formWithCreatedComponents = formsByItem.get(item)
129
- if (!formWithCreatedComponents) {
130
- formWithCreatedComponents = computed(() =>
131
- createFormWithComponents(form, context)
137
+ let entry = formsByItem.get(item)
138
+ if (!entry) {
139
+ entry = { context }
140
+ entry.formWithCreatedComponents = computed(() =>
141
+ createFormWithComponents(form, entry.context)
132
142
  )
133
- formsByItem.set(item, formWithCreatedComponents)
143
+ formsByItem.set(item, entry)
134
144
  }
135
- return formWithCreatedComponents.value
145
+ // Recreate the components with the latest context rather than the one of
146
+ // the first caller, whose component may have been unmounted since. Changes
147
+ // of the context alone don't recreate them, see above.
148
+ entry.context = context
149
+ return entry.formWithCreatedComponents.value
136
150
  }
137
151
 
138
152
  function createFormWithComponents(form, context) {
@@ -214,16 +228,27 @@ export function getButtonSchemas(buttons) {
214
228
  }
215
229
 
216
230
  // Returns the entry of the panel described by `schema`, which `DitoPanels`
217
- // displays at `dataPath` and `componentPath`.
218
- function getPanelEntry(schema, dataPath, componentPath, tabComponent) {
219
- return schema ? { schema, dataPath, componentPath, tabComponent } : null
231
+ // displays at `dataPath` and `componentPath`. Panels of components in tabs are
232
+ // only displayed while their tab is selected: `tab` describes that tab as
233
+ // `{ tabName, isTabSelected }`, where `isTabSelected()` tells if it currently
234
+ // is, see `DitoPane.panelTab`.
235
+ function getPanelEntry(schema, dataPath, componentPath, tab) {
236
+ return schema
237
+ ? {
238
+ schema,
239
+ dataPath,
240
+ componentPath,
241
+ tabName: tab?.tabName ?? null,
242
+ isTabSelected: tab?.isTabSelected ?? null
243
+ }
244
+ : null
220
245
  }
221
246
 
222
247
  export function getPanelEntries(
223
248
  panelSchemas,
224
249
  dataPath,
225
250
  componentPath,
226
- tabComponent = null,
251
+ tab = null,
227
252
  panelEntries = []
228
253
  ) {
229
254
  if (panelSchemas) {
@@ -232,7 +257,7 @@ export function getPanelEntries(
232
257
  schema,
233
258
  appendDataPath(dataPath, key),
234
259
  appendDataPath(componentPath, key),
235
- tabComponent
260
+ tab
236
261
  )
237
262
  if (entry) {
238
263
  panelEntries.push(entry)
@@ -256,7 +281,7 @@ export function getAllPanelEntries(
256
281
  dataPathComponentPath,
257
282
  schemaComponentPath,
258
283
  component = null,
259
- tabComponent = null
284
+ tab = null
260
285
  ) {
261
286
  const panelSchema = getTypeOptions(schema)?.getPanelSchema?.(
262
287
  api,
@@ -270,7 +295,7 @@ export function getAllPanelEntries(
270
295
  panelSchema,
271
296
  appendDataPath(dataPath, panelSchema.name),
272
297
  appendDataPath(dataPathComponentPath, panelSchema.name),
273
- tabComponent
298
+ tab
274
299
  )
275
300
  ]
276
301
  : []
@@ -280,7 +305,7 @@ export function getAllPanelEntries(
280
305
  schema?.panels,
281
306
  dataPath,
282
307
  schemaComponentPath,
283
- tabComponent,
308
+ tab,
284
309
  panelEntries
285
310
  )
286
311
  return panelEntries
@@ -97,13 +97,13 @@ export async function resolveSchemas(
97
97
  export async function resolveViews(unresolvedViews) {
98
98
  return resolveSchemas(unresolvedViews, async (schema, unwrapModule) => {
99
99
  schema = await resolveSchema(schema, unwrapModule)
100
- if (!schema.name && isMenu(schema)) {
101
- // Generate a name for sub-menus from their label if it's missing.
102
- // NOTE: This is never actually referenced from anywhere, but they need
103
- // a name by which they're stored in the parent object.
100
+ if (isMenu(schema)) {
104
101
  schema = {
105
102
  ...schema,
106
- name: camelize(schema.label),
103
+ // Generate a name for sub-menus from their label if it's missing.
104
+ // NOTE: This is never actually referenced from anywhere, but they need
105
+ // a name by which they're stored in the parent object.
106
+ name: schema.name || camelize(schema.label),
107
107
  items: await resolveSchemas(schema.items)
108
108
  }
109
109
  }
@@ -40,8 +40,7 @@ export function getValidations(schema, context) {
40
40
  export function getValueValidationErrors(value, validations) {
41
41
  const errors = []
42
42
  for (const [name, setting] of Object.entries(validations)) {
43
- // eslint-disable-next-line import/namespace
44
- const validator = validators[name]
43
+ const validator = getValidator(name)
45
44
  if (
46
45
  validator &&
47
46
  // Only apply 'required' validator to empty values.
@@ -61,6 +60,21 @@ export function getValueValidationErrors(value, validations) {
61
60
  return errors
62
61
  }
63
62
 
63
+ // The names of unknown validators that were warned about, see `getValidator()`:
64
+ const warnedUnknownValidatorNames = new Set()
65
+
66
+ // Returns the validator of the name, or `null` with a warning, once per name,
67
+ // e.g. for misspelled `schema.rules`.
68
+ function getValidator(name) {
69
+ // eslint-disable-next-line import/namespace
70
+ const validator = validators[name] ?? null
71
+ if (!validator && !warnedUnknownValidatorNames.has(name)) {
72
+ warnedUnknownValidatorNames.add(name)
73
+ console.warn(`Unknown validator '${name}' is ignored.`)
74
+ }
75
+ return validator
76
+ }
77
+
64
78
  /**
65
79
  * Validates `data` along the components of `schema`, including tabs, panels
66
80
  * and the nested forms of list and object items, skipping components whose
package/src/utils/uid.js CHANGED
@@ -1,15 +1,112 @@
1
1
  import { toRaw } from 'vue'
2
+ import { isArray, isPlainObject, isString } from '@ditojs/utils'
3
+ import { isTemporaryId } from './data.js'
2
4
 
3
- const uidMap = new WeakMap()
5
+ const uidsByItem = new WeakMap()
4
6
 
5
- // Generated and remembers unique ids per passed object using a weak map.
6
- let uid = 0
7
+ // Generated uids have their own prefix, so that they can't be mistaken for
8
+ // temporary ids, see `setTemporaryId()`.
9
+ const generatedUidPrefix = 'uid-'
10
+ let lastGeneratedUid = 0
11
+
12
+ // Generates and remembers unique ids per passed object using a weak map.
7
13
  export function getUid(item, getItemId = null) {
8
14
  const raw = toRaw(item)
9
- let id = uidMap.get(raw)
10
- if (!id && item) {
11
- id = getItemId?.(item) || `@${++uid}`
12
- uidMap.set(raw, id)
15
+ let uid = uidsByItem.get(raw)
16
+ if (!uid && item) {
17
+ uid = getItemId?.(item) || `${generatedUidPrefix}${++lastGeneratedUid}`
18
+ uidsByItem.set(raw, uid)
19
+ }
20
+ return uid
21
+ }
22
+
23
+ // Transfers the uids of new objects in `source`, which are generated or
24
+ // temporary ids, to the corresponding objects in `target` that don't have uids
25
+ // yet, e.g. when the saved data replaces the data that was edited. This way,
26
+ // the components and stores that are keyed by the uids of new items are kept,
27
+ // even though the items only have their ids once they are saved.
28
+ // Objects are matched by their keys. Array items are matched by their `id`
29
+ // properties, see `transferArrayUids()`. The items of lists with a custom
30
+ // `idKey` have no `id`, so they count as new items and are matched in order.
31
+ export function transferUids(source, target) {
32
+ const rawSource = toRaw(source)
33
+ const rawTarget = toRaw(target)
34
+ if (
35
+ rawSource !== rawTarget &&
36
+ isTransferable(rawSource) &&
37
+ isTransferable(rawTarget)
38
+ ) {
39
+ const uid = uidsByItem.get(rawSource)
40
+ if (isNewItemUid(uid) && !uidsByItem.has(rawTarget)) {
41
+ uidsByItem.set(rawTarget, uid)
42
+ }
43
+ if (isArray(rawSource) && isArray(rawTarget)) {
44
+ transferArrayUids(rawSource, rawTarget)
45
+ } else {
46
+ for (const key of Object.keys(rawSource)) {
47
+ if (key in rawTarget) {
48
+ transferUids(rawSource[key], rawTarget[key])
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ // Matches the items of two arrays by their ids rather than their positions, as
56
+ // the saved items may be ordered differently than the edited ones: Each saved
57
+ // item in `source` is matched with the item in `target` that has the same id.
58
+ // The new items in `source`, which have no id or a temporary one, can only be
59
+ // matched with the items in `target` that have none of the saved ids, in order,
60
+ // and only if their counts are equal. Otherwise, they are left unmatched rather
61
+ // than risking to give an item the uid, component and store of another.
62
+ function transferArrayUids(source, target) {
63
+ const sourceItems = source.filter(isTransferable)
64
+ const targetItems = target.filter(isTransferable)
65
+ const targetItemsById = new Map()
66
+ for (const item of targetItems) {
67
+ const id = getSavedItemId(item)
68
+ if (id !== undefined) {
69
+ targetItemsById.set(id, item)
70
+ }
71
+ }
72
+ const savedIds = new Set()
73
+ const newSourceItems = []
74
+ for (const item of sourceItems) {
75
+ const id = getSavedItemId(item)
76
+ if (id !== undefined) {
77
+ savedIds.add(id)
78
+ const targetItem = targetItemsById.get(id)
79
+ if (targetItem) {
80
+ transferUids(item, targetItem)
81
+ }
82
+ } else {
83
+ newSourceItems.push(item)
84
+ }
13
85
  }
14
- return id
86
+ const newTargetItems = targetItems.filter(
87
+ item => !savedIds.has(getSavedItemId(item))
88
+ )
89
+ if (newSourceItems.length === newTargetItems.length) {
90
+ newSourceItems.forEach((item, index) => {
91
+ transferUids(item, newTargetItems[index])
92
+ })
93
+ }
94
+ }
95
+
96
+ // Returns the id of a saved item as a string, so that numeric ids match their
97
+ // string representations, or `undefined` for new items.
98
+ function getSavedItemId(item) {
99
+ const { id } = item
100
+ return id != null && !isTemporaryId(id) ? String(id) : undefined
101
+ }
102
+
103
+ function isTransferable(value) {
104
+ return isArray(value) || isPlainObject(value)
105
+ }
106
+
107
+ function isNewItemUid(uid) {
108
+ return (
109
+ isString(uid) &&
110
+ (uid.startsWith(generatedUidPrefix) || isTemporaryId(uid))
111
+ )
15
112
  }
@@ -1,11 +1,22 @@
1
+ function hasUnlimitedDecimals(decimals) {
2
+ return decimals == null || decimals === '*'
3
+ }
4
+
1
5
  export const decimals = {
2
6
  validate: (value, decimals) => {
3
- const match = decimals === '*' ? '+' : `{1,${decimals}}`
4
- return new RegExp(`^[-+]?\\d*(\\.\\d${match})?$`).test(value)
7
+ // `decimals: 0` allows no decimal point at all, only integers:
8
+ const fraction = hasUnlimitedDecimals(decimals)
9
+ ? '(\\.\\d+)?'
10
+ : decimals > 0
11
+ ? `(\\.\\d{1,${decimals}})?`
12
+ : ''
13
+ return new RegExp(`^[-+]?\\d*${fraction}$`).test(value)
5
14
  },
6
15
 
7
16
  message: (value, decimals) =>
8
- `must be numeric and may contain ${
9
- !decimals || decimals === '*' ? '' : decimals
10
- } decimal points`
17
+ hasUnlimitedDecimals(decimals)
18
+ ? 'must be numeric and may contain decimal points'
19
+ : decimals > 0
20
+ ? `must be numeric and may contain ${decimals} decimal points`
21
+ : 'must be numeric and may not contain decimal points'
11
22
  }
package/types/index.d.ts CHANGED
@@ -353,6 +353,10 @@ export interface BaseSchema<$Item>
353
353
  * context's component properties, e.g. `component`
354
354
  * and `schemaComponent`, refer to the form, view or
355
355
  * dialog that holds the data, not to the field.
356
+ *
357
+ * Reading `context.options` waits while the options
358
+ * load, and is `undefined` when the load fails or its
359
+ * data resolves to nothing, so use `options?.`.
356
360
  */
357
361
  compute?: ItemAccessor<$Item>
358
362
  /**
@@ -629,6 +633,9 @@ export interface SchemaSourceMixin<$Item> {
629
633
  * the default is the 'name' property of the item,
630
634
  * followed by the label of the form (plus item id)
631
635
  * and other defaults.
636
+ * Labels are HTML: Callbacks return HTML, and need to
637
+ * escape the item values that they include with
638
+ * `escapeHtml()` from `@ditojs/utils`.
632
639
  */
633
640
  itemLabel?:
634
641
  | OrItemAccessor<
@@ -850,7 +857,9 @@ export type SchemaOptions<$Item, $Option = any> =
850
857
  */
851
858
  value?: keyof $Option | ItemAccessor<$Item, { option: $Option }>
852
859
  /**
853
- * The key of the option property which should used to group the options.
860
+ * @deprecated Not supported, the options are grouped by the `groupBy`
861
+ * setting of the component's schema, see
862
+ * {@link SchemaOptionsMixin.groupBy}.
854
863
  */
855
864
  groupBy?: keyof $Option
856
865
  /**
@@ -913,7 +922,9 @@ export interface SchemaOptionsMixin<$Item, $Option = any> {
913
922
  relateBy?: string
914
923
  /**
915
924
  * The key of the option property which should be used to
916
- * group the options.
925
+ * group the options. Selects and multiselects show the groups
926
+ * in their dropdowns, radio buttons and checkboxes list each
927
+ * group in a fieldset with the group's name as its legend.
917
928
  */
918
929
  groupBy?: OrItemAccessor<$Item, {}, string>
919
930
  /**
@@ -942,7 +953,8 @@ export interface SchemaOptionsMixin<$Item, $Option = any> {
942
953
  }
943
954
  /**
944
955
  * Whether the selected option can be edited by navigating
945
- * to it.
956
+ * to its form in the view of `view`. Radio buttons and checkboxes
957
+ * show an edit button for each of their options.
946
958
  */
947
959
  editable?: OrItemAccessor<$Item, {}, boolean>
948
960
  /**
@@ -1220,7 +1232,8 @@ export interface DateSchema<$Item = any>
1220
1232
  }
1221
1233
  >
1222
1234
  /**
1223
- * @deprecated Use `formats` instead.
1235
+ * @deprecated Use `formats.date` instead. `dateFormat` is no longer read and
1236
+ * has no effect.
1224
1237
  */
1225
1238
  dateFormat?: OrItemAccessor<$Item, {}, DateFormat>
1226
1239
  }
@@ -1924,9 +1937,10 @@ export type DitoContext<$Item = any> = {
1924
1937
  */
1925
1938
  itemDataPath: string
1926
1939
  /**
1927
- * Data path of the parent item ancestor.
1940
+ * Data path of the parent item ancestor, `null` for
1941
+ * root items.
1928
1942
  */
1929
- parentItemDataPath: string
1943
+ parentItemDataPath: string | null
1930
1944
  /** Index of the closest item in its list. */
1931
1945
  itemIndex: number | null
1932
1946
  /** Index of the parent item in its list. */
@@ -1959,11 +1973,11 @@ export type DitoContext<$Item = any> = {
1959
1973
  * copy/paste operations.
1960
1974
  */
1961
1975
  clipboardItem: any
1962
- /** The currently authenticated user. */
1976
+ /** The currently authenticated user, `null` when logged out. */
1963
1977
  user: {
1964
1978
  roles?: string[]
1965
1979
  hasRole(...roles: string[]): boolean
1966
- }
1980
+ } | null
1967
1981
  /** The admin API configuration. */
1968
1982
  api: ApiConfig
1969
1983
  /** The schema definition for the current component. */
@@ -1972,8 +1986,16 @@ export type DitoContext<$Item = any> = {
1972
1986
  views: Record<string, View>
1973
1987
  /** All views flattened into a single record. */
1974
1988
  flattenedViews: Record<string, ViewSchema>
1975
- /** Display label of the current item. */
1989
+ /**
1990
+ * Display label of the current item, as HTML with
1991
+ * escaped item values.
1992
+ */
1976
1993
  itemLabel: string | null
1994
+ /**
1995
+ * The text of `itemLabel`, with its item values
1996
+ * unescaped.
1997
+ */
1998
+ itemLabelText: string | null
1977
1999
  /** Display label of the current form. */
1978
2000
  formLabel: string | null
1979
2001
  /** The current Vue component instance. */
@@ -2071,25 +2093,55 @@ export type DitoContext<$Item = any> = {
2071
2093
  }
2072
2094
  /** Returns the full URL for a given resource. */
2073
2095
  getResourceUrl(resource: Resource): string
2074
- /** Displays a notification to the user. */
2075
- notify(options: {
2076
- /** Notification style. @defaultValue `'info'` */
2077
- type?: LiteralUnion<'warning' | 'error' | 'info' | 'success'>
2078
- /** Heading text. Defaults to a label matching `type`. */
2079
- title?: string
2080
- /** Body text, rendered as HTML paragraphs. */
2081
- text: OrArrayOf<string>
2082
- /**
2083
- * The original error object. When `type` is `'error'`,
2084
- * this is logged to the console alongside the message.
2085
- */
2086
- error?: unknown
2087
- /**
2088
- * Display duration in milliseconds. When omitted, calculated
2089
- * automatically from content length. Use `0` for sticky.
2090
- */
2091
- duration?: number
2092
- }): void
2096
+ /**
2097
+ * Displays a notification to the user, with the
2098
+ * paragraphs of either `text` or `html`.
2099
+ */
2100
+ notify(
2101
+ options: {
2102
+ /** Notification style. @defaultValue `'info'` */
2103
+ type?: LiteralUnion<'warning' | 'error' | 'info' | 'success'>
2104
+ /**
2105
+ * Heading text, escaped like `text`. Defaults to
2106
+ * a label matching `type`.
2107
+ */
2108
+ title?: string
2109
+ /**
2110
+ * The original error object. When `type` is
2111
+ * `'error'`, this is logged to the console
2112
+ * alongside the message.
2113
+ */
2114
+ error?: unknown
2115
+ /**
2116
+ * Display duration in milliseconds. When omitted,
2117
+ * calculated automatically from content length.
2118
+ * Use `0` for sticky.
2119
+ */
2120
+ duration?: number
2121
+ } & (
2122
+ | {
2123
+ /**
2124
+ * Body text, escaped as plain text. Each entry
2125
+ * is a paragraph, newlines become `<br>`, and
2126
+ * `false` and `null` entries are skipped.
2127
+ */
2128
+ text: OrArrayOf<string | false | null>
2129
+ html?: never
2130
+ }
2131
+ | {
2132
+ /**
2133
+ * Body HTML from trusted sources, rendered as
2134
+ * it is. Every interpolated value must be
2135
+ * escaped with `escapeHtml()` from
2136
+ * `@ditojs/utils`. Each entry is a paragraph,
2137
+ * newlines become `<br>`, and `false` and
2138
+ * `null` entries are skipped.
2139
+ */
2140
+ html: OrArrayOf<string | false | null>
2141
+ text?: never
2142
+ }
2143
+ )
2144
+ ): void
2093
2145
  }
2094
2146
 
2095
2147
  /**
@@ -2169,16 +2221,6 @@ export interface DitoComponentInstanceBase<$Item = any>
2169
2221
  errors: string[] | null
2170
2222
  /** Whether the field has validation errors. */
2171
2223
  hasErrors: boolean
2172
- /** Whether async data is currently being loaded. */
2173
- isLoading: boolean
2174
- /**
2175
- * Sets the loading state. Optionally propagates
2176
- * to the root or view component.
2177
- */
2178
- setLoading(
2179
- isLoading: boolean,
2180
- options?: { updateRoot?: boolean; updateView?: boolean }
2181
- ): void
2182
2224
  /**
2183
2225
  * Whether the component works with data not yet
2184
2226
  * persisted to the server.
@@ -2256,7 +2298,7 @@ export interface DitoComponentInstanceBase<$Item = any>
2256
2298
  /** The parent form component. */
2257
2299
  parentFormComponent: DitoComponentInstanceBase | null
2258
2300
  /** The root component instance. */
2259
- rootComponent: DitoComponentInstanceBase | null
2301
+ rootComponent: DitoRootInstance | null
2260
2302
  /** The nearest ancestor tab component. */
2261
2303
  tabComponent: DitoComponentInstanceBase | null
2262
2304
  /** The parent route component. */
@@ -2485,8 +2527,63 @@ export interface EmitterMixin {
2485
2527
  ): this
2486
2528
  }
2487
2529
 
2530
+ /**
2531
+ * Counts the pending operations of a scope, e.g. the
2532
+ * requests of a resource component, of all components
2533
+ * in a view, or of the whole admin, and forwards them
2534
+ * to the tracker of the enclosing scope.
2535
+ */
2536
+ export interface LoadingTracker {
2537
+ /** The tracker of the enclosing scope, if any. */
2538
+ readonly parent: LoadingTracker | null
2539
+ /** The tracker of the outermost scope. */
2540
+ readonly root: LoadingTracker
2541
+ /** Whether any operation within the scope is pending. */
2542
+ readonly isLoading: boolean
2543
+ /**
2544
+ * Begins an operation in this scope and its enclosing
2545
+ * ones, and returns the function that ends it.
2546
+ */
2547
+ begin(): () => void
2548
+ /**
2549
+ * Tracks the operation that `callback()` performs until
2550
+ * its returned promise settles, and returns its result.
2551
+ */
2552
+ track<T>(callback: () => T | Promise<T>): Promise<T>
2553
+ }
2554
+
2555
+ /**
2556
+ * The members of components that load and submit their
2557
+ * data through API resources (ResourceMixin): forms and
2558
+ * sources.
2559
+ */
2560
+ export interface DitoResourceInstance {
2561
+ /**
2562
+ * Whether any request or other operation within this
2563
+ * component's scope is pending, as tracked by its
2564
+ * `LoadingTracker`.
2565
+ */
2566
+ readonly isLoading: boolean
2567
+ /** Tracks the pending requests of the component. */
2568
+ readonly loadingTracker: LoadingTracker
2569
+ /**
2570
+ * Begins or ends a loading operation of the component.
2571
+ * The options are ignored, since the operations are
2572
+ * always forwarded to the enclosing view and the root.
2573
+ *
2574
+ * @deprecated Use `loadingTracker.begin()` and call the
2575
+ * function that it returns, or
2576
+ * `loadingTracker.track(callback)` instead.
2577
+ */
2578
+ setLoading(
2579
+ isLoading: boolean,
2580
+ options?: { updateRoot?: boolean; updateView?: boolean }
2581
+ ): void
2582
+ }
2583
+
2488
2584
  export interface DitoFormInstance<$Item = any>
2489
- extends DitoComponentInstanceBase<$Item> {
2585
+ extends DitoComponentInstanceBase<$Item>,
2586
+ DitoResourceInstance {
2490
2587
  /**
2491
2588
  * Whether this form is creating a new item
2492
2589
  * (`true`) or editing an existing one (`false`).
@@ -2645,10 +2742,38 @@ export interface DitoFormInstance<$Item = any>
2645
2742
  getChildPath(path: string): string
2646
2743
  }
2647
2744
 
2745
+ /** The root component of the admin (DitoRoot). */
2746
+ export interface DitoRootInstance extends DitoComponentInstanceBase {
2747
+ /**
2748
+ * Whether any request or other operation within the
2749
+ * admin is pending, as tracked by its `LoadingTracker`.
2750
+ */
2751
+ readonly isLoading: boolean
2752
+ /** Tracks the pending requests of the whole admin. */
2753
+ readonly loadingTracker: LoadingTracker
2754
+ }
2755
+
2648
2756
  export interface DitoViewInstance<$Item = any>
2649
2757
  extends DitoComponentInstanceBase<$Item> {
2650
2758
  /** Always `true` for view components. */
2651
2759
  isView: true
2760
+ /**
2761
+ * Whether any request or other operation within the
2762
+ * view, e.g. of its components, is pending, as
2763
+ * tracked by its `LoadingTracker`. The view is
2764
+ * disabled while it is loading.
2765
+ */
2766
+ readonly isLoading: boolean
2767
+ /** Tracks the pending requests of the view's components. */
2768
+ readonly loadingTracker: LoadingTracker
2769
+ /**
2770
+ * Begins or ends a loading operation of the view.
2771
+ *
2772
+ * @deprecated Use `loadingTracker.begin()` and call the
2773
+ * function that it returns, or
2774
+ * `loadingTracker.track(callback)` instead.
2775
+ */
2776
+ setLoading(isLoading: boolean): void
2652
2777
 
2653
2778
  // -- Route (RouteMixin) --
2654
2779
 
@@ -2767,7 +2892,8 @@ export interface DitoSchemaInstance<$Item = any>
2767
2892
  }
2768
2893
 
2769
2894
  export interface DitoSourceInstance<$Item = any>
2770
- extends DitoComponentInstanceBase<$Item> {
2895
+ extends DitoComponentInstanceBase<$Item>,
2896
+ DitoResourceInstance {
2771
2897
  // -- Data access (SourceMixin) --
2772
2898
 
2773
2899
  /** The list data array (getter/setter). */
@@ -3465,6 +3591,7 @@ export type SchemaByType<$Item = any> = {
3465
3591
  'url': InputSchema<$Item>
3466
3592
  'label': LabelSchema<$Item>
3467
3593
  'section': SectionSchema<$Item>
3594
+ 'panel': PanelSchema<$Item>
3468
3595
  'hidden': HiddenSchema<$Item>
3469
3596
  'unknown': never
3470
3597
  }
@@ -1,23 +0,0 @@
1
- // @vue/component
2
- export default {
3
- data() {
4
- return {
5
- isLoading: false
6
- }
7
- },
8
-
9
- methods: {
10
- setLoading(isLoading, { updateRoot = false, updateView = false } = {}) {
11
- if (!this.isLoading ^ !isLoading) {
12
- // Boolean xor
13
- this.isLoading = !!isLoading
14
- if (updateRoot) {
15
- this.rootComponent.registerLoading(isLoading)
16
- }
17
- if (updateView) {
18
- this.viewComponent.setLoading(this.isLoading)
19
- }
20
- }
21
- }
22
- }
23
- }