@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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +3421 -2662
- package/dist/dito-admin.umd.js +6 -6
- package/package.json +81 -45
- package/src/DitoComponent.js +1 -1
- package/src/DitoContext.js +51 -0
- package/src/DitoTypeComponent.js +6 -1
- package/src/components/DitoAffixes.vue +6 -3
- package/src/components/DitoContainer.vue +19 -10
- package/src/components/DitoCreateButton.vue +11 -2
- package/src/components/DitoDialog.vue +21 -2
- package/src/components/DitoForm.vue +156 -83
- package/src/components/DitoLabel.vue +3 -2
- package/src/components/DitoPane.vue +55 -11
- package/src/components/DitoPanel.vue +33 -5
- package/src/components/DitoPanels.vue +2 -1
- package/src/components/DitoRoot.vue +13 -6
- package/src/components/DitoSchema.vue +278 -145
- package/src/components/DitoSchemaInlined.vue +1 -1
- package/src/components/DitoTabs.vue +16 -18
- package/src/components/DitoTrail.vue +5 -4
- package/src/components/DitoTreeItem.vue +17 -6
- package/src/components/DitoView.vue +25 -2
- package/src/mixins/DitoMixin.js +42 -7
- package/src/mixins/ItemMixin.js +3 -6
- package/src/mixins/NumberMixin.js +47 -31
- package/src/mixins/OptionsMixin.js +122 -71
- package/src/mixins/ResourceMixin.js +5 -2
- package/src/mixins/RouteMixin.js +39 -1
- package/src/mixins/SortableMixin.js +0 -13
- package/src/mixins/SourceMixin.js +142 -95
- package/src/mixins/TextMixin.js +1 -1
- package/src/mixins/TypeMixin.js +37 -22
- package/src/mixins/ValidationMixin.js +9 -27
- package/src/mixins/ValidatorMixin.js +5 -7
- package/src/mixins/ValueMixin.js +5 -2
- package/src/types/DitoTypeButton.vue +1 -1
- package/src/types/DitoTypeCheckbox.vue +1 -1
- package/src/types/DitoTypeCheckboxes.vue +1 -1
- package/src/types/DitoTypeCode.vue +8 -1
- package/src/types/DitoTypeColor.vue +20 -34
- package/src/types/DitoTypeComponent.vue +1 -1
- package/src/types/DitoTypeComputed.vue +4 -27
- package/src/types/DitoTypeDate.vue +16 -5
- package/src/types/DitoTypeLabel.vue +1 -4
- package/src/types/DitoTypeList.vue +9 -5
- package/src/types/DitoTypeMarkup.vue +25 -13
- package/src/types/DitoTypeMultiselect.vue +26 -6
- package/src/types/DitoTypeNumber.vue +1 -1
- package/src/types/DitoTypeObject.vue +10 -7
- package/src/types/DitoTypePanel.vue +19 -6
- package/src/types/DitoTypeProgress.vue +1 -1
- package/src/types/DitoTypeRadio.vue +1 -1
- package/src/types/DitoTypeSection.vue +32 -4
- package/src/types/DitoTypeSelect.vue +1 -1
- package/src/types/DitoTypeSlider.vue +1 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +13 -15
- package/src/types/DitoTypeTextarea.vue +1 -1
- package/src/types/DitoTypeTreeList.vue +12 -7
- package/src/types/DitoTypeUpload.vue +1 -2
- package/src/utils/DataSchemaResolver.js +141 -0
- package/src/utils/DataSchemaResolver.test.js +132 -0
- package/src/utils/FormModel.js +556 -0
- package/src/utils/FormModel.test.js +808 -0
- package/src/utils/data.js +20 -0
- package/src/utils/data.test.js +28 -0
- package/src/utils/filter.js +3 -2
- package/src/utils/options.js +3 -0
- package/src/utils/schema/data.js +617 -0
- package/src/utils/schema/data.test.js +471 -0
- package/src/utils/schema/lookup.js +266 -0
- package/src/utils/schema/lookup.test.js +61 -0
- package/src/utils/schema/setup.js +354 -0
- package/src/utils/schema/structure.js +172 -0
- package/src/utils/schema/structure.test.js +57 -0
- package/src/utils/schema/types.js +34 -0
- package/src/utils/schema/validation.js +142 -0
- package/src/utils/schema/validation.test.js +395 -0
- package/src/utils/units.js +70 -3
- package/src/utils/units.test.js +71 -0
- package/types/index.d.ts +149 -59
- package/src/mixins/DataMixin.js +0 -131
- 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
|
+
}
|