@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.
- package/dist/dito-admin.css +1 -1
- package/dist/dito-admin.es.js +6012 -5827
- package/dist/dito-admin.umd.js +3 -7
- package/package.json +18 -17
- package/src/DitoAdmin.js +44 -24
- package/src/DitoContext.js +28 -2
- package/src/DitoTypeComponent.js +1 -0
- package/src/appState.js +6 -3
- package/src/components/DitoAccount.vue +35 -27
- package/src/components/DitoAffixes.vue +9 -2
- package/src/components/DitoClipboard.vue +59 -93
- package/src/components/DitoConfirmMessage.vue +18 -0
- package/src/components/DitoContainer.vue +56 -18
- package/src/components/DitoCreateButton.vue +36 -84
- package/src/components/DitoDialog.vue +20 -15
- package/src/components/DitoDragHandle.vue +44 -0
- package/src/components/DitoDraggable.vue +43 -16
- package/src/components/DitoEditButtons.vue +48 -24
- package/src/components/DitoErrors.vue +9 -56
- package/src/components/DitoForm.vue +82 -42
- package/src/components/DitoInputAffixes.vue +50 -0
- package/src/components/DitoLabel.vue +1 -15
- package/src/components/DitoMenu.vue +45 -52
- package/src/components/DitoNotifications.vue +38 -14
- package/src/components/DitoOptionList.vue +81 -0
- package/src/components/DitoOptionsEditButtons.vue +57 -0
- package/src/components/DitoPagination.vue +1 -8
- package/src/components/DitoPane.vue +51 -39
- package/src/components/DitoPanel.vue +12 -26
- package/src/components/DitoPanels.vue +11 -5
- package/src/components/DitoResizeHandle.vue +47 -0
- package/src/components/DitoRoot.vue +113 -320
- package/src/components/DitoSchema.vue +72 -63
- package/src/components/DitoSchemaInlined.vue +8 -9
- package/src/components/DitoScopes.vue +14 -18
- package/src/components/DitoSidebar.vue +4 -29
- package/src/components/DitoSourceItem.vue +114 -0
- package/src/components/DitoTableCell.vue +4 -0
- package/src/components/DitoTableHead.vue +15 -17
- package/src/components/DitoTabs.vue +25 -0
- package/src/components/DitoTrail.vue +25 -25
- package/src/components/DitoTreeItem.vue +102 -141
- package/src/components/DitoUploadFile.vue +5 -2
- package/src/components/DitoView.vue +33 -24
- package/src/index.js +4 -1
- package/src/mixins/DitoMixin.js +77 -110
- package/src/mixins/DomMixin.js +12 -14
- package/src/mixins/EmitterMixin.js +3 -2
- package/src/mixins/ItemMixin.js +38 -94
- package/src/mixins/OptionsMixin.js +38 -25
- package/src/mixins/ResourceMixin.js +182 -111
- package/src/mixins/RouteMixin.js +61 -44
- package/src/mixins/SchemaParentMixin.js +4 -1
- package/src/mixins/SourceMixin.js +118 -100
- package/src/mixins/TypeMixin.js +42 -40
- package/src/mixins/ValidationMixin.js +16 -4
- package/src/mixins/ValidatorMixin.js +1 -1
- package/src/styles/_settings.scss +4 -0
- package/src/styles/_tippy.scss +0 -11
- package/src/styles/style.scss +0 -1
- package/src/types/DitoTypeButton.vue +11 -14
- package/src/types/DitoTypeCheckboxes.vue +35 -17
- package/src/types/DitoTypeCode.vue +19 -41
- package/src/types/DitoTypeColor.vue +67 -19
- package/src/types/DitoTypeDate.vue +11 -14
- package/src/types/DitoTypeList.vue +50 -62
- package/src/types/DitoTypeMarkup.vue +41 -54
- package/src/types/DitoTypeMultiselect.vue +82 -52
- package/src/types/DitoTypeNumber.vue +12 -15
- package/src/types/DitoTypeObject.vue +4 -38
- package/src/types/DitoTypeRadio.vue +19 -6
- package/src/types/DitoTypeSection.vue +23 -9
- package/src/types/DitoTypeSelect.vue +39 -23
- package/src/types/DitoTypeSlider.vue +9 -1
- package/src/types/DitoTypeSwitch.vue +1 -1
- package/src/types/DitoTypeText.vue +11 -14
- package/src/types/DitoTypeTreeList.vue +46 -8
- package/src/types/DitoTypeUpload.vue +190 -67
- package/src/utils/DataModel.js +368 -146
- package/src/utils/FileDropTargets.js +97 -0
- package/src/utils/ItemStores.js +31 -0
- package/src/utils/ListQuery.js +115 -0
- package/src/utils/LoadingTracker.js +76 -0
- package/src/utils/SchemaGraph.js +3 -2
- package/src/utils/Session.js +151 -0
- package/src/utils/ViewRegistry.js +85 -0
- package/src/utils/accessor.js +12 -4
- package/src/utils/affixes.js +33 -0
- package/src/utils/changeOnceEdited.js +33 -0
- package/src/utils/clipboard.js +55 -0
- package/src/utils/data.js +10 -5
- package/src/utils/dialogs.js +59 -0
- package/src/utils/dragResize.js +61 -0
- package/src/utils/filter.js +36 -2
- package/src/utils/html.js +9 -0
- package/src/utils/list.js +10 -0
- package/src/utils/options.js +4 -0
- package/src/utils/registration.js +18 -0
- package/src/utils/request.js +108 -13
- package/src/utils/route.js +6 -17
- package/src/utils/schema/data.js +29 -2
- package/src/utils/schema/events.js +19 -0
- package/src/utils/schema/item.js +114 -0
- package/src/utils/schema/lookup.js +39 -14
- package/src/utils/schema/setup.js +5 -5
- package/src/utils/schema/validation.js +16 -2
- package/src/utils/uid.js +105 -8
- package/src/validators/_decimals.js +16 -5
- package/types/index.d.ts +168 -41
- package/src/mixins/LoadingMixin.js +0 -23
- package/src/mixins/PulldownMixin.js +0 -72
- 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
|
|
129
|
-
if (!
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
|
143
|
+
formsByItem.set(item, entry)
|
|
134
144
|
}
|
|
135
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
+
const uidsByItem = new WeakMap()
|
|
4
6
|
|
|
5
|
-
// Generated
|
|
6
|
-
|
|
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
|
|
10
|
-
if (!
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
}
|