@ditojs/admin 2.100.0 → 3.0.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 (84) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3421 -2662
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +81 -45
  5. package/src/DitoComponent.js +1 -1
  6. package/src/DitoContext.js +51 -0
  7. package/src/DitoTypeComponent.js +6 -1
  8. package/src/components/DitoAffixes.vue +6 -3
  9. package/src/components/DitoContainer.vue +19 -10
  10. package/src/components/DitoCreateButton.vue +11 -2
  11. package/src/components/DitoDialog.vue +21 -2
  12. package/src/components/DitoForm.vue +156 -83
  13. package/src/components/DitoLabel.vue +3 -2
  14. package/src/components/DitoPane.vue +55 -11
  15. package/src/components/DitoPanel.vue +33 -5
  16. package/src/components/DitoPanels.vue +2 -1
  17. package/src/components/DitoRoot.vue +13 -6
  18. package/src/components/DitoSchema.vue +278 -145
  19. package/src/components/DitoSchemaInlined.vue +1 -1
  20. package/src/components/DitoTabs.vue +16 -18
  21. package/src/components/DitoTrail.vue +5 -4
  22. package/src/components/DitoTreeItem.vue +17 -6
  23. package/src/components/DitoView.vue +25 -2
  24. package/src/mixins/DitoMixin.js +42 -7
  25. package/src/mixins/ItemMixin.js +3 -6
  26. package/src/mixins/NumberMixin.js +47 -31
  27. package/src/mixins/OptionsMixin.js +122 -71
  28. package/src/mixins/ResourceMixin.js +5 -2
  29. package/src/mixins/RouteMixin.js +39 -1
  30. package/src/mixins/SortableMixin.js +0 -13
  31. package/src/mixins/SourceMixin.js +142 -95
  32. package/src/mixins/TextMixin.js +1 -1
  33. package/src/mixins/TypeMixin.js +37 -22
  34. package/src/mixins/ValidationMixin.js +9 -27
  35. package/src/mixins/ValidatorMixin.js +5 -7
  36. package/src/mixins/ValueMixin.js +5 -2
  37. package/src/types/DitoTypeButton.vue +1 -1
  38. package/src/types/DitoTypeCheckbox.vue +1 -1
  39. package/src/types/DitoTypeCheckboxes.vue +1 -1
  40. package/src/types/DitoTypeCode.vue +8 -1
  41. package/src/types/DitoTypeColor.vue +20 -34
  42. package/src/types/DitoTypeComponent.vue +1 -1
  43. package/src/types/DitoTypeComputed.vue +4 -27
  44. package/src/types/DitoTypeDate.vue +16 -5
  45. package/src/types/DitoTypeLabel.vue +1 -4
  46. package/src/types/DitoTypeList.vue +9 -5
  47. package/src/types/DitoTypeMarkup.vue +25 -13
  48. package/src/types/DitoTypeMultiselect.vue +26 -6
  49. package/src/types/DitoTypeNumber.vue +1 -1
  50. package/src/types/DitoTypeObject.vue +10 -7
  51. package/src/types/DitoTypePanel.vue +19 -6
  52. package/src/types/DitoTypeProgress.vue +1 -1
  53. package/src/types/DitoTypeRadio.vue +1 -1
  54. package/src/types/DitoTypeSection.vue +32 -4
  55. package/src/types/DitoTypeSelect.vue +1 -1
  56. package/src/types/DitoTypeSlider.vue +1 -1
  57. package/src/types/DitoTypeSwitch.vue +1 -1
  58. package/src/types/DitoTypeText.vue +13 -15
  59. package/src/types/DitoTypeTextarea.vue +1 -1
  60. package/src/types/DitoTypeTreeList.vue +12 -7
  61. package/src/types/DitoTypeUpload.vue +1 -2
  62. package/src/utils/DataSchemaResolver.js +141 -0
  63. package/src/utils/DataSchemaResolver.test.js +132 -0
  64. package/src/utils/FormModel.js +556 -0
  65. package/src/utils/FormModel.test.js +808 -0
  66. package/src/utils/data.js +20 -0
  67. package/src/utils/data.test.js +28 -0
  68. package/src/utils/filter.js +3 -2
  69. package/src/utils/options.js +3 -0
  70. package/src/utils/schema/data.js +617 -0
  71. package/src/utils/schema/data.test.js +471 -0
  72. package/src/utils/schema/lookup.js +266 -0
  73. package/src/utils/schema/lookup.test.js +61 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/structure.test.js +57 -0
  77. package/src/utils/schema/types.js +34 -0
  78. package/src/utils/schema/validation.js +142 -0
  79. package/src/utils/schema/validation.test.js +395 -0
  80. package/src/utils/units.js +70 -3
  81. package/src/utils/units.test.js +71 -0
  82. package/types/index.d.ts +149 -59
  83. package/src/mixins/DataMixin.js +0 -131
  84. package/src/utils/schema.js +0 -1121
