@ditojs/admin 3.2.6 → 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 +6012 -5827
  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 +29 -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
@@ -0,0 +1,114 @@
1
+ import {
2
+ isObject,
3
+ isString,
4
+ isNumber,
5
+ isFunction,
6
+ escapeHtml
7
+ } from '@ditojs/utils'
8
+ import { isListSource } from './structure.js'
9
+ import { getItemId } from './data.js'
10
+
11
+ // Returns the id of `item`, by which routes address it. Transient items aren't
12
+ // stored yet and may lack ids, so they're addressed by their `index` instead,
13
+ // when it's known.
14
+ export function getItemIdOrIndex(
15
+ sourceSchema,
16
+ item,
17
+ { index = null, isTransient }
18
+ ) {
19
+ return isTransient && index !== null
20
+ ? String(index)
21
+ : getItemId(sourceSchema, item)
22
+ }
23
+
24
+ // Returns the index of the item in `items` that `itemId` addresses, see
25
+ // `getItemIdOrIndex()`, or `null` if there is none. The ids of transient items
26
+ // are their indices already, and are returned as they are.
27
+ export function findItemIndexById(sourceSchema, items, itemId, {
28
+ isTransient
29
+ }) {
30
+ const index = isTransient
31
+ ? itemId
32
+ : items?.findIndex(item => getItemId(sourceSchema, item) === itemId)
33
+ return index != null && index !== -1 ? index : null
34
+ }
35
+
36
+ // Returns the label of `item` as HTML: Values of the item are escaped,
37
+ // while labels that the schema provides, e.g. through `itemLabel()`, can
38
+ // contain HTML, so they need to escape the values that they include.
39
+ // The label depends on the component that displays the item through two
40
+ // callbacks: `evaluateItemLabel()` calls `sourceSchema.itemLabel()` with the
41
+ // item's context, and `getFormLabel()` returns the label of the item's form.
42
+ export function getItemLabel(
43
+ sourceSchema,
44
+ item,
45
+ {
46
+ index = null,
47
+ extended = false,
48
+ asObject = false,
49
+ evaluateItemLabel,
50
+ getFormLabel
51
+ }
52
+ ) {
53
+ const { itemLabel } = sourceSchema
54
+ if (!item || !extended && itemLabel === false) {
55
+ return null
56
+ }
57
+
58
+ let text
59
+ let prefix
60
+ let suffix
61
+ if (isFunction(itemLabel)) {
62
+ const label = evaluateItemLabel()
63
+ if (isObject(label)) {
64
+ ;({ text, prefix, suffix } = label)
65
+ } else {
66
+ text = label
67
+ }
68
+ // It's up to `itemLabel()` entirely to produce the label:
69
+ extended = false
70
+ } else if (isString(itemLabel) && !(itemLabel in item)) {
71
+ // `itemLabel` can be both a key, or simply a label.
72
+ text = itemLabel
73
+ } else {
74
+ // Look up the name on the item, by these rules:
75
+ // 1. If `itemLabel` is a string, use it as the property key
76
+ // 2. Otherwise, if there are columns, use the value of the first
77
+ // 3. Otherwise, see if the item has a property named 'name'
78
+ const { columns } = sourceSchema
79
+ const key = (
80
+ isString(itemLabel) && itemLabel ||
81
+ isListSource(sourceSchema) && columns && Object.keys(columns)[0] ||
82
+ 'name'
83
+ )
84
+ const value = item[key]
85
+ // Only primitives display as a label. If the property holds an array
86
+ // or object, fall through to the auto-generated label. Strings are
87
+ // escaped, as the label is rendered as HTML.
88
+ text = isString(value)
89
+ ? escapeHtml(value)
90
+ : isNumber(value)
91
+ ? value
92
+ : null
93
+ }
94
+ const hadLabel = !!text
95
+ // If no label was found so far, try to produce one from the index.
96
+ if (text == null) {
97
+ // Always use extended style when auto-generating labels from index/id:
98
+ extended = true
99
+ text = isListSource(sourceSchema) && index !== null ? `${index + 1}` : ''
100
+ }
101
+ if (extended) {
102
+ const formLabel = getFormLabel()
103
+ if (formLabel) {
104
+ // If a label was provided, put in quotes when prefixed with the
105
+ // form label for the extended style:
106
+ text = `${formLabel} ${hadLabel ? `'${text}'` : text}`
107
+ }
108
+ }
109
+ return asObject
110
+ ? text || prefix || suffix
111
+ ? { text, prefix, suffix }
112
+ : null
113
+ : text
114
+ }
@@ -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
  }