@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.
Files changed (153) hide show
  1. package/README.md +180 -0
  2. package/dist/dito-admin.css +1 -0
  3. package/dist/dito-admin.es.js +12106 -0
  4. package/dist/dito-admin.umd.js +7 -0
  5. package/package.json +96 -0
  6. package/src/DitoAdmin.js +293 -0
  7. package/src/DitoComponent.js +34 -0
  8. package/src/DitoContext.js +318 -0
  9. package/src/DitoTypeComponent.js +42 -0
  10. package/src/DitoUser.js +12 -0
  11. package/src/appState.js +12 -0
  12. package/src/components/DitoAccount.vue +60 -0
  13. package/src/components/DitoAffix.vue +68 -0
  14. package/src/components/DitoAffixes.vue +200 -0
  15. package/src/components/DitoButtons.vue +80 -0
  16. package/src/components/DitoClipboard.vue +186 -0
  17. package/src/components/DitoContainer.vue +374 -0
  18. package/src/components/DitoCreateButton.vue +146 -0
  19. package/src/components/DitoDialog.vue +242 -0
  20. package/src/components/DitoDraggable.vue +117 -0
  21. package/src/components/DitoEditButtons.vue +135 -0
  22. package/src/components/DitoErrors.vue +83 -0
  23. package/src/components/DitoForm.vue +521 -0
  24. package/src/components/DitoFormInner.vue +26 -0
  25. package/src/components/DitoFormNested.vue +17 -0
  26. package/src/components/DitoHeader.vue +84 -0
  27. package/src/components/DitoLabel.vue +200 -0
  28. package/src/components/DitoMenu.vue +186 -0
  29. package/src/components/DitoNavigation.vue +40 -0
  30. package/src/components/DitoNotifications.vue +170 -0
  31. package/src/components/DitoPagination.vue +42 -0
  32. package/src/components/DitoPane.vue +334 -0
  33. package/src/components/DitoPanel.vue +256 -0
  34. package/src/components/DitoPanels.vue +61 -0
  35. package/src/components/DitoRoot.vue +524 -0
  36. package/src/components/DitoSchema.vue +846 -0
  37. package/src/components/DitoSchemaInlined.vue +97 -0
  38. package/src/components/DitoScopes.vue +76 -0
  39. package/src/components/DitoSidebar.vue +50 -0
  40. package/src/components/DitoSpinner.vue +95 -0
  41. package/src/components/DitoTableCell.vue +64 -0
  42. package/src/components/DitoTableHead.vue +121 -0
  43. package/src/components/DitoTabs.vue +103 -0
  44. package/src/components/DitoTrail.vue +124 -0
  45. package/src/components/DitoTreeItem.vue +420 -0
  46. package/src/components/DitoUploadFile.vue +199 -0
  47. package/src/components/DitoVNode.vue +14 -0
  48. package/src/components/DitoView.vue +143 -0
  49. package/src/components/index.js +42 -0
  50. package/src/directives/resize.js +83 -0
  51. package/src/index.js +1 -0
  52. package/src/mixins/ContextMixin.js +68 -0
  53. package/src/mixins/DataMixin.js +131 -0
  54. package/src/mixins/DitoMixin.js +591 -0
  55. package/src/mixins/DomMixin.js +29 -0
  56. package/src/mixins/EmitterMixin.js +158 -0
  57. package/src/mixins/ItemMixin.js +144 -0
  58. package/src/mixins/LoadingMixin.js +23 -0
  59. package/src/mixins/NumberMixin.js +118 -0
  60. package/src/mixins/OptionsMixin.js +304 -0
  61. package/src/mixins/PulldownMixin.js +63 -0
  62. package/src/mixins/ResourceMixin.js +398 -0
  63. package/src/mixins/RouteMixin.js +190 -0
  64. package/src/mixins/SchemaParentMixin.js +33 -0
  65. package/src/mixins/SortableMixin.js +49 -0
  66. package/src/mixins/SourceMixin.js +734 -0
  67. package/src/mixins/TextMixin.js +26 -0
  68. package/src/mixins/TypeMixin.js +280 -0
  69. package/src/mixins/ValidationMixin.js +119 -0
  70. package/src/mixins/ValidatorMixin.js +57 -0
  71. package/src/mixins/ValueMixin.js +31 -0
  72. package/src/styles/_base.scss +17 -0
  73. package/src/styles/_button.scss +191 -0
  74. package/src/styles/_imports.scss +3 -0
  75. package/src/styles/_info.scss +19 -0
  76. package/src/styles/_layout.scss +19 -0
  77. package/src/styles/_pulldown.scss +38 -0
  78. package/src/styles/_scroll.scss +13 -0
  79. package/src/styles/_settings.scss +88 -0
  80. package/src/styles/_table.scss +223 -0
  81. package/src/styles/_tippy.scss +45 -0
  82. package/src/styles/style.scss +9 -0
  83. package/src/types/DitoTypeButton.vue +143 -0
  84. package/src/types/DitoTypeCheckbox.vue +27 -0
  85. package/src/types/DitoTypeCheckboxes.vue +65 -0
  86. package/src/types/DitoTypeCode.vue +199 -0
  87. package/src/types/DitoTypeColor.vue +272 -0
  88. package/src/types/DitoTypeComponent.vue +31 -0
  89. package/src/types/DitoTypeComputed.vue +50 -0
  90. package/src/types/DitoTypeDate.vue +99 -0
  91. package/src/types/DitoTypeLabel.vue +23 -0
  92. package/src/types/DitoTypeList.vue +364 -0
  93. package/src/types/DitoTypeMarkup.vue +700 -0
  94. package/src/types/DitoTypeMultiselect.vue +522 -0
  95. package/src/types/DitoTypeNumber.vue +66 -0
  96. package/src/types/DitoTypeObject.vue +136 -0
  97. package/src/types/DitoTypePanel.vue +18 -0
  98. package/src/types/DitoTypeProgress.vue +40 -0
  99. package/src/types/DitoTypeRadio.vue +45 -0
  100. package/src/types/DitoTypeSection.vue +80 -0
  101. package/src/types/DitoTypeSelect.vue +133 -0
  102. package/src/types/DitoTypeSlider.vue +66 -0
  103. package/src/types/DitoTypeSpacer.vue +11 -0
  104. package/src/types/DitoTypeSwitch.vue +40 -0
  105. package/src/types/DitoTypeText.vue +101 -0
  106. package/src/types/DitoTypeTextarea.vue +48 -0
  107. package/src/types/DitoTypeTreeList.vue +193 -0
  108. package/src/types/DitoTypeUpload.vue +503 -0
  109. package/src/types/index.js +30 -0
  110. package/src/utils/SchemaGraph.js +147 -0
  111. package/src/utils/accessor.js +75 -0
  112. package/src/utils/agent.js +47 -0
  113. package/src/utils/data.js +92 -0
  114. package/src/utils/filter.js +266 -0
  115. package/src/utils/math.js +14 -0
  116. package/src/utils/options.js +48 -0
  117. package/src/utils/path.js +5 -0
  118. package/src/utils/resource.js +44 -0
  119. package/src/utils/route.js +53 -0
  120. package/src/utils/schema.js +1121 -0
  121. package/src/utils/type.js +81 -0
  122. package/src/utils/uid.js +15 -0
  123. package/src/utils/units.js +5 -0
  124. package/src/validators/_creditcard.js +6 -0
  125. package/src/validators/_decimals.js +11 -0
  126. package/src/validators/_domain.js +6 -0
  127. package/src/validators/_email.js +6 -0
  128. package/src/validators/_hostname.js +6 -0
  129. package/src/validators/_integer.js +6 -0
  130. package/src/validators/_max.js +6 -0
  131. package/src/validators/_min.js +6 -0
  132. package/src/validators/_password.js +5 -0
  133. package/src/validators/_range.js +6 -0
  134. package/src/validators/_required.js +9 -0
  135. package/src/validators/_url.js +6 -0
  136. package/src/validators/index.js +12 -0
  137. package/src/verbs.js +17 -0
  138. package/types/index.d.ts +3298 -0
  139. package/types/tests/admin.test-d.ts +27 -0
  140. package/types/tests/component-buttons.test-d.ts +44 -0
  141. package/types/tests/component-list.test-d.ts +159 -0
  142. package/types/tests/component-misc.test-d.ts +137 -0
  143. package/types/tests/component-object.test-d.ts +69 -0
  144. package/types/tests/component-section.test-d.ts +174 -0
  145. package/types/tests/component-select.test-d.ts +107 -0
  146. package/types/tests/components.test-d.ts +81 -0
  147. package/types/tests/context.test-d.ts +31 -0
  148. package/types/tests/fixtures.ts +24 -0
  149. package/types/tests/form.test-d.ts +109 -0
  150. package/types/tests/instance.test-d.ts +20 -0
  151. package/types/tests/schema-features.test-d.ts +402 -0
  152. package/types/tests/variance.test-d.ts +125 -0
  153. 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
+ }