@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
@@ -0,0 +1,172 @@
1
+ import { isObject, isString } from '@ditojs/utils'
2
+ import { getTypeOptions, getSourceType } from './types.js'
3
+
4
+ export function iterateSchemaComponents(schemas, callback) {
5
+ for (const schema of schemas) {
6
+ if (isSingleComponentView(schema)) {
7
+ const res = callback(schema.component, schema.name, 0)
8
+ if (res !== undefined) {
9
+ return res
10
+ }
11
+ } else if (isSchema(schema)) {
12
+ for (const [name, component] of Object.entries(schema.components || {})) {
13
+ const res = callback(component, name, 1)
14
+ if (res !== undefined) {
15
+ return res
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ export function iterateNestedSchemaComponents(schema, callback) {
23
+ return schema
24
+ ? iterateSchemaComponents([schema, ...getTabSchemas(schema)], callback)
25
+ : undefined
26
+ }
27
+
28
+ export function findNestedSchemaComponent(schema, callback) {
29
+ return (
30
+ iterateNestedSchemaComponents(
31
+ schema,
32
+ component => (callback(component) ? component : undefined)
33
+ ) ?? null
34
+ )
35
+ }
36
+
37
+ export function someNestedSchemaComponent(schema, callback) {
38
+ return (
39
+ iterateNestedSchemaComponents(
40
+ schema,
41
+ (component, name) => (callback(component, name) ? true : undefined)
42
+ ) ?? false
43
+ )
44
+ }
45
+
46
+ export function everyNestedSchemaComponent(schema, callback) {
47
+ return (
48
+ iterateNestedSchemaComponents(
49
+ schema,
50
+ component => (callback(component) ? undefined : false)
51
+ ) ?? true
52
+ )
53
+ }
54
+
55
+ export function hasNestedSchemaComponents(schema) {
56
+ return someNestedSchemaComponent(schema, () => true) ?? false
57
+ }
58
+
59
+ /**
60
+ * Returns whether `schema` has a component named `name` in its components or
61
+ * tabs, or in those of its unnested components, e.g. sections without
62
+ * `nested: true`, which share the data of `schema` without adding their own
63
+ * name to the data path.
64
+ */
65
+ export function hasComponentNamed(schema, name) {
66
+ const found = iterateNestedSchemaComponents(
67
+ schema,
68
+ (component, componentName) => {
69
+ if (
70
+ isNested(component)
71
+ ? componentName === name
72
+ : hasComponentNamed(component, name)
73
+ ) {
74
+ return true
75
+ }
76
+ }
77
+ )
78
+ return !!found
79
+ }
80
+
81
+ export function isSchema(schema) {
82
+ return isObject(schema) && isString(schema.type)
83
+ }
84
+
85
+ export function isForm(schema) {
86
+ return isSchema(schema) && schema.type === 'form'
87
+ }
88
+
89
+ export function isView(schema) {
90
+ return isSchema(schema) && schema.type === 'view'
91
+ }
92
+
93
+ export function isTab(schema) {
94
+ return isSchema(schema) && schema.type === 'tab'
95
+ }
96
+
97
+ export function isPanel(schema) {
98
+ return isSchema(schema) && schema.type === 'panel'
99
+ }
100
+
101
+ // Returns whether `schema` is a panel that provides its own data through
102
+ // `schema.data`, e.g. a filters panel, instead of sharing the data of its
103
+ // schema, see `DitoPanel`.
104
+ export function isPanelWithOwnData(schema) {
105
+ return isPanel(schema) && !!schema.data
106
+ }
107
+
108
+ export function isMenu(schema) {
109
+ return isSchema(schema) && schema.type === 'menu'
110
+ }
111
+
112
+ export function getSchemaIdentifier(schema) {
113
+ return JSON.stringify(schema)
114
+ }
115
+
116
+ export function isSingleComponentView(schema) {
117
+ return (
118
+ isView(schema) &&
119
+ isObject(schema.component)
120
+ )
121
+ }
122
+
123
+ export function isCompact(schema) {
124
+ return !!schema.compact
125
+ }
126
+
127
+ export function isInlined(schema) {
128
+ return !!(schema.inlined || schema.components)
129
+ }
130
+
131
+ export function isNested(schema) {
132
+ return !!(schema.nested || getTypeOptions(schema)?.defaultNested === true)
133
+ }
134
+
135
+ export function hasLabel(schema, generateLabels) {
136
+ const { label } = schema
137
+ return (
138
+ label !== false && (
139
+ !!label ||
140
+ generateLabels && getTypeOptions(schema)?.generateLabel
141
+ )
142
+ )
143
+ }
144
+
145
+ export function omitSpacing(schema) {
146
+ return !!getTypeOptions(schema)?.omitSpacing
147
+ }
148
+
149
+ export function getTabSchemas(schema) {
150
+ return schema?.tabs ? Object.values(schema.tabs) : []
151
+ }
152
+
153
+ export function getPanelSchemas(schema) {
154
+ return schema?.panels ? Object.values(schema.panels) : []
155
+ }
156
+
157
+ export function isObjectSource(schemaOrType) {
158
+ return getSourceType(schemaOrType) === 'object'
159
+ }
160
+
161
+ export function isListSource(schemaOrType) {
162
+ return getSourceType(schemaOrType) === 'list'
163
+ }
164
+
165
+ /**
166
+ * Returns whether `schema` is a source with its own resource, whose items are
167
+ * loaded, validated and submitted through their own forms, separately from
168
+ * the data that contains them.
169
+ */
170
+ export function isSourceWithResource(schema) {
171
+ return !!getSourceType(schema) && !!schema.resource
172
+ }
@@ -0,0 +1,57 @@
1
+ import { registerTypeComponent } from './types.js'
2
+ import { hasComponentNamed } from './structure.js'
3
+
4
+ // Register minimal type options, as the actual type components can't be
5
+ // imported without a Vue SFC compiler:
6
+ registerTypeComponent('text', { defaultNested: true })
7
+ registerTypeComponent('section', { defaultNested: false })
8
+
9
+ describe('hasComponentNamed()', () => {
10
+ const schema = {
11
+ type: 'form',
12
+ components: {
13
+ title: { type: 'text' },
14
+ unnested: {
15
+ type: 'section',
16
+ components: {
17
+ note: { type: 'text' },
18
+ inner: {
19
+ type: 'section',
20
+ components: { remark: { type: 'text' } }
21
+ }
22
+ }
23
+ },
24
+ nested: {
25
+ type: 'section',
26
+ nested: true,
27
+ components: { subtitle: { type: 'text' } }
28
+ }
29
+ },
30
+ tabs: {
31
+ tab: {
32
+ type: 'tab',
33
+ components: { edition: { type: 'text' } }
34
+ }
35
+ }
36
+ }
37
+
38
+ it('finds components and components in tabs', () => {
39
+ expect(hasComponentNamed(schema, 'title')).toBe(true)
40
+ expect(hasComponentNamed(schema, 'edition')).toBe(true)
41
+ })
42
+
43
+ it('finds components of unnested components', () => {
44
+ expect(hasComponentNamed(schema, 'note')).toBe(true)
45
+ expect(hasComponentNamed(schema, 'remark')).toBe(true)
46
+ })
47
+
48
+ it('finds nested components, but not their components', () => {
49
+ expect(hasComponentNamed(schema, 'nested')).toBe(true)
50
+ expect(hasComponentNamed(schema, 'subtitle')).toBe(false)
51
+ })
52
+
53
+ it('ignores the names of unnested components', () => {
54
+ expect(hasComponentNamed(schema, 'unnested')).toBe(false)
55
+ expect(hasComponentNamed(schema, 'unknown')).toBe(false)
56
+ })
57
+ })
@@ -0,0 +1,34 @@
1
+ import { isObject } from '@ditojs/utils'
2
+
3
+ const typeComponents = {}
4
+
5
+ const unknownTypeReported = {}
6
+
7
+ export function registerTypeComponent(type, component) {
8
+ typeComponents[type] = component
9
+ }
10
+
11
+ export function getTypeComponent(type, allowNull = false) {
12
+ const component = typeComponents[type] || null
13
+ if (!component && !allowNull && !unknownTypeReported[type]) {
14
+ // Report each missing type only once, to avoid flooding the console:
15
+ unknownTypeReported[type] = true
16
+ throw new Error(`Unknown Dito.js component type: '${type}'`)
17
+ }
18
+ return component
19
+ }
20
+
21
+ function getType(schemaOrType) {
22
+ return isObject(schemaOrType) ? schemaOrType.type : schemaOrType
23
+ }
24
+
25
+ export function getTypeOptions(schemaOrType) {
26
+ return getTypeComponent(getType(schemaOrType), true) ?? null
27
+ }
28
+
29
+ export function getSourceType(schemaOrType) {
30
+ return (
31
+ getTypeOptions(schemaOrType)?.getSourceType?.(getType(schemaOrType)) ??
32
+ null
33
+ )
34
+ }
@@ -0,0 +1,142 @@
1
+ import DitoContext from '../../DitoContext.js'
2
+ import * as validators from '../../validators/index.js'
3
+ import { isFunction, isRegExp, asArray, normalizeDataPath } from '@ditojs/utils'
4
+ import { getTypeOptions } from './types.js'
5
+ import { isSourceWithResource } from './structure.js'
6
+ import {
7
+ getSchemaValue,
8
+ shouldRenderSchema,
9
+ processSchemaData
10
+ } from './data.js'
11
+
12
+ /**
13
+ * Returns the validations of a component schema, without requiring a
14
+ * component: the validations of the type's static `getTypeValidations()`
15
+ * option, `required`, and finally the `schema.rules` overrides, where
16
+ * `undefined` removes a validation. Schema values are evaluated with `context`.
17
+ */
18
+ export function getValidations(schema, context) {
19
+ const validations = {
20
+ ...getTypeOptions(schema)?.getTypeValidations?.(schema, context)
21
+ }
22
+ if (getSchemaValue('required', { type: Boolean, schema, context })) {
23
+ validations.required = true
24
+ }
25
+ // Allow schema to override default rules and add any new ones:
26
+ for (const [name, setting] of Object.entries(schema.rules || {})) {
27
+ if (setting === undefined) {
28
+ delete validations[name]
29
+ } else {
30
+ validations[name] = setting
31
+ }
32
+ }
33
+ return validations
34
+ }
35
+
36
+ /**
37
+ * Returns the errors of validating `value` against `validations`, in the
38
+ * format of server errors, `[{ message }]`, with messages without the label
39
+ * prefix, or an empty array if the value is valid.
40
+ */
41
+ export function getValueValidationErrors(value, validations) {
42
+ const errors = []
43
+ for (const [name, setting] of Object.entries(validations)) {
44
+ // eslint-disable-next-line import/namespace
45
+ const validator = validators[name]
46
+ if (
47
+ validator &&
48
+ // Only apply 'required' validator to empty values.
49
+ // Apply all other validators only to non-empty values.
50
+ (name === 'required' || value != null && value !== '')
51
+ ) {
52
+ const { validate, message } = validator
53
+ if (!validate(value, setting, validations)) {
54
+ errors.push({
55
+ message: isFunction(message)
56
+ ? message(value, setting, validations)
57
+ : message
58
+ })
59
+ }
60
+ }
61
+ }
62
+ return errors
63
+ }
64
+
65
+ /**
66
+ * Validates `data` along the components of `schema`, including tabs, panels
67
+ * and the nested forms of list and object items, skipping components whose
68
+ * `if` evaluates to `false` and sources with their own resource. Returns the
69
+ * errors in the format of server errors, `{ [dataPath]: [{ message }] }`, or
70
+ * `null` if the data is valid.
71
+ */
72
+ export function getDataValidationErrors(schema, data, {
73
+ dataPath = '',
74
+ component = null,
75
+ rootData = data
76
+ } = {}) {
77
+ let errors = null
78
+
79
+ const createContext = ({ schema, data, name, dataPath }) =>
80
+ new DitoContext(component, {
81
+ schema,
82
+ value: name != null ? data[name] : undefined,
83
+ name,
84
+ data,
85
+ dataPath,
86
+ rootData
87
+ })
88
+
89
+ const shouldProcess = entry => (
90
+ !isSourceWithResource(entry.schema) &&
91
+ shouldRenderSchema(entry.schema, createContext(entry))
92
+ )
93
+
94
+ const before = entry => {
95
+ const { schema, data, name, dataPath } = entry
96
+ const context = createContext(entry)
97
+ let value = data[name]
98
+ // Validate the value as displayed, like the components do, see
99
+ // `ValueMixin.value`:
100
+ const { format } = schema
101
+ if (isFunction(format)) {
102
+ value = format(context)
103
+ }
104
+ const validations = getValidations(schema, context)
105
+ const valueErrors = getValueValidationErrors(value, validations)
106
+ if (valueErrors.length > 0) {
107
+ errors ||= {}
108
+ errors[dataPath] = valueErrors
109
+ }
110
+ }
111
+
112
+ processSchemaData(schema, data, {
113
+ dataPath,
114
+ shouldProcess,
115
+ before,
116
+ options: { component, rootData }
117
+ })
118
+ return errors
119
+ }
120
+
121
+ /**
122
+ * Returns the entries of `errors` whose data paths match `match`, or `null` if
123
+ * none match. `match` can be a function receiving the data path, a regular
124
+ * expression, a data path, or an array of data paths.
125
+ */
126
+ export function getMatchingValidationErrors(errors, match) {
127
+ let isMatchingDataPath
128
+ if (isFunction(match)) {
129
+ isMatchingDataPath = match
130
+ } else if (isRegExp(match)) {
131
+ isMatchingDataPath = dataPath => match.test(dataPath)
132
+ } else {
133
+ const dataPaths = asArray(match).map(normalizeDataPath)
134
+ isMatchingDataPath = dataPath => dataPaths.includes(dataPath)
135
+ }
136
+ const matchingEntries = Object.entries(errors || {}).filter(
137
+ ([dataPath]) => isMatchingDataPath(dataPath)
138
+ )
139
+ return matchingEntries.length > 0
140
+ ? Object.fromEntries(matchingEntries)
141
+ : null
142
+ }