@ditojs/admin 2.100.0 → 3.1.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 (81) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3555 -2719
  3. package/dist/dito-admin.umd.js +5 -6
  4. package/package.json +84 -46
  5. package/src/DitoAdmin.js +1 -65
  6. package/src/DitoComponent.js +1 -1
  7. package/src/DitoContext.js +51 -0
  8. package/src/DitoTypeComponent.js +23 -1
  9. package/src/components/DitoAffixes.vue +6 -3
  10. package/src/components/DitoButtons.vue +14 -1
  11. package/src/components/DitoContainer.vue +19 -10
  12. package/src/components/DitoCreateButton.vue +11 -2
  13. package/src/components/DitoDialog.vue +21 -2
  14. package/src/components/DitoForm.vue +152 -84
  15. package/src/components/DitoLabel.vue +3 -2
  16. package/src/components/DitoPane.vue +55 -11
  17. package/src/components/DitoPanel.vue +34 -5
  18. package/src/components/DitoPanels.vue +2 -1
  19. package/src/components/DitoRoot.vue +6 -6
  20. package/src/components/DitoRouterView.vue +27 -0
  21. package/src/components/DitoSchema.vue +278 -145
  22. package/src/components/DitoSchemaInlined.vue +1 -1
  23. package/src/components/DitoTabs.vue +16 -18
  24. package/src/components/DitoTrail.vue +5 -4
  25. package/src/components/DitoTreeItem.vue +17 -6
  26. package/src/components/DitoView.vue +21 -3
  27. package/src/components/index.js +1 -0
  28. package/src/mixins/DitoMixin.js +53 -7
  29. package/src/mixins/ItemMixin.js +3 -6
  30. package/src/mixins/NumberMixin.js +47 -31
  31. package/src/mixins/OptionsMixin.js +132 -71
  32. package/src/mixins/ResourceMixin.js +5 -2
  33. package/src/mixins/RouteMixin.js +39 -1
  34. package/src/mixins/SortableMixin.js +0 -13
  35. package/src/mixins/SourceMixin.js +155 -96
  36. package/src/mixins/TextMixin.js +1 -1
  37. package/src/mixins/TypeMixin.js +37 -22
  38. package/src/mixins/ValidationMixin.js +9 -27
  39. package/src/mixins/ValidatorMixin.js +5 -7
  40. package/src/mixins/ValueMixin.js +5 -2
  41. package/src/types/DitoTypeButton.vue +1 -1
  42. package/src/types/DitoTypeCheckbox.vue +1 -1
  43. package/src/types/DitoTypeCheckboxes.vue +1 -1
  44. package/src/types/DitoTypeCode.vue +8 -1
  45. package/src/types/DitoTypeColor.vue +20 -34
  46. package/src/types/DitoTypeComponent.vue +1 -1
  47. package/src/types/DitoTypeComputed.vue +4 -27
  48. package/src/types/DitoTypeDate.vue +16 -5
  49. package/src/types/DitoTypeLabel.vue +1 -4
  50. package/src/types/DitoTypeList.vue +9 -5
  51. package/src/types/DitoTypeMarkup.vue +25 -13
  52. package/src/types/DitoTypeMultiselect.vue +37 -6
  53. package/src/types/DitoTypeNumber.vue +1 -1
  54. package/src/types/DitoTypeObject.vue +10 -7
  55. package/src/types/DitoTypePanel.vue +20 -6
  56. package/src/types/DitoTypeProgress.vue +1 -1
  57. package/src/types/DitoTypeRadio.vue +1 -1
  58. package/src/types/DitoTypeSection.vue +32 -4
  59. package/src/types/DitoTypeSelect.vue +1 -1
  60. package/src/types/DitoTypeSlider.vue +1 -1
  61. package/src/types/DitoTypeSwitch.vue +1 -1
  62. package/src/types/DitoTypeText.vue +13 -15
  63. package/src/types/DitoTypeTextarea.vue +1 -1
  64. package/src/types/DitoTypeTreeList.vue +5 -7
  65. package/src/types/DitoTypeUpload.vue +1 -2
  66. package/src/utils/DataSchemaResolver.js +154 -0
  67. package/src/utils/FormModel.js +615 -0
  68. package/src/utils/data.js +20 -0
  69. package/src/utils/filter.js +3 -2
  70. package/src/utils/options.js +3 -0
  71. package/src/utils/request.js +70 -0
  72. package/src/utils/schema/data.js +659 -0
  73. package/src/utils/schema/lookup.js +250 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/types.js +34 -0
  77. package/src/utils/schema/validation.js +142 -0
  78. package/src/utils/units.js +70 -3
  79. package/types/index.d.ts +174 -59
  80. package/src/mixins/DataMixin.js +0 -131
  81. 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,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
