@radio-garden/ditojs-admin 2.85.2-0.5067ad799
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/README.md +180 -0
- package/dist/dito-admin.css +1 -0
- package/dist/dito-admin.es.js +12106 -0
- package/dist/dito-admin.umd.js +7 -0
- package/package.json +96 -0
- package/src/DitoAdmin.js +293 -0
- package/src/DitoComponent.js +34 -0
- package/src/DitoContext.js +318 -0
- package/src/DitoTypeComponent.js +42 -0
- package/src/DitoUser.js +12 -0
- package/src/appState.js +12 -0
- package/src/components/DitoAccount.vue +60 -0
- package/src/components/DitoAffix.vue +68 -0
- package/src/components/DitoAffixes.vue +200 -0
- package/src/components/DitoButtons.vue +80 -0
- package/src/components/DitoClipboard.vue +186 -0
- package/src/components/DitoContainer.vue +374 -0
- package/src/components/DitoCreateButton.vue +146 -0
- package/src/components/DitoDialog.vue +242 -0
- package/src/components/DitoDraggable.vue +117 -0
- package/src/components/DitoEditButtons.vue +135 -0
- package/src/components/DitoErrors.vue +83 -0
- package/src/components/DitoForm.vue +521 -0
- package/src/components/DitoFormInner.vue +26 -0
- package/src/components/DitoFormNested.vue +17 -0
- package/src/components/DitoHeader.vue +84 -0
- package/src/components/DitoLabel.vue +200 -0
- package/src/components/DitoMenu.vue +186 -0
- package/src/components/DitoNavigation.vue +40 -0
- package/src/components/DitoNotifications.vue +170 -0
- package/src/components/DitoPagination.vue +42 -0
- package/src/components/DitoPane.vue +334 -0
- package/src/components/DitoPanel.vue +256 -0
- package/src/components/DitoPanels.vue +61 -0
- package/src/components/DitoRoot.vue +524 -0
- package/src/components/DitoSchema.vue +846 -0
- package/src/components/DitoSchemaInlined.vue +97 -0
- package/src/components/DitoScopes.vue +76 -0
- package/src/components/DitoSidebar.vue +50 -0
- package/src/components/DitoSpinner.vue +95 -0
- package/src/components/DitoTableCell.vue +64 -0
- package/src/components/DitoTableHead.vue +121 -0
- package/src/components/DitoTabs.vue +103 -0
- package/src/components/DitoTrail.vue +124 -0
- package/src/components/DitoTreeItem.vue +420 -0
- package/src/components/DitoUploadFile.vue +199 -0
- package/src/components/DitoVNode.vue +14 -0
- package/src/components/DitoView.vue +143 -0
- package/src/components/index.js +42 -0
- package/src/directives/resize.js +83 -0
- package/src/index.js +1 -0
- package/src/mixins/ContextMixin.js +68 -0
- package/src/mixins/DataMixin.js +131 -0
- package/src/mixins/DitoMixin.js +591 -0
- package/src/mixins/DomMixin.js +29 -0
- package/src/mixins/EmitterMixin.js +158 -0
- package/src/mixins/ItemMixin.js +144 -0
- package/src/mixins/LoadingMixin.js +23 -0
- package/src/mixins/NumberMixin.js +118 -0
- package/src/mixins/OptionsMixin.js +304 -0
- package/src/mixins/PulldownMixin.js +63 -0
- package/src/mixins/ResourceMixin.js +398 -0
- package/src/mixins/RouteMixin.js +190 -0
- package/src/mixins/SchemaParentMixin.js +33 -0
- package/src/mixins/SortableMixin.js +49 -0
- package/src/mixins/SourceMixin.js +734 -0
- package/src/mixins/TextMixin.js +26 -0
- package/src/mixins/TypeMixin.js +280 -0
- package/src/mixins/ValidationMixin.js +119 -0
- package/src/mixins/ValidatorMixin.js +57 -0
- package/src/mixins/ValueMixin.js +31 -0
- package/src/styles/_base.scss +17 -0
- package/src/styles/_button.scss +191 -0
- package/src/styles/_imports.scss +3 -0
- package/src/styles/_info.scss +19 -0
- package/src/styles/_layout.scss +19 -0
- package/src/styles/_pulldown.scss +38 -0
- package/src/styles/_scroll.scss +13 -0
- package/src/styles/_settings.scss +88 -0
- package/src/styles/_table.scss +223 -0
- package/src/styles/_tippy.scss +45 -0
- package/src/styles/style.scss +9 -0
- package/src/types/DitoTypeButton.vue +143 -0
- package/src/types/DitoTypeCheckbox.vue +27 -0
- package/src/types/DitoTypeCheckboxes.vue +65 -0
- package/src/types/DitoTypeCode.vue +199 -0
- package/src/types/DitoTypeColor.vue +272 -0
- package/src/types/DitoTypeComponent.vue +31 -0
- package/src/types/DitoTypeComputed.vue +50 -0
- package/src/types/DitoTypeDate.vue +99 -0
- package/src/types/DitoTypeLabel.vue +23 -0
- package/src/types/DitoTypeList.vue +364 -0
- package/src/types/DitoTypeMarkup.vue +700 -0
- package/src/types/DitoTypeMultiselect.vue +522 -0
- package/src/types/DitoTypeNumber.vue +66 -0
- package/src/types/DitoTypeObject.vue +136 -0
- package/src/types/DitoTypePanel.vue +18 -0
- package/src/types/DitoTypeProgress.vue +40 -0
- package/src/types/DitoTypeRadio.vue +45 -0
- package/src/types/DitoTypeSection.vue +80 -0
- package/src/types/DitoTypeSelect.vue +133 -0
- package/src/types/DitoTypeSlider.vue +66 -0
- package/src/types/DitoTypeSpacer.vue +11 -0
- package/src/types/DitoTypeSwitch.vue +40 -0
- package/src/types/DitoTypeText.vue +101 -0
- package/src/types/DitoTypeTextarea.vue +48 -0
- package/src/types/DitoTypeTreeList.vue +193 -0
- package/src/types/DitoTypeUpload.vue +503 -0
- package/src/types/index.js +30 -0
- package/src/utils/SchemaGraph.js +147 -0
- package/src/utils/accessor.js +75 -0
- package/src/utils/agent.js +47 -0
- package/src/utils/data.js +92 -0
- package/src/utils/filter.js +266 -0
- package/src/utils/math.js +14 -0
- package/src/utils/options.js +48 -0
- package/src/utils/path.js +5 -0
- package/src/utils/resource.js +44 -0
- package/src/utils/route.js +53 -0
- package/src/utils/schema.js +1121 -0
- package/src/utils/type.js +81 -0
- package/src/utils/uid.js +15 -0
- package/src/utils/units.js +5 -0
- package/src/validators/_creditcard.js +6 -0
- package/src/validators/_decimals.js +11 -0
- package/src/validators/_domain.js +6 -0
- package/src/validators/_email.js +6 -0
- package/src/validators/_hostname.js +6 -0
- package/src/validators/_integer.js +6 -0
- package/src/validators/_max.js +6 -0
- package/src/validators/_min.js +6 -0
- package/src/validators/_password.js +5 -0
- package/src/validators/_range.js +6 -0
- package/src/validators/_required.js +9 -0
- package/src/validators/_url.js +6 -0
- package/src/validators/index.js +12 -0
- package/src/verbs.js +17 -0
- package/types/index.d.ts +3298 -0
- package/types/tests/admin.test-d.ts +27 -0
- package/types/tests/component-buttons.test-d.ts +44 -0
- package/types/tests/component-list.test-d.ts +159 -0
- package/types/tests/component-misc.test-d.ts +137 -0
- package/types/tests/component-object.test-d.ts +69 -0
- package/types/tests/component-section.test-d.ts +174 -0
- package/types/tests/component-select.test-d.ts +107 -0
- package/types/tests/components.test-d.ts +81 -0
- package/types/tests/context.test-d.ts +31 -0
- package/types/tests/fixtures.ts +24 -0
- package/types/tests/form.test-d.ts +109 -0
- package/types/tests/instance.test-d.ts +20 -0
- package/types/tests/schema-features.test-d.ts +402 -0
- package/types/tests/variance.test-d.ts +125 -0
- package/types/tests/view.test-d.ts +146 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// NOTE: index.js exports nothing, but type components will be registered in
|
|
2
|
+
// DitoComponent and can be retrieved through DitoTypeComponent.get(type) and
|
|
3
|
+
// rendered through their component-names (e.g. dito-type-list).
|
|
4
|
+
|
|
5
|
+
export { default as DitoTypeButton } from './DitoTypeButton.vue'
|
|
6
|
+
export { default as DitoTypeCheckbox } from './DitoTypeCheckbox.vue'
|
|
7
|
+
export { default as DitoTypeCheckboxes } from './DitoTypeCheckboxes.vue'
|
|
8
|
+
export { default as DitoTypeCode } from './DitoTypeCode.vue'
|
|
9
|
+
export { default as DitoTypeColor } from './DitoTypeColor.vue'
|
|
10
|
+
export { default as DitoDitoTypeComponent } from './DitoTypeComponent.vue'
|
|
11
|
+
export { default as DitoTypeComputed } from './DitoTypeComputed.vue'
|
|
12
|
+
export { default as DitoTypeDate } from './DitoTypeDate.vue'
|
|
13
|
+
export { default as DitoTypeList } from './DitoTypeList.vue'
|
|
14
|
+
export { default as DitoTypeLabel } from './DitoTypeLabel.vue'
|
|
15
|
+
export { default as DitoTypeMarkup } from './DitoTypeMarkup.vue'
|
|
16
|
+
export { default as DitoTypeMultiselect } from './DitoTypeMultiselect.vue'
|
|
17
|
+
export { default as DitoTypeNumber } from './DitoTypeNumber.vue'
|
|
18
|
+
export { default as DitoTypeObject } from './DitoTypeObject.vue'
|
|
19
|
+
export { default as DitoTypePanel } from './DitoTypePanel.vue'
|
|
20
|
+
export { default as DitoTypeProgress } from './DitoTypeProgress.vue'
|
|
21
|
+
export { default as DitoTypeRadio } from './DitoTypeRadio.vue'
|
|
22
|
+
export { default as DitoTypeSection } from './DitoTypeSection.vue'
|
|
23
|
+
export { default as DitoTypeSelect } from './DitoTypeSelect.vue'
|
|
24
|
+
export { default as DitoTypeSlider } from './DitoTypeSlider.vue'
|
|
25
|
+
export { default as DitoTypeSpacer } from './DitoTypeSpacer.vue'
|
|
26
|
+
export { default as DitoTypeSwitch } from './DitoTypeSwitch.vue'
|
|
27
|
+
export { default as DitoTypeText } from './DitoTypeText.vue'
|
|
28
|
+
export { default as DitoTypeTextarea } from './DitoTypeTextarea.vue'
|
|
29
|
+
export { default as DitoTypeTreeList } from './DitoTypeTreeList.vue'
|
|
30
|
+
export { default as DitoTypeUpload } from './DitoTypeUpload.vue'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isTemporaryId } from './data.js'
|
|
2
|
+
import {
|
|
3
|
+
isInteger,
|
|
4
|
+
asArray,
|
|
5
|
+
parseDataPath,
|
|
6
|
+
getValueAtDataPath
|
|
7
|
+
} from '@ditojs/utils'
|
|
8
|
+
import { nanoid } from 'nanoid'
|
|
9
|
+
|
|
10
|
+
// SchemaGraph is a class to collect schema graph meta information in order to
|
|
11
|
+
// process sources and relations for the given targets 'server' and 'clipboard',
|
|
12
|
+
// according to the following table:
|
|
13
|
+
//
|
|
14
|
+
// | --------------------------------------------| --------- | --------- |
|
|
15
|
+
// | data | server | clipboard |
|
|
16
|
+
// | --------------------------------------------| --------- | --------- |
|
|
17
|
+
// | type: 'relation', internal: false | keep id | keep id |
|
|
18
|
+
// | type: 'relation', internal: true | keep id | ref, #ref |
|
|
19
|
+
// | type: 'relation', internal: true, temporary | ref, #ref | ref, #ref |
|
|
20
|
+
// | type: 'source', related: false | keep id | remove id |
|
|
21
|
+
// | type: 'source', related: false, temporary | ref, #id | remove id |
|
|
22
|
+
// | type: 'source', related: true | keep id | ref, #id |
|
|
23
|
+
// | type: 'source', related: true, temporary | ref, #id | ref, #id |
|
|
24
|
+
// | --------------------------------------------| --------- | --------- |
|
|
25
|
+
|
|
26
|
+
export class SchemaGraph {
|
|
27
|
+
graph = {}
|
|
28
|
+
references = {}
|
|
29
|
+
|
|
30
|
+
set(dataPath, settings, defaults) {
|
|
31
|
+
dataPath = parseDataPath(dataPath)
|
|
32
|
+
let subGraph = this.graph
|
|
33
|
+
for (const part of dataPath) {
|
|
34
|
+
const key = isInteger(+part) ? '*' : part
|
|
35
|
+
subGraph = subGraph[key] ??= {}
|
|
36
|
+
}
|
|
37
|
+
subGraph.$settings = {
|
|
38
|
+
...defaults, // See `addSource(dataPath)`
|
|
39
|
+
...subGraph.$settings,
|
|
40
|
+
...settings
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
addSource(dataPath, schema) {
|
|
45
|
+
// Only set `related: false` through the defaults, as `setSourceRelated()`
|
|
46
|
+
// may be called before `addSource()`, depending on the graph structure.
|
|
47
|
+
this.set(dataPath, { type: 'source', schema }, { related: false })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setSourceRelated(dataPath) {
|
|
51
|
+
this.set(dataPath, {
|
|
52
|
+
related: true,
|
|
53
|
+
reference: this.getReferencePrefix(dataPath)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addRelation(dataPath, relatedDataPath, schema) {
|
|
58
|
+
this.set(dataPath, {
|
|
59
|
+
type: 'relation',
|
|
60
|
+
schema,
|
|
61
|
+
internal: !!relatedDataPath,
|
|
62
|
+
reference: this.getReferencePrefix(relatedDataPath)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getReferencePrefix(dataPath) {
|
|
67
|
+
return dataPath
|
|
68
|
+
? (this.references[dataPath] ??= nanoid(6))
|
|
69
|
+
: null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
flatten() {
|
|
73
|
+
const flatten = graph => {
|
|
74
|
+
const entries = []
|
|
75
|
+
for (const [key, { $settings, ...subGraph }] of Object.entries(graph)) {
|
|
76
|
+
if ($settings) {
|
|
77
|
+
entries.push([key, $settings])
|
|
78
|
+
}
|
|
79
|
+
for (const [subKey, settings] of flatten(subGraph)) {
|
|
80
|
+
entries.push([`${key}/${subKey}`, settings])
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return entries
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return flatten(this.graph)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process(sourceSchema, data, { target }) {
|
|
90
|
+
const clipboard = target === 'clipboard'
|
|
91
|
+
if (clipboard) {
|
|
92
|
+
delete data[sourceSchema.idKey || 'id']
|
|
93
|
+
}
|
|
94
|
+
for (const [dataPath, settings] of this.flatten()) {
|
|
95
|
+
const { type, schema, internal, related, reference } = settings
|
|
96
|
+
const source = type === 'source'
|
|
97
|
+
const relation = type === 'relation'
|
|
98
|
+
if (source || relation && internal) {
|
|
99
|
+
const values = getValueAtDataPath(data, dataPath, () => null)
|
|
100
|
+
const removeId = clipboard && source && !related
|
|
101
|
+
const referenceId = (
|
|
102
|
+
clipboard && (
|
|
103
|
+
relation && internal ||
|
|
104
|
+
source && related
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
for (const value of asArray(values).flat()) {
|
|
108
|
+
const idKey = (
|
|
109
|
+
source && schema.idKey ||
|
|
110
|
+
relation && schema.relateBy ||
|
|
111
|
+
'id'
|
|
112
|
+
)
|
|
113
|
+
let id = value?.[idKey]
|
|
114
|
+
if (id != null) {
|
|
115
|
+
if (removeId) {
|
|
116
|
+
delete value[idKey]
|
|
117
|
+
}
|
|
118
|
+
if (referenceId || isTemporaryId(id)) {
|
|
119
|
+
if (isTemporaryId(id)) {
|
|
120
|
+
id = id.slice(1)
|
|
121
|
+
}
|
|
122
|
+
const refKey = clipboard
|
|
123
|
+
? // Clipboard just needs temporary ids under the actual `idKey`
|
|
124
|
+
idKey
|
|
125
|
+
: // Server wants Objection-style '#id' / '#ref' pairs.
|
|
126
|
+
source
|
|
127
|
+
? '#id'
|
|
128
|
+
: '#ref'
|
|
129
|
+
const revValue = clipboard
|
|
130
|
+
? `@${id}`
|
|
131
|
+
: // Keep the ids unique in reference groups, since they
|
|
132
|
+
// reference across the full graph.
|
|
133
|
+
reference
|
|
134
|
+
? `${reference}-${id}`
|
|
135
|
+
: id // A temporary id without a related, just preserve it.
|
|
136
|
+
value[refKey] = revValue
|
|
137
|
+
if (refKey !== idKey) {
|
|
138
|
+
delete value[idKey]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return data
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isFunction,
|
|
3
|
+
isString,
|
|
4
|
+
parseDataPath,
|
|
5
|
+
normalizeDataPath
|
|
6
|
+
} from '@ditojs/utils'
|
|
7
|
+
|
|
8
|
+
export function getSchemaAccessor(
|
|
9
|
+
keyOrDataPath,
|
|
10
|
+
{ type, default: def, get, set, callback = true } = {}
|
|
11
|
+
) {
|
|
12
|
+
// `keyOrDataPath` can be a simple property key,
|
|
13
|
+
// or a data-path into sub-properties, both in array or string format.
|
|
14
|
+
if (isString(keyOrDataPath) && keyOrDataPath.includes('.')) {
|
|
15
|
+
keyOrDataPath = parseDataPath(keyOrDataPath)
|
|
16
|
+
}
|
|
17
|
+
// Use the normalized data path for the handling overrides
|
|
18
|
+
const name = normalizeDataPath(keyOrDataPath)
|
|
19
|
+
return {
|
|
20
|
+
get() {
|
|
21
|
+
// Only determine schema value if we have no getter, or the getter
|
|
22
|
+
// wants to receive the value and process it further:
|
|
23
|
+
const value =
|
|
24
|
+
!get || get.length > 0
|
|
25
|
+
? // NOTE: Because `schema` objects are retrieved from `meta`, they
|
|
26
|
+
// don't seem to be reactive. To allow changed in `schema` values,
|
|
27
|
+
// `set()` stores changed values in the separate `overrides` object.
|
|
28
|
+
this.overrides && name in this.overrides
|
|
29
|
+
? this.overrides[name]
|
|
30
|
+
: this.getSchemaValue(keyOrDataPath, {
|
|
31
|
+
type,
|
|
32
|
+
default: def,
|
|
33
|
+
callback
|
|
34
|
+
})
|
|
35
|
+
: undefined
|
|
36
|
+
return get ? get.call(this, value) : value
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
set(value) {
|
|
40
|
+
if (set) {
|
|
41
|
+
set.call(this, value)
|
|
42
|
+
} else {
|
|
43
|
+
this.overrides ||= {}
|
|
44
|
+
this.overrides[name] = value
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getStoreAccessor(name, { default: def, get, set } = {}) {
|
|
51
|
+
return {
|
|
52
|
+
get() {
|
|
53
|
+
let value = this.getStore(name)
|
|
54
|
+
if (value === undefined && def !== undefined) {
|
|
55
|
+
// Support `default()` functions:
|
|
56
|
+
value = isFunction(def) ? def.call(this) : def
|
|
57
|
+
// Trigger setter by setting value and accessor to default:
|
|
58
|
+
this[name] = value
|
|
59
|
+
// Now access store again, for reactivity tracking
|
|
60
|
+
this.getStore(name)
|
|
61
|
+
}
|
|
62
|
+
// Allow the provided getter to further change or process the value
|
|
63
|
+
// retrieved from the store:
|
|
64
|
+
return get ? get.call(this, value) : value
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
set(value) {
|
|
68
|
+
// Allow the provided setter to return a new value to set, or do the
|
|
69
|
+
// setting itself, and then return `undefined`:
|
|
70
|
+
if (!set || (value = set.call(this, value)) !== undefined) {
|
|
71
|
+
this.setStore(name, value)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// This user-agent parser was lifted from Paper.js:
|
|
2
|
+
// https://github.com/paperjs/paper.js/blob/cc15696750035ab00e00c64c7c95daa2c85efe01/src/core/PaperScope.js#L75-L107
|
|
3
|
+
|
|
4
|
+
export function parseUserAgent(userAgent = '') {
|
|
5
|
+
const agent = {}
|
|
6
|
+
// Use replace() to get all matches, and deal with Chrome/Webkit overlap:
|
|
7
|
+
const ua = userAgent.toLowerCase()
|
|
8
|
+
const [os] =
|
|
9
|
+
/(iphone|ipad|linux; android|darwin|win|mac|linux|freebsd|sunos)/.exec(
|
|
10
|
+
ua
|
|
11
|
+
) || []
|
|
12
|
+
const platform = (
|
|
13
|
+
{
|
|
14
|
+
'darwin': 'mac',
|
|
15
|
+
'iphone': 'ios',
|
|
16
|
+
'ipad': 'ios',
|
|
17
|
+
'linux; android': 'android'
|
|
18
|
+
}[os] ||
|
|
19
|
+
os
|
|
20
|
+
)
|
|
21
|
+
if (platform) {
|
|
22
|
+
agent.platform = platform
|
|
23
|
+
agent[platform] = true
|
|
24
|
+
}
|
|
25
|
+
ua.replace(
|
|
26
|
+
/(opera|chrome|safari|webkit|firefox|msie|trident)\/?\s*([.\d]+)(?:.*version\/([.\d]+))?(?:.*rv:v?([.\d]+))?/g,
|
|
27
|
+
(match, browser, v1, v2, rv) => {
|
|
28
|
+
// Do not set additional browsers once chrome is detected.
|
|
29
|
+
if (!agent.chrome) {
|
|
30
|
+
const version = rv || v2 || v1
|
|
31
|
+
if (!agent.version || browser !== 'safari') {
|
|
32
|
+
// Use the version we get for webkit for Safari, which is actually
|
|
33
|
+
// The Safari version, e.g. 16.0
|
|
34
|
+
agent.version = version
|
|
35
|
+
agent.versionNumber = parseFloat(version)
|
|
36
|
+
}
|
|
37
|
+
agent.browser = browser
|
|
38
|
+
agent[browser] = true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
if (agent.chrome) {
|
|
43
|
+
// Can't have it both ways, Chrome.
|
|
44
|
+
delete agent.webkit
|
|
45
|
+
}
|
|
46
|
+
return agent
|
|
47
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isInteger,
|
|
3
|
+
parseDataPath,
|
|
4
|
+
getValueAtDataPath,
|
|
5
|
+
normalizeDataPath
|
|
6
|
+
} from '@ditojs/utils'
|
|
7
|
+
|
|
8
|
+
export function appendDataPath(dataPath, token) {
|
|
9
|
+
return dataPath
|
|
10
|
+
? `${dataPath}/${token}`
|
|
11
|
+
: token
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseParentDataPath(dataPath) {
|
|
15
|
+
const path = parseDataPath(dataPath)
|
|
16
|
+
path?.pop()
|
|
17
|
+
return path
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getParentDataPath(dataPath) {
|
|
21
|
+
return normalizeDataPath(parseParentDataPath(dataPath))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseItemDataPath(dataPath, nested = false) {
|
|
25
|
+
return nested ? parseParentDataPath(dataPath) : parseDataPath(dataPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getItemDataPath(dataPath, nested = false) {
|
|
29
|
+
return normalizeDataPath(parseItemDataPath(dataPath, nested))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseParentItemDataPath(dataPath, nested = false) {
|
|
33
|
+
const path = parseItemDataPath(dataPath, nested)
|
|
34
|
+
if (path) {
|
|
35
|
+
// Remove the parent token. If it's a number, then we're dealing with an
|
|
36
|
+
// array and need to remove more tokens until we meet the actual parent:
|
|
37
|
+
let token
|
|
38
|
+
do {
|
|
39
|
+
token = path.pop()
|
|
40
|
+
} while (token != null && isInteger(+token))
|
|
41
|
+
// If the removed token is valid, we can get the parent data:
|
|
42
|
+
if (token != null) {
|
|
43
|
+
return path
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getParentItemDataPath(dataPath, nested = false) {
|
|
50
|
+
return normalizeDataPath(parseParentItemDataPath(dataPath, nested))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getItem(rootItem, dataPath, nested = false) {
|
|
54
|
+
const path = parseItemDataPath(dataPath, nested)
|
|
55
|
+
return path ? getValueAtDataPath(rootItem, path) : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getParentItem(rootItem, dataPath, nested = false) {
|
|
59
|
+
const path = parseParentItemDataPath(dataPath, nested)
|
|
60
|
+
return path ? getValueAtDataPath(rootItem, path) : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getLastDataPathToken(dataPath) {
|
|
64
|
+
const path = parseDataPath(dataPath)
|
|
65
|
+
return path[path.length - 1]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getLastDataPathName(dataPath) {
|
|
69
|
+
const token = getLastDataPathToken(dataPath)
|
|
70
|
+
return token == null || isInteger(+token) ? null : token
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getLastDataPathIndex(dataPath) {
|
|
74
|
+
const token = getLastDataPathToken(dataPath)
|
|
75
|
+
const index = token == null ? null : +token
|
|
76
|
+
return isInteger(index) ? index : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let temporaryId = 0
|
|
80
|
+
export function setTemporaryId(data, idKey = 'id') {
|
|
81
|
+
// Temporary ids are marked with a '@' at the beginning.
|
|
82
|
+
data[idKey] = `@${++temporaryId}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isTemporaryId(id) {
|
|
86
|
+
return /^@/.test(id)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isReference(data, idKey = 'id') {
|
|
90
|
+
// Returns true if value is an object that holds nothing more than an id.
|
|
91
|
+
return data?.[idKey] != null && Object.keys(data).length === 1
|
|
92
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { isArray, asArray, labelize } from '@ditojs/utils'
|
|
2
|
+
import { getNamedSchemas, processNestedSchemaDefaults } from './schema'
|
|
3
|
+
|
|
4
|
+
export const filterComponents = {
|
|
5
|
+
'text'(filter) {
|
|
6
|
+
const options = [
|
|
7
|
+
{
|
|
8
|
+
label: 'contains',
|
|
9
|
+
value: 'contains'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: 'equals',
|
|
13
|
+
value: 'equals'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: 'starts with',
|
|
17
|
+
value: 'starts-with'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'ends with',
|
|
21
|
+
value: 'ends-with'
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
return {
|
|
25
|
+
operator: filter.operators
|
|
26
|
+
? {
|
|
27
|
+
type: 'select',
|
|
28
|
+
width: '2/5',
|
|
29
|
+
options: isArray(filter.operators)
|
|
30
|
+
? options.filter(
|
|
31
|
+
option => filter.operators.includes(option.value)
|
|
32
|
+
)
|
|
33
|
+
: options,
|
|
34
|
+
clearable: true
|
|
35
|
+
}
|
|
36
|
+
: null,
|
|
37
|
+
text: {
|
|
38
|
+
type: 'text',
|
|
39
|
+
width: filter.operators ? '3/5' : 'fill',
|
|
40
|
+
clearable: true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
'date-range'() {
|
|
46
|
+
const datetime = {
|
|
47
|
+
type: 'datetime',
|
|
48
|
+
width: '1/2',
|
|
49
|
+
formats: {
|
|
50
|
+
// Use shorter date format in date-range filters:
|
|
51
|
+
date: {
|
|
52
|
+
day: '2-digit',
|
|
53
|
+
month: '2-digit',
|
|
54
|
+
year: 'numeric'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
clearable: true
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
from: datetime,
|
|
61
|
+
to: datetime
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createFiltersPanel(api, filters, dataPath, query) {
|
|
67
|
+
const { sticky, ...filterSchemas } = filters
|
|
68
|
+
const panel = {
|
|
69
|
+
type: 'panel',
|
|
70
|
+
label: 'Filters',
|
|
71
|
+
name: '$filters',
|
|
72
|
+
// Override the default value
|
|
73
|
+
disabled: false,
|
|
74
|
+
sticky,
|
|
75
|
+
|
|
76
|
+
// NOTE: On panels, the data() callback does something else than on normal
|
|
77
|
+
// schema: It produces the `data` property to be passed to the panel's
|
|
78
|
+
// schema, not the data to be used for the panel component directly.
|
|
79
|
+
data() {
|
|
80
|
+
return parseFiltersData(
|
|
81
|
+
panel,
|
|
82
|
+
query.value
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
components: createFiltersComponents(filterSchemas),
|
|
87
|
+
buttons: createFiltersButtons(false),
|
|
88
|
+
panelButtons: createFiltersButtons(true),
|
|
89
|
+
|
|
90
|
+
events: {
|
|
91
|
+
change() {
|
|
92
|
+
this.applyFilters()
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
computed: {
|
|
97
|
+
filters() {
|
|
98
|
+
return formatFiltersData(this.schema, this.data)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
hasFilters() {
|
|
102
|
+
return this.filters.length > 0
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
methods: {
|
|
107
|
+
applyFilters() {
|
|
108
|
+
query.value = {
|
|
109
|
+
...query.value,
|
|
110
|
+
filter: this.filters,
|
|
111
|
+
// Clear pagination when applying or clearing filters:
|
|
112
|
+
page: undefined
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
clearFilters() {
|
|
117
|
+
this.resetData()
|
|
118
|
+
this.applyFilters()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
processNestedSchemaDefaults(api, panel)
|
|
123
|
+
return panel
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createFiltersButtons(small) {
|
|
127
|
+
return {
|
|
128
|
+
clear: {
|
|
129
|
+
type: 'button',
|
|
130
|
+
text: small ? null : 'Clear',
|
|
131
|
+
disabled: ({ schemaComponent }) => !schemaComponent.hasFilters,
|
|
132
|
+
events: {
|
|
133
|
+
click({ schemaComponent }) {
|
|
134
|
+
// Since panel buttons are outside of the schema, we need to use the
|
|
135
|
+
// schema component received from the initialize event below:
|
|
136
|
+
schemaComponent.clearFilters()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
submit: {
|
|
142
|
+
type: 'submit',
|
|
143
|
+
text: small ? null : 'Filter',
|
|
144
|
+
visible: !small,
|
|
145
|
+
events: {
|
|
146
|
+
click({ schemaComponent }) {
|
|
147
|
+
schemaComponent.applyFilters()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getDataName(filterName) {
|
|
155
|
+
// Prefix filter data keys with '$' to avoid conflicts with other data keys:
|
|
156
|
+
return `$${filterName}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getFilterName(dataName) {
|
|
160
|
+
return dataName.startsWith('$') ? dataName.slice(1) : null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createFiltersComponents(filters) {
|
|
164
|
+
const comps = {}
|
|
165
|
+
for (const filter of Object.values(getNamedSchemas(filters) || {})) {
|
|
166
|
+
// Support both custom forms and default filter components, through the
|
|
167
|
+
// `filterComponents` registry. Even for default filters, still use the
|
|
168
|
+
// properties in `filter` as the base for `form`, so things like `label`
|
|
169
|
+
// can be changed on the resulting form.
|
|
170
|
+
const { filter: type, width, ...form } = filter
|
|
171
|
+
const components = type
|
|
172
|
+
? filterComponents[type]?.(filter)
|
|
173
|
+
: filter.components
|
|
174
|
+
if (components) {
|
|
175
|
+
form.type = 'form'
|
|
176
|
+
form.components = {}
|
|
177
|
+
// Convert labels to placeholders:
|
|
178
|
+
for (const [key, component] of Object.entries(components)) {
|
|
179
|
+
if (component) {
|
|
180
|
+
const label = component.label || labelize(component.name || key)
|
|
181
|
+
form.components[key] = {
|
|
182
|
+
...component,
|
|
183
|
+
label: false,
|
|
184
|
+
placeholder: label
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
comps[getDataName(filter.name)] = {
|
|
189
|
+
label: form.label ?? labelize(filter.name),
|
|
190
|
+
type: 'object',
|
|
191
|
+
width,
|
|
192
|
+
default: () => ({}),
|
|
193
|
+
form,
|
|
194
|
+
inlined: true
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Invalid filter '${filter.name}': Unknown filter type '${type}'.`
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return comps
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getComponentsForFilter(schema, dataName) {
|
|
206
|
+
const component = schema.components[dataName]
|
|
207
|
+
return component?.form?.components
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function formatFiltersData(schema, data) {
|
|
211
|
+
const filters = []
|
|
212
|
+
for (const dataName in data) {
|
|
213
|
+
const entry = data[dataName]
|
|
214
|
+
if (entry) {
|
|
215
|
+
// Map components sequence to arguments:
|
|
216
|
+
const args = Object.keys(
|
|
217
|
+
getComponentsForFilter(schema, dataName)
|
|
218
|
+
).map(
|
|
219
|
+
key => entry[key] ?? null
|
|
220
|
+
)
|
|
221
|
+
// Only apply filter if there are some arguments that aren't null:
|
|
222
|
+
if (args.some(value => value !== null)) {
|
|
223
|
+
filters.push(
|
|
224
|
+
`${
|
|
225
|
+
getFilterName(dataName)
|
|
226
|
+
}:${
|
|
227
|
+
args.map(JSON.stringify).join(',')
|
|
228
|
+
}`
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return filters
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseFiltersData(schema, query) {
|
|
237
|
+
const filters = {}
|
|
238
|
+
// Same as @ditojs/server's QueryParameters.filter: Translate the string data
|
|
239
|
+
// from $route.query back to param lists per filter:
|
|
240
|
+
if (query) {
|
|
241
|
+
for (const filter of asArray(query.filter)) {
|
|
242
|
+
const [, filterName, json] = filter.match(/^(\w+):(.*)$/)
|
|
243
|
+
try {
|
|
244
|
+
filters[filterName] = asArray(JSON.parse(`[${json}]`))
|
|
245
|
+
} catch {}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const filtersData = {}
|
|
249
|
+
for (const dataName in schema.components) {
|
|
250
|
+
const data = {}
|
|
251
|
+
// If we have retrieved params from the query, fetch the associated
|
|
252
|
+
// form components so we can map the values back to object keys:
|
|
253
|
+
const args = filters[getFilterName(dataName)]
|
|
254
|
+
if (args) {
|
|
255
|
+
const components = getComponentsForFilter(schema, dataName)
|
|
256
|
+
if (components) {
|
|
257
|
+
let index = 0
|
|
258
|
+
for (const key in components) {
|
|
259
|
+
data[key] = args[index++]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
filtersData[dataName] = data
|
|
264
|
+
}
|
|
265
|
+
return filtersData
|
|
266
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isString } from '@ditojs/utils'
|
|
2
|
+
|
|
3
|
+
export function parseFraction(value) {
|
|
4
|
+
const match = (
|
|
5
|
+
isString(value) &&
|
|
6
|
+
value.match(/^\s*([+-]?\d+)\s*\/\s*([+-]?\d+)\s*$/)
|
|
7
|
+
)
|
|
8
|
+
if (match) {
|
|
9
|
+
const [, dividend, divisor] = match
|
|
10
|
+
return parseFloat(dividend) / parseFloat(divisor)
|
|
11
|
+
} else {
|
|
12
|
+
return parseFloat(value)
|
|
13
|
+
}
|
|
14
|
+
}
|