@@ -1,7 +1,7 @@
1
1
  <template lang="pug">
2
2
  .dito-slider
3
3
  input.dito-range(
4
- :id="dataPath"
4
+ :id="componentPath"
5
5
  ref="element"
6
6
  v-model="inputValue"
7
7
  type="range"
@@ -1,6 +1,6 @@
1
1
  <template lang="pug">
2
2
  DitoSwitch.dito-switch(
3
- :id="dataPath"
3
+ :id="componentPath"
4
4
  ref="element"
5
5
  v-model="value"
6
6
  :labels="labels"
@@ -1,6 +1,6 @@
1
1
  <template lang="pug">
2
2
  DitoInput.dito-text(
3
- :id="dataPath"
3
+ :id="componentPath"
4
4
  ref="element"
5
5
  v-model="inputValue"
6
6
  :type="inputType"
@@ -54,6 +54,18 @@ export default DitoTypeComponent.register(
54
54
  textField: true,
55
55
  ignoreMissingValue: ({ schema }) => schema.type === 'password',
56
56
 
57
+ getTypeValidations(schema) {
58
+ const validationName = {
59
+ email: 'email',
60
+ url: 'url',
61
+ hostname: 'hostname',
62
+ domain: 'domain',
63
+ password: 'password',
64
+ creditcard: 'creditcard'
65
+ }[schema.type]
66
+ return validationName ? { [validationName]: true } : {}
67
+ },
68
+
57
69
  computed: {
58
70
  inputType() {
59
71
  return (
@@ -81,20 +93,6 @@ export default DitoTypeComponent.register(
81
93
  this.value = value
82
94
  }
83
95
  }
84
- },
85
-
86
- methods: {
87
- getValidations() {
88
- const rule = {
89
- email: 'email',
90
- url: 'url',
91
- hostname: 'hostname',
92
- domain: 'domain',
93
- password: 'password',
94
- creditcard: 'creditcard'
95
- }[this.type]
96
- return rule ? { [rule]: true } : {}
97
- }
98
96
  }
99
97
  }
100
98
  )
@@ -1,6 +1,6 @@
1
1
  <template lang="pug">
2
2
  textarea.dito-textarea.dito-input(
3
- :id="dataPath"
3
+ :id="componentPath"
4
4
  ref="element"
5
5
  v-model="value"
6
6
  v-bind="attributes"
@@ -18,17 +18,22 @@
18
18
  v-if="editPath && hasEditableForms"
19
19
  )
20
20
  //- Include a router-view for the optional DitoFormInlined
21
- RouterView
21
+ RouterView(
22
+ v-slot="{ Component, route }"
23
+ )
24
+ component(
25
+ v-if="Component"
26
+ :is="Component"
27
+ :key="getRouteComponentKey(route, routeComponent.routeLevel + 1)"
28
+ )
22
29
  </template>
23
30
 
24
31
  <script>
25
32
  import DitoTypeComponent from '../DitoTypeComponent.js'
26
33
  import SourceMixin from '../mixins/SourceMixin.js'
27
- import {
28
- hasFormSchema,
29
- getFormSchemas,
30
- resolveSchemaComponents
31
- } from '../utils/schema.js'
34
+ import { resolveSchemaComponents } from '../utils/schema/setup.js'
35
+ import { hasFormSchema, getFormSchemas } from '../utils/schema/lookup.js'
36
+ import { getParentDataPath } from '../utils/data.js'
32
37
 
33
38
  export default DitoTypeComponent.register(
34
39
  ['tree-list', 'tree-object'],
@@ -66,7 +71,7 @@ export default DitoTypeComponent.register(
66
71
  // Remove `name` from `dataPath`, as it is added
67
72
  // to `treeData` and `treeSchema`
68
73
  return this.isListSource
69
- ? this.dataPath.slice(0, this.dataPath.length - this.name.length)
74
+ ? getParentDataPath(this.dataPath)
70
75
  : this.dataPath
71
76
  },
72
77
 
@@ -138,9 +138,8 @@
138
138
  import DitoTypeComponent from '../DitoTypeComponent.js'
139
139
  import DitoContext from '../DitoContext.js'
140
140
  import SortableMixin from '../mixins/SortableMixin.js'
141
- import parseFileSize from 'filesize-parser'
142
141
  import { getSchemaAccessor } from '../utils/accessor.js'
143
- import { formatFileSize } from '../utils/units.js'
142
+ import { formatFileSize, parseFileSize } from '../utils/units.js'
144
143
  import { appendDataPath } from '../utils/data.js'
145
144
  import { isArray, asArray } from '@ditojs/utils'
146
145
  import VueUpload from 'vue-upload-component'
@@ -0,0 +1,141 @@
1
+ import { computed, shallowRef } from 'vue'
2
+ import {
3
+ isObject,
4
+ isFunction,
5
+ isPromise,
6
+ normalizeDataPath,
7
+ getValueAtDataPath
8
+ } from '@ditojs/utils'
9
+
10
+ // DataSchemaResolver resolves a data schema, `{ data, dataPath }` as used by
11
+ // `schema.options` and by the `computed` types, to its value, lazily: Nothing
12
+ // is evaluated or loaded before `value` or `isLoading` is read.
13
+ //
14
+ // - `data` is the value itself, or a function that returns the value, a
15
+ // promise of it, or a function that loads it. In the last case, `data()`
16
+ // only tracks the dependencies and the returned function loads the value,
17
+ // e.g. `data: ({ item: { id } }) => async () => load(id)`, so that loading
18
+ // only starts again when these dependencies change.
19
+ // - `dataPath` is resolved relative to the data path of the context, in its
20
+ // root data, e.g. `'../fontFamily/fontShopSets'`.
21
+ // - Data schemas that aren't objects are values, e.g. `options: ['A', 'B']`.
22
+ //
23
+ // While a value is loading, `value` is `undefined` and `isLoading` is `true`.
24
+ // If loading fails, the error is logged and kept in `lastLoadError`. Results
25
+ // of outdated loads are discarded. Resolved values are returned as they are, so
26
+ // that their object identity is kept.
27
+ //
28
+ // `createContext()` returns the `DitoContext` that `data()` and the loading
29
+ // functions are called with. `onLoadStart(promise)` is called with the promise
30
+ // of each load, which resolves once the load finished, see `finishLoad()`.
31
+
32
+ export class DataSchemaResolver {
33
+ // The promise or loading function of the current load, see `startLoad()`:
34
+ currentPromiseOrLoader = null
35
+ // The promise or loading function of the last finished load, with its value
36
+ // or error, see `finishLoad()`:
37
+ lastFinishedLoad = shallowRef({
38
+ promiseOrLoader: null,
39
+ value: undefined,
40
+ error: null
41
+ })
42
+
43
+ constructor(dataSchema, { createContext, onLoadStart = null }) {
44
+ this.dataSchema = isObject(dataSchema) ? dataSchema : { data: dataSchema }
45
+ this.createContext = createContext
46
+ this.onLoadStart = onLoadStart
47
+ this.valueOrPromiseOrLoader = computed(() =>
48
+ this.getValueOrPromiseOrLoader()
49
+ )
50
+ this.valueOrUndefinedWhileLoading = computed(() =>
51
+ this.getValueOrStartLoad()
52
+ )
53
+ }
54
+
55
+ get value() {
56
+ return this.valueOrUndefinedWhileLoading.value
57
+ }
58
+
59
+ get isLoading() {
60
+ // Read `value` first, so that its loading starts:
61
+ if (this.value !== undefined) {
62
+ return false
63
+ }
64
+ const valueOrPromiseOrLoader = this.valueOrPromiseOrLoader.value
65
+ return (
66
+ isPromiseOrLoader(valueOrPromiseOrLoader) &&
67
+ this.lastFinishedLoad.value.promiseOrLoader !== valueOrPromiseOrLoader
68
+ )
69
+ }
70
+
71
+ // The error of the last load, if it failed and isn't outdated:
72
+ get lastLoadError() {
73
+ const { promiseOrLoader, error } = this.lastFinishedLoad.value
74
+ return promiseOrLoader === this.valueOrPromiseOrLoader.value ? error : null
75
+ }
76
+
77
+ // Returns the value of the data schema, a promise of it, or a function that
78
+ // loads it. Called by a computed property, so that it only runs again when
79
+ // its dependencies change.
80
+ getValueOrPromiseOrLoader() {
81
+ const { data, dataPath } = this.dataSchema
82
+ if (data !== undefined) {
83
+ return isFunction(data) ? data(this.createContext()) : data
84
+ } else if (dataPath) {
85
+ const context = this.createContext()
86
+ // Data that isn't there, e.g. after it was reset by code, has no value,
87
+ // the same as a missing value at an existing data path.
88
+ return getValueAtDataPath(
89
+ context.rootItem,
90
+ normalizeDataPath(`${context.dataPath}/${dataPath}`),
91
+ () => undefined
92
+ )
93
+ }
94
+ }
95
+
96
+ // Returns the value of the data schema, or its loaded value once the load
97
+ // of its promise or loading function finished. Until then, it starts the
98
+ // load if it isn't started yet, and returns `undefined`. Called by a
99
+ // computed property, see `value`.
100
+ getValueOrStartLoad() {
101
+ const valueOrPromiseOrLoader = this.valueOrPromiseOrLoader.value
102
+ if (!isPromiseOrLoader(valueOrPromiseOrLoader)) {
103
+ return valueOrPromiseOrLoader
104
+ }
105
+ const { promiseOrLoader, value } = this.lastFinishedLoad.value
106
+ if (promiseOrLoader === valueOrPromiseOrLoader) {
107
+ return value
108
+ }
109
+ if (this.currentPromiseOrLoader !== valueOrPromiseOrLoader) {
110
+ this.startLoad(valueOrPromiseOrLoader)
111
+ }
112
+ return undefined
113
+ }
114
+
115
+ startLoad(promiseOrLoader) {
116
+ this.currentPromiseOrLoader = promiseOrLoader
117
+ const context = this.createContext()
118
+ // Keeps the result in `lastFinishedLoad`, unless the load is outdated:
119
+ const finishLoad = (value, error = null) => {
120
+ if (this.currentPromiseOrLoader === promiseOrLoader) {
121
+ this.lastFinishedLoad.value = { promiseOrLoader, value, error }
122
+ }
123
+ }
124
+ // Call loading functions in a microtask, outside of the computed property
125
+ // that started the load, so that they don't add to its dependencies.
126
+ const promise = Promise.resolve()
127
+ .then(() =>
128
+ isFunction(promiseOrLoader) ? promiseOrLoader(context) : promiseOrLoader
129
+ )
130
+ .then(value => finishLoad(value))
131
+ .catch(error => {
132
+ console.error(error)
133
+ finishLoad(undefined, error)
134
+ })
135
+ this.onLoadStart?.(promise)
136
+ }
137
+ }
138
+
139
+ function isPromiseOrLoader(valueOrPromiseOrLoader) {
140
+ return isFunction(valueOrPromiseOrLoader) || isPromise(valueOrPromiseOrLoader)
141
+ }
@@ -0,0 +1,132 @@
1
+ import { vi } from 'vitest'
2
+ import { reactive, nextTick } from 'vue'
3
+ import { DataSchemaResolver } from './DataSchemaResolver.js'
4
+
5
+ function createResolver(dataSchema, context = {}) {
6
+ const pendingLoads = []
7
+ const resolver = new DataSchemaResolver(dataSchema, {
8
+ createContext: () => context,
9
+ onLoadStart: promise => pendingLoads.push(promise)
10
+ })
11
+ const waitForPendingLoads = () => Promise.all(pendingLoads)
12
+ return { resolver, pendingLoads, waitForPendingLoads }
13
+ }
14
+
15
+ describe('DataSchemaResolver', () => {
16
+ it('resolves data schemas that are values', () => {
17
+ const options = ['Small', 'Large']
18
+ const { resolver } = createResolver(options)
19
+ expect(resolver.value).toBe(options)
20
+ expect(resolver.isLoading).toBe(false)
21
+ })
22
+
23
+ it('resolves `data` values and functions', () => {
24
+ const item = reactive({ title: 'Hello' })
25
+ const { resolver } = createResolver(
26
+ { data: ({ item }) => item.title.toUpperCase() },
27
+ { item }
28
+ )
29
+ expect(resolver.value).toBe('HELLO')
30
+ item.title = 'Bye'
31
+ expect(resolver.value).toBe('BYE')
32
+ expect(createResolver({ data: 'Static' }).resolver.value).toBe('Static')
33
+ })
34
+
35
+ it('resolves `dataPath` relative to the data path of the context', () => {
36
+ const rootItem = reactive({ sizes: ['S', 'M'], item: {} })
37
+ const { resolver } = createResolver(
38
+ { dataPath: '../../sizes' },
39
+ { rootItem, dataPath: 'item/size' }
40
+ )
41
+ expect(resolver.value).toBe(rootItem.sizes)
42
+ })
43
+
44
+ it('evaluates and loads nothing before it is read', async () => {
45
+ const data = vi.fn(() => async () => ['A'])
46
+ const { resolver, pendingLoads } = createResolver({ data })
47
+ await nextTick()
48
+ expect(data).not.toHaveBeenCalled()
49
+ expect(pendingLoads).toHaveLength(0)
50
+ expect(resolver.value).toBe(undefined)
51
+ expect(data).toHaveBeenCalledTimes(1)
52
+ expect(pendingLoads).toHaveLength(1)
53
+ })
54
+
55
+ it('loads values asynchronously, keeping their object identity', async () => {
56
+ const options = [{ id: 1 }]
57
+ const { resolver, waitForPendingLoads } = createResolver({
58
+ data: async () => options
59
+ })
60
+ expect(resolver.value).toBe(undefined)
61
+ expect(resolver.isLoading).toBe(true)
62
+ await waitForPendingLoads()
63
+ expect(resolver.value).toBe(options)
64
+ expect(resolver.isLoading).toBe(false)
65
+ })
66
+
67
+ it('loads again only when the dependencies of `data()` change', async () => {
68
+ const item = reactive({ category: 'news', title: 'Hello' })
69
+ const load = vi.fn(async category => [`${category}-1`])
70
+ const { resolver, waitForPendingLoads } = createResolver(
71
+ {
72
+ data: ({ item }) => {
73
+ const { category } = item
74
+ return async () => load(category)
75
+ }
76
+ },
77
+ { item }
78
+ )
79
+ resolver.value
80
+ await waitForPendingLoads()
81
+ expect(resolver.value).toEqual(['news-1'])
82
+ item.title = 'Changed'
83
+ expect(resolver.value).toEqual(['news-1'])
84
+ item.category = 'sports'
85
+ expect(resolver.value).toBe(undefined)
86
+ await waitForPendingLoads()
87
+ expect(resolver.value).toEqual(['sports-1'])
88
+ expect(load).toHaveBeenCalledTimes(2)
89
+ })
90
+
91
+ it('discards the values of outdated loads', async () => {
92
+ const item = reactive({ category: 'news' })
93
+ const resolvers = {}
94
+ const { resolver, waitForPendingLoads } = createResolver(
95
+ {
96
+ data: ({ item }) => {
97
+ const { category } = item
98
+ return () =>
99
+ new Promise(resolve => {
100
+ resolvers[category] = () => resolve(category)
101
+ })
102
+ }
103
+ },
104
+ { item }
105
+ )
106
+ resolver.value
107
+ item.category = 'sports'
108
+ resolver.value
109
+ await nextTick()
110
+ resolvers.sports()
111
+ resolvers.news()
112
+ await waitForPendingLoads()
113
+ expect(resolver.value).toBe('sports')
114
+ })
115
+
116
+ it('keeps the error of the last load in `lastLoadError`', async () => {
117
+ vi.spyOn(console, 'error').mockImplementation(() => {})
118
+ const error = new Error('Failed')
119
+ const { resolver, waitForPendingLoads } = createResolver({
120
+ data: async () => {
121
+ throw error
122
+ }
123
+ })
124
+ resolver.value
125
+ await waitForPendingLoads()
126
+ expect(resolver.value).toBe(undefined)
127
+ expect(resolver.isLoading).toBe(false)
128
+ expect(resolver.lastLoadError).toBe(error)
129
+ expect(console.error).toHaveBeenCalledWith(error)
130
+ vi.restoreAllMocks()
131
+ })
132
+ })