+ }
@@ -1,5 +1,72 @@
1
- import { filesize } from 'filesize'
1
+ // Unit prefixes in order of their exponent, shared by parsing and formatting.
2
+ const prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
2
3
 
3
- export function formatFileSize(size) {
4
- return filesize(size, { base: 10 })
4
+ /**
5
+ * Formats a number of bytes like `'1.5 MB'`, using base 10 (1 kB = 1000 bytes)
6
+ * unless specified. Base 2 uses IEC units (1 KiB = 1024 bytes).
7
+ */
8
+ export function formatFileSize(size, { base = 10 } = {}) {
9
+ const multiplier = getMultiplier(base)
10
+ const absolute = Math.abs(size)
11
+ let exponent = Math.min(
12
+ absolute >= 1 ? Math.floor(Math.log(absolute) / Math.log(multiplier)) : 0,
13
+ prefixes.length - 1
14
+ )
15
+ let amount = roundFileSize(absolute, multiplier, exponent)
16
+ // Rounding can reach the next unit, e.g. 999999 bytes to 1000 kB = 1 MB.
17
+ if (amount >= multiplier && exponent < prefixes.length - 1) {
18
+ amount = roundFileSize(absolute, multiplier, ++exponent)
19
+ }
20
+ const prefix = prefixes[exponent]
21
+ const unit = !prefix
22
+ ? 'B'
23
+ : base === 10
24
+ ? `${prefix}B`
25
+ : `${prefix.toUpperCase()}iB`
26
+ return `${size < 0 ? '-' : ''}${amount} ${unit}`
27
+ }
28
+
29
+ /**
30
+ * Parses file sizes like `'10 MB'`, `'1.5G'`, `'512kb'` or `'100 bytes'` into
31
+ * a number of bytes, using base 10 (1 kB = 1000 bytes) unless specified.
32
+ * IEC units like `'10 MiB'` always use base 2 (1 KiB = 1024 bytes).
33
+ * Bit units are case-sensitive (`Kb` = kilobit), the byte units aren't.
34
+ */
35
+ export function parseFileSize(input, { base = 10 } = {}) {
36
+ const [, amountString, unit] = (
37
+ String(input).match(/^([\d.,]*)\s*(\D*)$/) || []
38
+ )
39
+ const amount = parseFloat(amountString?.replace(',', '.'))
40
+ if (!isFinite(amount)) {
41
+ throw new Error(`Can't interpret file size: ${input}`)
42
+ }
43
+ const multiplier = getMultiplier(/^[kmgtpezy]ib?$/i.test(unit) ? 2 : base)
44
+ const bitMatch = unit.replace(/^bits?$/, 'b').match(/^([KMGTPEZY]?)b$/)
45
+ if (bitMatch) {
46
+ const exponent = prefixes.indexOf(bitMatch[1].replace('K', 'k'))
47
+ return Math.round(amount * multiplier ** exponent / 8)
48
+ }
49
+ const exponent = prefixes.findIndex(
50
+ prefix => (
51
+ prefix.toLowerCase() ===
52
+ unit
53
+ .toLowerCase()
54
+ .replace(/^(bytes?|b)$/, '')
55
+ .replace(/^(\w)(i?b|i)$/, '$1')
56
+ )
57
+ )
58
+ if (exponent >= 0) {
59
+ return Math.round(amount * multiplier ** exponent)
60
+ }
61
+ throw new Error(`Invalid file size unit: ${unit}`)
62
+ }
63
+
64
+ function getMultiplier(base) {
65
+ return base === 10 ? 1000 : 1024
66
+ }
67
+
68
+ function roundFileSize(size, multiplier, exponent) {
69
+ // Round bytes to integers, larger units to two decimals.
70
+ const factor = exponent > 0 ? 100 : 1
71
+ return Math.round(size / multiplier ** exponent * factor) / factor
5
72
  }